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', () {