diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a472fa7aa3..65bc8026670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,15 @@ [#34233]: https://github.com/dart-lang/sdk/issues/34233 [`DEFAULT_BUFFER_SIZE`]: https://api.dart.dev/stable/2.17.6/dart-convert/JsonUtf8Encoder/DEFAULT_BUFFER_SIZE-constant.html +#### `dart:core` + +- The `Uri` class will parse a backslash in the path or the authority separator + of a URI as a forward slash. This affects the `Uri` constructor's `path` + parameter, and the `Uri.parse` method. + This change was made to not diverge as much from the browser `URL` behavior. + The Dart `Uri` class is still not an implementation of the same standard + as the browser's `URL` implementation. + #### `dart:developer` - **Breaking change** [#34233][]: The previously deprecated APIs diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart index 8954e01238a..81cdcab2b79 100644 --- a/sdk/lib/core/uri.dart +++ b/sdk/lib/core/uri.dart @@ -150,7 +150,8 @@ abstract class Uri { /// [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. @@ -238,7 +239,7 @@ abstract class Uri { /// /// 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] @@ -1000,6 +1001,14 @@ abstract class Uri { // 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)) || @@ -2308,7 +2317,7 @@ class _Uri implements Uri { 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 "/"; @@ -2325,7 +2334,10 @@ class _Uri implements Uri { /// "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); @@ -2454,9 +2466,10 @@ class _Uri implements Uri { /// this methods returns the substring if [component] from [start] to [end]. static String _normalizeOrSubstring( String component, int start, int end, List 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); } @@ -2471,7 +2484,7 @@ class _Uri implements Uri { /// Returns `null` if the original content was already normalized. static String? _normalize( String component, int start, int end, List charTable, - {bool escapeDelimiters = false}) { + {bool escapeDelimiters = false, bool replaceBackslash = false}) { StringBuffer? buffer; int sectionStart = start; int index = start; @@ -2497,6 +2510,9 @@ class _Uri implements Uri { } 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. @@ -4200,6 +4216,7 @@ List _createTables() { 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); @@ -4207,7 +4224,7 @@ List _createTables() { 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); @@ -4216,6 +4233,7 @@ List _createTables() { setChars(b, "%", schemeOrPath | notSimple); setChars(b, ':', authOrPath | schemeEnd); setChars(b, "/", relPathSeg); + setChars(b, r"\", relPathSeg | notSimple); setChars(b, "?", query | queryStart); setChars(b, "#", fragment | fragmentStart); @@ -4223,12 +4241,14 @@ List _createTables() { 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); @@ -4236,6 +4256,7 @@ List _createTables() { 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); @@ -4247,6 +4268,7 @@ List _createTables() { 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); @@ -4256,6 +4278,7 @@ List _createTables() { 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); @@ -4263,6 +4286,7 @@ List _createTables() { 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); @@ -4270,6 +4294,7 @@ List _createTables() { 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); @@ -4279,46 +4304,48 @@ List _createTables() { 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 dcddc9e1f08..a1310c757fb 100644 --- a/tests/corelib/uri_test.dart +++ b/tests/corelib/uri_test.dart @@ -573,6 +573,81 @@ void testPackageUris() { 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 @@ main() { 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 3ac87eae6b3..c78b4834ba1 100644 --- a/tests/corelib_2/uri_test.dart +++ b/tests/corelib_2/uri_test.dart @@ -575,6 +575,81 @@ void testPackageUris() { 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 @@ main() { testNormalization(); testReplace(); testPackageUris(); + testBackslashes(); } String dump(Uri uri) {