Make `Uri` treat `\` as `/` in path and authority.
When using `Uri.parse` or `Uri(path:..)`, a `\` is treated as, and converted to, a `/`.
This avoids a particular problematic difference in behavior between Dart and the browser's `URL` functionality. There are still examples where the two differ in interpretation of the same code, but in those cases, the Dart `Uri` will most likely end up without a host name, which should be easily detected.
Change-Id: I798df6c3c27c6d64fb9fc8dc30d90b06ba5a9004
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/258120
Reviewed-by: Nate Bosch <nbosch@google.com>
Reviewed-by: Michael Thomsen <mit@google.com>
Commit-Queue: Lasse Nielsen <lrn@google.com>
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart
index bb84862..3d27f1d 100644
--- a/sdk/lib/core/uri.dart
+++ b/sdk/lib/core/uri.dart
@@ -150,7 +150,8 @@
/// [pathSegments].
/// When [path] is used, it should be a valid URI path,
/// but invalid characters, except the general delimiters ':/@[]?#',
- /// will be escaped if necessary.
+ /// will be escaped if necessary. A backslash, `\`, will be converted
+ /// to a slash `/`.
/// When [pathSegments] is used, each of the provided segments
/// is first percent-encoded and then joined using the forward slash
/// separator.
@@ -237,7 +238,7 @@
///
/// The `path` component is set from the [unencodedPath]
/// argument. The path passed must not be encoded as this constructor
- /// encodes the path.
+ /// encodes the path. Only `/` is recognized as path separtor.
/// If omitted, the path defaults to being empty.
///
/// The `query` component is set from the optional [queryParameters]
@@ -997,6 +998,14 @@
// If the port is empty, it should be omitted.
// Pathological case, don't bother correcting it.
isSimple = false;
+ } else if (uri.startsWith(r"\", pathStart) ||
+ hostStart > start &&
+ (uri.startsWith(r"\", hostStart - 1) ||
+ uri.startsWith(r"\", hostStart - 2))) {
+ // Seeing a `\` anywhere.
+ // The scanner doesn't record when the first path character is a `\`
+ // or when the last slash before the authority is a `\`.
+ isSimple = false;
} else if (queryStart < end &&
(queryStart == pathStart + 2 &&
uri.startsWith("..", pathStart)) ||
@@ -2305,7 +2314,7 @@
throw ArgumentError('Both path and pathSegments specified');
} else {
result = _normalizeOrSubstring(path, start, end, _pathCharOrSlashTable,
- escapeDelimiters: true);
+ escapeDelimiters: true, replaceBackslash: true);
}
if (result.isEmpty) {
if (isFile) return "/";
@@ -2322,7 +2331,10 @@
/// "pure path" and normalization won't remove leading ".." segments.
/// Otherwise it follows the RFC 3986 "remove dot segments" algorithm.
static String _normalizePath(String path, String scheme, bool hasAuthority) {
- if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) {
+ if (scheme.isEmpty &&
+ !hasAuthority &&
+ !path.startsWith('/') &&
+ !path.startsWith(r'\')) {
return _normalizeRelativePath(path, scheme.isNotEmpty || hasAuthority);
}
return _removeDotSegments(path);
@@ -2451,9 +2463,10 @@
/// this methods returns the substring if [component] from [start] to [end].
static String _normalizeOrSubstring(
String component, int start, int end, List<int> charTable,
- {bool escapeDelimiters = false}) {
+ {bool escapeDelimiters = false, bool replaceBackslash = false}) {
return _normalize(component, start, end, charTable,
- escapeDelimiters: escapeDelimiters) ??
+ escapeDelimiters: escapeDelimiters,
+ replaceBackslash: replaceBackslash) ??
component.substring(start, end);
}
@@ -2468,7 +2481,7 @@
/// Returns `null` if the original content was already normalized.
static String? _normalize(
String component, int start, int end, List<int> charTable,
- {bool escapeDelimiters = false}) {
+ {bool escapeDelimiters = false, bool replaceBackslash = false}) {
StringBuffer? buffer;
int sectionStart = start;
int index = start;
@@ -2494,6 +2507,9 @@
} else {
sourceLength = 3;
}
+ } else if (char == _BACKSLASH && replaceBackslash) {
+ replacement = "/";
+ sourceLength = 1;
} else if (!escapeDelimiters && _isGeneralDelimiter(char)) {
_fail(component, index, "Invalid character");
throw "unreachable"; // TODO(lrn): Remove when Never-returning functions are recognized as throwing.
@@ -4197,6 +4213,7 @@
setChars(b, ".", schemeOrPathDot);
setChars(b, ":", authOrPath | schemeEnd); // Handle later.
setChars(b, "/", authOrPathSlash);
+ setChars(b, r"\", authOrPathSlash | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4204,7 +4221,7 @@
setChars(b, pchar, schemeOrPath);
setChars(b, ".", schemeOrPathDot2);
setChars(b, ':', authOrPath | schemeEnd);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4213,6 +4230,7 @@
setChars(b, "%", schemeOrPath | notSimple);
setChars(b, ':', authOrPath | schemeEnd);
setChars(b, "/", relPathSeg);
+ setChars(b, r"\", relPathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4220,12 +4238,14 @@
setChars(b, pchar, schemeOrPath);
setChars(b, ':', authOrPath | schemeEnd);
setChars(b, "/", pathSeg);
+ setChars(b, r"\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(authOrPath, path | notSimple);
setChars(b, pchar, path | pathStart);
setChars(b, "/", authOrPathSlash | pathStart);
+ setChars(b, r"\", authOrPathSlash | pathStart); // This should be non-simple.
setChars(b, ".", pathSegDot | pathStart);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4233,6 +4253,7 @@
b = build(authOrPathSlash, path | notSimple);
setChars(b, pchar, path);
setChars(b, "/", uinfoOrHost0 | hostStart);
+ setChars(b, r"\", uinfoOrHost0 | hostStart); // This should be non-simple.
setChars(b, ".", pathSegDot);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4244,6 +4265,7 @@
setChars(b, "@", uinfoOrHost0 | hostStart);
setChars(b, "[", ipv6Host | notSimple);
setChars(b, "/", pathSeg | pathStart);
+ setChars(b, r"\", pathSeg | pathStart); // This should be non-simple.
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4253,6 +4275,7 @@
setChars(b, ":", uinfoOrPort0 | portStart);
setChars(b, "@", uinfoOrHost0 | hostStart);
setChars(b, "/", pathSeg | pathStart);
+ setChars(b, r"\", pathSeg | pathStart); // This should be non-simple.
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4260,6 +4283,7 @@
setRange(b, "19", uinfoOrPort);
setChars(b, "@", uinfoOrHost0 | hostStart);
setChars(b, "/", pathSeg | pathStart);
+ setChars(b, r"\", pathSeg | pathStart); // This should be non-simple.
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4267,6 +4291,7 @@
setRange(b, "09", uinfoOrPort);
setChars(b, "@", uinfoOrHost0 | hostStart);
setChars(b, "/", pathSeg | pathStart);
+ setChars(b, r"\", pathSeg | pathStart); // This should be non-simple.
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
@@ -4276,46 +4301,48 @@
b = build(relPathSeg, path | notSimple);
setChars(b, pchar, path);
setChars(b, ".", relPathSegDot);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(relPathSegDot, path | notSimple);
setChars(b, pchar, path);
setChars(b, ".", relPathSegDot2);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(relPathSegDot2, path | notSimple);
setChars(b, pchar, path);
setChars(b, "/", relPathSeg);
+ setChars(b, r"\", relPathSeg | notSimple);
setChars(b, "?", query | queryStart); // This should be non-simple.
setChars(b, "#", fragment | fragmentStart); // This should be non-simple.
b = build(pathSeg, path | notSimple);
setChars(b, pchar, path);
setChars(b, ".", pathSegDot);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(pathSegDot, path | notSimple);
setChars(b, pchar, path);
setChars(b, ".", pathSegDot2);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(pathSegDot2, path | notSimple);
setChars(b, pchar, path);
- setChars(b, "/", pathSeg | notSimple);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
b = build(path, path | notSimple);
setChars(b, pchar, path);
setChars(b, "/", pathSeg);
+ setChars(b, r"/\", pathSeg | notSimple);
setChars(b, "?", query | queryStart);
setChars(b, "#", fragment | fragmentStart);
diff --git a/tests/corelib/uri_test.dart b/tests/corelib/uri_test.dart
index dcddc9e..a1310c7 100644
--- a/tests/corelib/uri_test.dart
+++ b/tests/corelib/uri_test.dart
@@ -573,6 +573,81 @@
uri.resolve("/qux").toString());
}
+void testBackslashes() {
+ // Tests change which makes `\` be treated as `/` in
+ // autority and path.
+
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:\\example.com\").toString());
+
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:\/example.com/").toString());
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:/\example.com/").toString());
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https://example.com/").toString());
+ Expect.stringEquals("https://example.com/foo//bar",
+ Uri.parse(r"https://example.com/foo\\bar").toString());
+
+ Expect.stringEquals("https:/example.com/foo?%5C#%5C",
+ Uri.parse(r"https:\example.com/foo?\#\").toString());
+
+ Expect.stringEquals("https://example.com/@example.net/foo",
+ Uri.parse(r"https://example.com\@example.net/foo").toString());
+
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\foo").toString());
+ Expect.stringEquals("file://foo/",
+ Uri.parse(r"file:\\foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\\\foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\//foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:/\/foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file://\foo").toString());
+
+ // No scheme.
+ Expect.stringEquals("//example.com/foo",
+ Uri.parse(r"\\example.com\foo").toString());
+
+ // No authority.
+ Expect.stringEquals("http:/foo",
+ Uri.parse(r"http:\foo").toString());
+
+ /// No scheme or authority.
+
+ Expect.stringEquals("foo/bar/baz",
+ Uri.parse(r"foo\bar\baz").toString());
+
+ Expect.stringEquals("foo/bar/baz",
+ Uri.parse(r"foo\bar\.\baz").toString());
+
+ Expect.stringEquals("foo/baz",
+ Uri.parse(r"foo\bar\..\baz").toString());
+
+ // Not converted to / in query or fragment, still escaped.
+ Expect.stringEquals("https://example.com/foo?%5C#%5C",
+ Uri.parse(r"https://example.com/foo?\#\").toString());
+
+ // Applies when a path is provided, but not when using path segments.
+ Expect.stringEquals("https://example.com/foo/bar",
+ Uri(scheme: "https", host: "example.com", path: r"\foo\bar").toString());
+
+ Expect.stringEquals("https://example.com/foo%5Cbar",
+ Uri(scheme: "https", host: "example.com", pathSegments: [r"foo\bar"])
+ .toString());
+
+ // Does not apply to constructors which expect an unencoded path.
+ Expect.stringEquals("http://example.com/%5Cfoo%5Cbar",
+ Uri.http("example.com", r"\foo\bar").toString());
+ Expect.stringEquals("https://example.com/%5Cfoo%5Cbar",
+ Uri.https("example.com", r"\foo\bar").toString());
+}
+
main() {
testUri("http:", true);
testUri("file:///", true);
@@ -725,6 +800,7 @@
testNormalization();
testReplace();
testPackageUris();
+ testBackslashes();
}
String dump(Uri uri) {
diff --git a/tests/corelib_2/uri_test.dart b/tests/corelib_2/uri_test.dart
index 3ac87ea..c78b483 100644
--- a/tests/corelib_2/uri_test.dart
+++ b/tests/corelib_2/uri_test.dart
@@ -575,6 +575,81 @@
uri.resolve("/qux").toString());
}
+void testBackslashes() {
+ // Tests change which makes `\` be treated as `/` in
+ // autority and path.
+
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:\\example.com\").toString());
+
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:\/example.com/").toString());
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https:/\example.com/").toString());
+ Expect.stringEquals("https://example.com/",
+ Uri.parse(r"https://example.com/").toString());
+ Expect.stringEquals("https://example.com/foo//bar",
+ Uri.parse(r"https://example.com/foo\\bar").toString());
+
+ Expect.stringEquals("https:/example.com/foo?%5C#%5C",
+ Uri.parse(r"https:\example.com/foo?\#\").toString());
+
+ Expect.stringEquals("https://example.com/@example.net/foo",
+ Uri.parse(r"https://example.com\@example.net/foo").toString());
+
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\foo").toString());
+ Expect.stringEquals("file://foo/",
+ Uri.parse(r"file:\\foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\\\foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:\//foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file:/\/foo").toString());
+ Expect.stringEquals("file:///foo",
+ Uri.parse(r"file://\foo").toString());
+
+ // No scheme.
+ Expect.stringEquals("//example.com/foo",
+ Uri.parse(r"\\example.com\foo").toString());
+
+ // No authority.
+ Expect.stringEquals("http:/foo",
+ Uri.parse(r"http:\foo").toString());
+
+ /// No scheme or authority.
+
+ Expect.stringEquals("foo/bar/baz",
+ Uri.parse(r"foo\bar\baz").toString());
+
+ Expect.stringEquals("foo/bar/baz",
+ Uri.parse(r"foo\bar\.\baz").toString());
+
+ Expect.stringEquals("foo/baz",
+ Uri.parse(r"foo\bar\..\baz").toString());
+
+ // Not converted to / in query or fragment, still escaped.
+ Expect.stringEquals("https://example.com/foo?%5C#%5C",
+ Uri.parse(r"https://example.com/foo?\#\").toString());
+
+ // Applies when a path is provided, but not when using path segments.
+ Expect.stringEquals("https://example.com/foo/bar",
+ Uri(scheme: "https", host: "example.com", path: r"\foo\bar").toString());
+
+ Expect.stringEquals("https://example.com/foo%5Cbar",
+ Uri(scheme: "https", host: "example.com", pathSegments: [r"foo\bar"])
+ .toString());
+
+ // Does not apply to constructors which expect an unencoded path.
+ Expect.stringEquals("http://example.com/%5Cfoo%5Cbar",
+ Uri.http("example.com", r"\foo\bar").toString());
+ Expect.stringEquals("https://example.com/%5Cfoo%5Cbar",
+ Uri.https("example.com", r"\foo\bar").toString());
+}
+
main() {
testUri("http:", true);
testUri("file:///", true);
@@ -727,6 +802,7 @@
testNormalization();
testReplace();
testPackageUris();
+ testBackslashes();
}
String dump(Uri uri) {