Merge pull request #20 from dart-lang/windows-drive-letters
Update Windows file URI behavior.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2818f7b..f7dde67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## 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
+
+* When a root-relative URLs like `/foo` is converted to a Windows path using
+ `fromUrl()`, it is now resolved relative to the drive letter. This matches
+ IE's behavior.
+
## 1.4.0
* Add `equals()`, `hash()` and `canonicalize()` top-level functions and
diff --git a/README.md b/README.md
index 5a172cd..5803470 100644
--- a/README.md
+++ b/README.md
@@ -41,13 +41,34 @@
This will join "directory" and "file.txt" using the Windows path separator,
even when the program is run on a POSIX machine.
+## Stability
+
+The `path` package is used by many Dart packages, and as such it strives for a
+very high degree of stability. For the same reason, though, releasing a new
+major version would probably cause a lot of versioning pain, so some flexibility
+is necessary.
+
+We try to guarantee that **operations with valid inputs and correct output will
+not change**. Operations where one or more inputs are invalid according to the
+semantics of the corresponding platform may produce different output over time.
+Operations for which `path` produces incorrect output will also change so that
+we can fix bugs.
+
+Also, the `path` package's URL handling is based on [the WHATWG URL spec][].
+This is a living standard, and some parts of it haven't yet been entirely
+solidified by vendor support. The `path` package reserves the right to change
+its URL behavior if the underlying specification changes, although if the change
+is big enough to break many valid uses we may elect to treat it as a breaking
+change anyway.
+
+[the WHATWG URL spec]: https://url.spec.whatwg.org/
+
## FAQ
### Where can I use this?
-Pathos runs on the Dart VM and in the browser under both dart2js and Dartium.
-Under dart2js, it currently returns "." as the current working directory, while
-under Dartium it returns the current URL.
+The `path` package runs on the Dart VM and in the browser under both dart2js and
+Dartium. On the browser, `window.location.href` is used as the current path.
### Why doesn't this make paths first-class objects?
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..31114db 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) {
@@ -78,8 +78,13 @@
var path = uri.path;
if (uri.host == '') {
// Drive-letter paths look like "file:///C:/path/to/file". The
- // replaceFirst removes the extra initial slash.
- if (path.startsWith('/')) path = path.replaceFirst("/", "");
+ // replaceFirst removes the extra initial slash. Otherwise, leave the
+ // slash to match IE's interpretation of "/foo" as a root-relative path.
+ if (path.length >= 3 &&
+ path.startsWith('/') &&
+ isDriveLetter(path, 1)) {
+ path = path.replaceFirst("/", "");
+ }
} else {
// Network paths look like "file://hostname/path/to/file".
path = '\\\\${uri.host}$path';
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', () {
diff --git a/test/windows_test.dart b/test/windows_test.dart
index f4453a7..ad58774 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -704,6 +704,7 @@
r'\\server\share\path\to\foo#bar');
expect(context.fromUri(Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')),
r'_{_}_`_^_ _"_%_');
+ expect(context.fromUri(Uri.parse('/foo')), r'\foo');
expect(() => context.fromUri(Uri.parse('http://dartlang.org')),
throwsArgumentError);
});