Fix root-relative file URI handling.

According to the WHATWG spec, "/foo" should be considered relative to
the drive letter if one exists for file URIs.

See https://url.spec.whatwg.org/#file-slash-state
See #18
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2818f7b..3a43eea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.4.1
+
+* Root-relative URLs like `/foo` are now resolved relative to the drive letter
+  for `file` URLs that begin with a Windows-style drive letter. This matches the
+  [WHATWG URL specification][].
+
+[WHATWG URL specification]: https://url.spec.whatwg.org/#file-slash-state
+
 ## 1.4.0
 
 * Add `equals()`, `hash()` and `canonicalize()` top-level functions and
diff --git a/lib/src/context.dart b/lib/src/context.dart
index af97f90..c5ebb69 100644
--- a/lib/src/context.dart
+++ b/lib/src/context.dart
@@ -245,7 +245,9 @@
         // If the new part is root-relative, it preserves the previous root but
         // replaces the path after it.
         var parsed = _parse(part);
-        parsed.root = this.rootPrefix(buffer.toString());
+        var path = buffer.toString();
+        parsed.root = path.substring(
+            0, style.rootLength(path, withDrive: true));
         if (style.needsSeparator(parsed.root)) {
           parsed.separators[0] = style.separator;
         }
diff --git a/lib/src/internal_style.dart b/lib/src/internal_style.dart
index 1874afc..21897d6 100644
--- a/lib/src/internal_style.dart
+++ b/lib/src/internal_style.dart
@@ -33,10 +33,11 @@
 
   /// Returns the number of characters of the root part.
   ///
-  /// Returns 0 if the path is relative.
+  /// Returns 0 if the path is relative and 1 if the path is root-relative.
   ///
-  /// If the path is root-relative, the root length is 1.
-  int rootLength(String path);
+  /// If [withDrive] is `true`, this should include the drive letter for `file:`
+  /// URLs. Non-URL styles may ignore the parameter.
+  int rootLength(String path, {bool withDrive: false});
 
   /// Gets the root prefix of [path] if path is absolute. If [path] is relative,
   /// returns `null`.
diff --git a/lib/src/style/posix.dart b/lib/src/style/posix.dart
index 6f183fc..5044d43 100644
--- a/lib/src/style/posix.dart
+++ b/lib/src/style/posix.dart
@@ -28,7 +28,7 @@
   bool needsSeparator(String path) =>
       path.isNotEmpty && !isSeparator(path.codeUnitAt(path.length - 1));
 
-  int rootLength(String path) {
+  int rootLength(String path, {bool withDrive: false}) {
     if (path.isNotEmpty && isSeparator(path.codeUnitAt(0))) return 1;
     return 0;
   }
diff --git a/lib/src/style/url.dart b/lib/src/style/url.dart
index 659275c..f4bab64 100644
--- a/lib/src/style/url.dart
+++ b/lib/src/style/url.dart
@@ -4,6 +4,7 @@
 
 import '../characters.dart' as chars;
 import '../internal_style.dart';
+import '../utils.dart';
 
 /// The style for URL paths.
 class UrlStyle extends InternalStyle {
@@ -36,16 +37,23 @@
     return path.endsWith("://") && rootLength(path) == path.length;
   }
 
-  int rootLength(String path) {
+  int rootLength(String path, {bool withDrive: false}) {
     if (path.isEmpty) return 0;
     if (isSeparator(path.codeUnitAt(0))) return 1;
+
     var index = path.indexOf("/");
     if (index > 0 && path.startsWith('://', index - 1)) {
       // The root part is up until the next '/', or the full path. Skip
       // '://' and search for '/' after that.
       index = path.indexOf('/', index + 2);
-      if (index > 0) return index;
-      return path.length;
+      if (index <= 0) return path.length;
+
+      // file: URLs sometimes consider Windows drive letters part of the root.
+      // See https://url.spec.whatwg.org/#file-slash-state.
+      if (!withDrive || path.length < index + 3) return index;
+      if (!path.startsWith('file://')) return index;
+      if (!isDriveLetter(path, index + 1)) return index;
+      return path.length == index + 3 ? index + 3 : index + 4;
     }
     return 0;
   }
diff --git a/lib/src/style/windows.dart b/lib/src/style/windows.dart
index 57989af..ed54ab9 100644
--- a/lib/src/style/windows.dart
+++ b/lib/src/style/windows.dart
@@ -36,7 +36,7 @@
     return !isSeparator(path.codeUnitAt(path.length - 1));
   }
 
-  int rootLength(String path) {
+  int rootLength(String path, {bool withDrive: false}) {
     if (path.isEmpty) return 0;
     if (path.codeUnitAt(0) == chars.SLASH) return 1;
     if (path.codeUnitAt(0) == chars.BACKSLASH) {
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 64e471e..3d71e56 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -12,3 +12,13 @@
 
 /// Returns whether [char] is the code for an ASCII digit.
 bool isNumeric(int char) => char >= chars.ZERO && char <= chars.NINE;
+
+/// Returns whether [path] has a URL-formatted Windows drive letter beginning at
+/// [index].
+bool isDriveLetter(String path, int index) {
+  if (path.length < index + 2) return false;
+  if (!isAlphabetic(path.codeUnitAt(index))) return false;
+  if (path.codeUnitAt(index + 1) != chars.COLON) return false;
+  if (path.length == index + 2) return true;
+  return path.codeUnitAt(index + 2) == chars.SLASH;
+}
diff --git a/test/url_test.dart b/test/url_test.dart
index 32c02da..e8f3413 100644
--- a/test/url_test.dart
+++ b/test/url_test.dart
@@ -245,13 +245,33 @@
       expect(() => context.join(null, 'a'), throwsArgumentError);
     });
 
-    test('Join does not modify internal ., .., or trailing separators', () {
+    test('does not modify internal ., .., or trailing separators', () {
       expect(context.join('a/', 'b/c/'), 'a/b/c/');
       expect(context.join('a/b/./c/..//', 'd/.././..//e/f//'),
           'a/b/./c/..//d/.././..//e/f//');
       expect(context.join('a/b', 'c/../../../..'), 'a/b/c/../../../..');
       expect(context.join('a', 'b${context.separator}'), 'a/b/');
     });
+
+    test('treats drive letters as part of the root for file: URLs', () {
+      expect(context.join('file:///c:/foo/bar', '/baz/qux'),
+          'file:///c:/baz/qux');
+      expect(context.join('file:///D:/foo/bar', '/baz/qux'),
+          'file:///D:/baz/qux');
+      expect(context.join('file:///c:/', '/baz/qux'), 'file:///c:/baz/qux');
+      expect(context.join('file:///c:', '/baz/qux'), 'file:///c:/baz/qux');
+      expect(context.join('file://host/c:/foo/bar', '/baz/qux'),
+          'file://host/c:/baz/qux');
+    });
+
+    test('treats drive letters as normal components for non-file: URLs', () {
+      expect(context.join('http://foo.com/c:/foo/bar', '/baz/qux'),
+          'http://foo.com/baz/qux');
+      expect(context.join('misfile:///c:/foo/bar', '/baz/qux'),
+          'misfile:///baz/qux');
+      expect(context.join('filer:///c:/foo/bar', '/baz/qux'),
+          'filer:///baz/qux');
+    });
   });
 
   group('joinAll', () {