mirror of
https://github.com/dart-lang/sdk
synced 2024-10-02 23:39:19 +00:00
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:
parent
b5972073a9
commit
e4ae0cf2ce
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue