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>
This commit is contained in:
Lasse R.H. Nielsen 2022-09-13 11:48:19 +00:00 committed by Commit Bot
parent b5972073a9
commit e4ae0cf2ce
4 changed files with 201 additions and 13 deletions

View file

@ -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

View file

@ -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<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);
}
@ -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<int> 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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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<Uint8List> _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);

View file

@ -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) {

View file

@ -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) {