Files
sdk/tests/corelib/uri_test.dart
Christophe Coevoet b6e5ae4b45 Add tests covering more cases for Uri.resolve
Closes https://github.com/dart-lang/sdk/pull/62073

GitOrigin-RevId: 3a50ef21cf15e7b0e0a2ef87c389da619bfecdc8
Change-Id: I75399055709a3b8260e67214b2017b76795e741e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/464040
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Lasse Nielsen <lrn@google.com>
2025-11-25 09:10:13 -08:00

1082 lines
34 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library uriTest;
import "package:expect/expect.dart";
import 'dart:convert';
void testUri(String uriText, bool isAbsolute) {
var uri = Uri.parse(uriText);
Expect.equals(isAbsolute, uri.isAbsolute);
Expect.stringEquals(uriText, uri.toString());
// Test equals and hashCode members.
var uri2 = Uri.parse(uriText);
Expect.equals(uri, uri2);
Expect.equals(uri.hashCode, uri2.hashCode);
// Test that removeFragment doesn't change anything else.
if (uri.hasFragment) {
Expect.equals(
Uri.parse(uriText.substring(0, uriText.indexOf('#'))),
uri.removeFragment(),
);
} else {
Expect.equals(uri, Uri.parse(uriText + "#fragment").removeFragment());
}
}
void testEncodeDecode(String orig, String encoded) {
var e = Uri.encodeFull(orig);
Expect.stringEquals(encoded, e);
var d = Uri.decodeFull(encoded);
Expect.stringEquals(orig, d);
}
void testEncodeDecodeComponent(String orig, String encoded) {
var e = Uri.encodeComponent(orig);
Expect.stringEquals(encoded, e);
var d = Uri.decodeComponent(encoded);
Expect.stringEquals(orig, d);
}
void testEncodeDecodeQueryComponent(
String orig,
String encodedUTF8,
String encodedLatin1,
String? encodedAscii,
) {
var e, d;
e = Uri.encodeQueryComponent(orig);
Expect.stringEquals(encodedUTF8, e);
d = Uri.decodeQueryComponent(encodedUTF8);
Expect.stringEquals(orig, d);
e = Uri.encodeQueryComponent(orig, encoding: utf8);
Expect.stringEquals(encodedUTF8, e);
d = Uri.decodeQueryComponent(encodedUTF8, encoding: utf8);
Expect.stringEquals(orig, d);
e = Uri.encodeQueryComponent(orig, encoding: latin1);
Expect.stringEquals(encodedLatin1, e);
d = Uri.decodeQueryComponent(encodedLatin1, encoding: latin1);
Expect.stringEquals(orig, d);
if (encodedAscii != null) {
e = Uri.encodeQueryComponent(orig, encoding: ascii);
Expect.stringEquals(encodedAscii, e);
d = Uri.decodeQueryComponent(encodedAscii, encoding: ascii);
Expect.stringEquals(orig, d);
} else {
Expect.throwsArgumentError(
() => Uri.encodeQueryComponent(orig, encoding: ascii),
);
}
}
void testUriPerRFCs() {
final urisSample = "http://a/b/c/d;p?q";
Uri base = Uri.parse(urisSample);
testResolve(expect, relative) {
Expect.stringEquals(expect, base.resolve(relative).toString());
}
// From RFC 3986.
testResolve("g:h", "g:h");
testResolve("http://a/b/c/g", "g");
testResolve("http://a/b/c/g", "./g");
testResolve("http://a/b/c/g/", "g/");
testResolve("http://a/g", "/g");
testResolve("http://g", "//g");
testResolve("http://a/b/c/d;p?y", "?y");
testResolve("http://a/b/c/g?y", "g?y");
testResolve("http://a/b/c/d;p?q#s", "#s");
testResolve("http://a/b/c/g#s", "g#s");
testResolve("http://a/b/c/g?y#s", "g?y#s");
testResolve("http://a/b/c/;x", ";x");
testResolve("http://a/b/c/g;x", "g;x");
testResolve("http://a/b/c/g;x?y#s", "g;x?y#s");
testResolve("http://a/b/c/d;p?q", "");
testResolve("http://a/b/c/", ".");
testResolve("http://a/b/c/", "./");
testResolve("http://a/b/", "..");
testResolve("http://a/b/", "../");
testResolve("http://a/b/g", "../g");
testResolve("http://a/", "../..");
testResolve("http://a/", "../../");
testResolve("http://a/g", "../../g");
testResolve("http://a/g", "../../../g");
testResolve("http://a/g", "../../../../g");
testResolve("http://a/g", "/./g");
testResolve("http://a/g", "/../g");
testResolve("http://a/b/c/g.", "g.");
testResolve("http://a/b/c/.g", ".g");
testResolve("http://a/b/c/g..", "g..");
testResolve("http://a/b/c/..g", "..g");
testResolve("http://a/b/g", "./../g");
testResolve("http://a/b/c/g/", "./g/.");
testResolve("http://a/b/c/g/h", "g/./h");
testResolve("http://a/b/c/h", "g/../h");
testResolve("http://a/b/c/g;x=1/y", "g;x=1/./y");
testResolve("http://a/b/c/y", "g;x=1/../y");
testResolve("http://a/b/c/g?y/./x", "g?y/./x");
testResolve("http://a/b/c/g?y/../x", "g?y/../x");
testResolve("http://a/b/c/g#s/./x", "g#s/./x");
testResolve("http://a/b/c/g#s/../x", "g#s/../x");
testResolve("http:g", "http:g");
// Additional tests (not from RFC 3986).
testResolve("http://a/b/g;p/h;s", "../g;p/h;s");
base = Uri.parse("http://a?q");
testResolve("http://a?p", "?p");
testResolve("http://a/b/c", "b/c");
base = Uri.parse("foo:/a/b?q");
testResolve("foo:/a/b?p", "?p");
testResolve("foo:/c", "../c");
testResolve("foo:/a/c", "c");
testResolve("foo:/c", "../../c");
testResolve("foo:/c", "/c");
// Test non-URI base (no scheme, no authority, relative path).
base = Uri.parse("a/b/c?_#_");
testResolve("a/b/g?q#f", "g?q#f");
testResolve("../", "../../..");
testResolve("./", "../..");
testResolve("a/b/", ".");
testResolve("c", "../../c");
testResolve("//foo/a", "//foo/a");
testResolve("a/b/c?_#f", "#f");
testResolve("a/b/c?q", "?q");
testResolve("a/b/c?q#c", "?q#c");
testResolve("a/b/c?_", "");
testResolve("/a", "/a");
testResolve("/b", "/a/../../b");
base = Uri.parse("/a/b?_#_");
testResolve("/a/c", "c");
testResolve("/c", "../../c");
base = Uri.parse("//foo/a");
testResolve("//foo/b", "b");
testResolve("//foo/", "..");
testResolve("//foo/c", "../c");
testResolve("s:/a/b", "s:/a/b");
testResolve("//bar/b", "//bar/b");
base = Uri.parse("?_#_");
testResolve("?q", "?q");
testResolve("?_#f", "#f");
testResolve("a/b", "a/b");
base = Uri.parse("s:a/b");
testResolve("s:/c", "../c");
}
void testResolvePath(String expected, String path) {
Expect.equals(
expected,
new Uri(path: '/').resolveUri(new Uri(path: path)).path,
);
Expect.equals(
"http://localhost$expected",
Uri.parse("http://localhost").resolveUri(new Uri(path: path)).toString(),
);
}
const ALPHA = r"abcdefghijklmnopqrstuvwxuzABCDEFGHIJKLMNOPQRSTUVWXUZ";
const DIGIT = r"0123456789";
const PERCENT_ENCODED = "%00%ff";
const SUBDELIM = r"!$&'()*+,;=";
const SCHEMECHAR = "$ALPHA$DIGIT+-.";
const UNRESERVED = "$ALPHA$DIGIT-._~";
const REGNAMECHAR = "$UNRESERVED$SUBDELIM$PERCENT_ENCODED";
const USERINFOCHAR = "$REGNAMECHAR:";
const PCHAR_NC = "$UNRESERVED$SUBDELIM$PERCENT_ENCODED@";
const PCHAR = "$PCHAR_NC:";
const QUERYCHAR = "$PCHAR/?";
void testValidCharacters() {
// test that all valid characters are accepted.
for (var scheme in ["", "$SCHEMECHAR$SCHEMECHAR:"]) {
for (var userinfo in [
"",
"@",
"$USERINFOCHAR$USERINFOCHAR@",
"$USERINFOCHAR:$DIGIT@",
]) {
for (var host in [
"", "$REGNAMECHAR$REGNAMECHAR",
"255.255.255.256", // valid reg-name.
"[ffff::ffff:ffff]", "[ffff::255.255.255.255]",
]) {
for (var port in ["", ":", ":$DIGIT$DIGIT"]) {
var auth = "$userinfo$host$port";
if (auth.isNotEmpty) auth = "//$auth";
var paths = ["", "/", "/$PCHAR", "/$PCHAR/"]; // Absolute or empty.
if (auth.isNotEmpty) {
// Initial segment may be empty.
paths..add("//$PCHAR");
} else {
// Path may begin with non-slash.
if (scheme.isEmpty) {
// Initial segment must not contain colon.
paths
..add(PCHAR_NC)
..add("$PCHAR_NC/$PCHAR")
..add("$PCHAR_NC/$PCHAR/");
} else {
paths
..add(PCHAR)
..add("$PCHAR/$PCHAR")
..add("$PCHAR/$PCHAR/");
}
}
for (var path in paths) {
for (var query in ["", "?", "?$QUERYCHAR"]) {
for (var fragment in ["", "#", "#$QUERYCHAR"]) {
var uri = "$scheme$auth$path$query$fragment";
// Should not throw.
var result = Uri.parse(uri);
}
}
}
}
}
}
}
}
void testInvalidUrls() {
void checkInvalid(uri) {
try {
var result = Uri.parse(uri);
Expect.fail("Invalid URI `$uri` parsed to $result\n" + dump(result));
} on FormatException {
// Success.
}
}
checkInvalid("s%41://x.x/"); // No escapes in scheme,
// and no colon before slash in path.
checkInvalid("1a://x.x/"); // Scheme must start with letter,
// and no colon before slash in path.
checkInvalid(".a://x.x/"); // Scheme must start with letter,
// and no colon before slash in path.
checkInvalid("_:"); // Character not valid in scheme,
// and no colon before slash in path.
checkInvalid(":"); // Scheme must start with letter,
// and no colon before slash in path.
void checkInvalidReplaced(uri, invalid, replacement) {
var source = uri.replaceAll('{}', invalid);
var expected = uri.replaceAll('{}', replacement);
var result = Uri.parse(source);
Expect.equals(expected, "$result", "Source: $source\n${dump(result)}");
}
// Regression test for http://dartbug.com/16081
checkInvalidReplaced(
"http://www.example.org/red%09ros{}#red)",
"\u00e9",
"%C3%A9",
);
checkInvalidReplaced("http://r{}sum\{}.example.org", "\u00E9", "%C3%A9");
// Invalid characters. The characters must be rejected, even if normalizing
// the input would cause them to be valid (normalization happens after
// validation).
var invalidCharsAndReplacements = [
"\xe7", "%C3%A7", // Arbitrary non-ASCII letter
" ", "%20", // Space, not allowed anywhere.
'"', "%22", // Quote, not allowed anywhere
"<>", "%3C%3E", // Less/greater-than, not allowed anywhere.
"\x7f", "%7F", // DEL, not allowed anywhere
"\xdf", "%C3%9F", // German lower-case scharf-S.
// Becomes ASCII when upper-cased.
"\u0130", "%C4%B0", // Latin capital dotted I,
// becomes ASCII lower-case in Turkish.
"%\uFB03", "%25%EF%AC%83", // % + Ligature ffi,
// becomes ASCII when upper-cased,
// should not be read as "%FFI".
"\u212a", "%E2%84%AA", // Kelvin sign. Becomes ASCII when lower-cased.
"%1g", "%251g", // Invalid escape.
"\u{10000}", "%F0%90%80%80", // Non-BMP character as surrogate pair.
];
for (int i = 0; i < invalidCharsAndReplacements.length; i += 2) {
var invalid = invalidCharsAndReplacements[i];
var valid = invalidCharsAndReplacements[i + 1];
checkInvalid("A{}b:///".replaceAll('{}', invalid));
checkInvalid("{}b:///".replaceAll('{}', invalid));
checkInvalidReplaced("s://user{}info@x.x/", invalid, valid);
checkInvalidReplaced("s://reg{}name/", invalid, valid);
checkInvalid("s://regname:12{}45/".replaceAll("{}", invalid));
checkInvalidReplaced("s://regname/p{}ath/", invalid, valid);
checkInvalidReplaced("/p{}ath/", invalid, valid);
checkInvalidReplaced("p{}ath/", invalid, valid);
checkInvalidReplaced("s://regname/path/?x{}x", invalid, valid);
checkInvalidReplaced("s://regname/path/#x{}x", invalid, valid);
checkInvalidReplaced("s://regname/path/??#x{}x", invalid, valid);
}
// At most one @ in userinfo.
checkInvalid("s://x@x@x.x/");
// No colon in host except before a port.
checkInvalid("s://x@x:x/");
// At most one port.
checkInvalid("s://x@x:9:9/");
// @ not allowed in scheme.
checkInvalid("s@://x:9/x?x#x");
// ] not allowed alone in host.
checkInvalid("s://xx]/");
// IPv6 must be enclosed in [ and ] for Uri.parse.
// It is allowed un-enclosed as argument to `Uri(host:...)` because we don't
// need to delimit.
checkInvalid("s://ffff::ffff:1234/");
}
void testNormalization() {
// The Uri constructor and the Uri.parse function performs RFC-3986
// syntax based normalization.
var uri;
// Scheme: Only case normalization. Schemes cannot contain escapes.
uri = Uri.parse("A:");
Expect.equals("a", uri.scheme);
uri = Uri.parse("Z:");
Expect.equals("z", uri.scheme);
uri = Uri.parse("$SCHEMECHAR:");
Expect.equals(SCHEMECHAR.toLowerCase(), uri.scheme);
// Percent escape normalization.
// Escapes of unreserved characters are converted to the character,
// subject to case normalization in reg-name.
for (var i = 0; i < UNRESERVED.length; i++) {
var char = UNRESERVED[i];
var escape = "%" + char.codeUnitAt(0).toRadixString(16); // all > 0xf.
uri = Uri.parse(
"s://xX${escape}xX@yY${escape}yY/zZ${escape}zZ"
"?vV${escape}vV#wW${escape}wW",
);
Expect.equals("xX${char}xX", uri.userInfo);
Expect.equals("yY${char}yY".toLowerCase(), uri.host);
Expect.equals("/zZ${char}zZ", uri.path);
Expect.equals("vV${char}vV", uri.query);
Expect.equals("wW${char}wW", uri.fragment);
}
// Escapes of reserved characters are kept, but upper-cased.
for (var escape in ["%00", "%1f", "%7F", "%fF"]) {
uri = Uri.parse(
"s://xX${escape}xX@yY${escape}yY/zZ${escape}zZ"
"?vV${escape}vV#wW${escape}wW",
);
var normalizedEscape = escape.toUpperCase();
Expect.equals("xX${normalizedEscape}xX", uri.userInfo);
Expect.equals("yy${normalizedEscape}yy", uri.host);
Expect.equals("/zZ${normalizedEscape}zZ", uri.path);
Expect.equals("vV${normalizedEscape}vV", uri.query);
Expect.equals("wW${normalizedEscape}wW", uri.fragment);
}
// Some host normalization edge cases.
uri = Uri.parse("x://x%61X%41x%41X%61x/");
Expect.equals("xaxaxaxax", uri.host);
uri = Uri.parse("x://Xxxxxxxx/");
Expect.equals("xxxxxxxx", uri.host);
uri = Uri.parse("x://xxxxxxxX/");
Expect.equals("xxxxxxxx", uri.host);
uri = Uri.parse("x://xxxxxxxx%61/");
Expect.equals("xxxxxxxxa", uri.host);
uri = Uri.parse("x://%61xxxxxxxx/");
Expect.equals("axxxxxxxx", uri.host);
uri = Uri.parse("x://X/");
Expect.equals("x", uri.host);
uri = Uri.parse("x://%61/");
Expect.equals("a", uri.host);
uri = new Uri(scheme: "x", path: "//y");
Expect.equals("//y", uri.path);
Expect.equals("x:////y", uri.toString());
uri = new Uri(scheme: "file", path: "//y");
Expect.equals("//y", uri.path);
Expect.equals("file:////y", uri.toString());
// File scheme noralizes to always showing authority, even if empty.
uri = new Uri(scheme: "file", path: "/y");
Expect.equals("file:///y", uri.toString());
uri = new Uri(scheme: "file", path: "y");
Expect.equals("file:///y", uri.toString());
// Empty host/query/fragment ensures the delimiter is there.
// Different from not being there.
Expect.equals("scheme:/", Uri.parse("scheme:/").toString());
Expect.equals("scheme:/", new Uri(scheme: "scheme", path: "/").toString());
Expect.equals("scheme:///?#", Uri.parse("scheme:///?#").toString());
Expect.equals(
"scheme:///#",
new Uri(
scheme: "scheme",
host: "",
path: "/",
query: "",
fragment: "",
).toString(),
);
// We allow, and escape, general delimiters in paths, queries and fragments.
// Allow `[` and `]` in path:
Expect.equals("s:/%5B%5D", Uri.parse("s:/[]").toString());
Expect.equals("s:%5B%5D", Uri.parse("s:[]").toString());
Expect.equals("%5B%5D", Uri.parse("[]").toString());
// Allow `[`, `]` and `?` in query (anything after *first* `?`).
// The `?` is not escaped.
Expect.equals("s://xx/?%5B%5D?", Uri.parse("s://xx/?[]?").toString());
// Allow `[`, `]`, `?` and `#` in fragment (anything after *first* `#`).
// The `?` is not escaped.
Expect.equals("s://xx/#%5B%5D%23?", Uri.parse("s://xx/#[]#?").toString());
}
void testReplace() {
var uris = [
Uri.parse(""),
Uri.parse("a://@:/?#"),
Uri.parse("a://b@c:4/e/f?g#h"),
Uri.parse(
"$SCHEMECHAR://$USERINFOCHAR@$REGNAMECHAR:$DIGIT/$PCHAR/$PCHAR"
"?$QUERYCHAR#$QUERYCHAR",
),
];
for (var uri1 in uris) {
for (var uri2 in uris) {
if (identical(uri1, uri2)) continue;
var scheme = uri1.scheme;
var userInfo = uri1.hasAuthority ? uri1.userInfo : "";
var host = uri1.hasAuthority ? uri1.host : null;
var port = uri1.hasAuthority ? uri1.port : 0;
var path = uri1.path;
var query = uri1.hasQuery ? uri1.query : null;
var fragment = uri1.hasFragment ? uri1.fragment : null;
var tmp1 = uri1;
void test() {
var tmp2 = new Uri(
scheme: scheme,
userInfo: userInfo,
host: host,
port: port,
path: path,
query: query == "" ? null : query,
queryParameters: query == "" ? {} : null,
fragment: fragment,
);
Expect.equals(tmp1, tmp2);
}
test();
scheme = uri2.scheme;
tmp1 = tmp1.replace(scheme: scheme);
test();
if (uri2.hasAuthority) {
userInfo = uri2.userInfo;
host = uri2.host;
port = uri2.port;
tmp1 = tmp1.replace(userInfo: userInfo, host: host, port: port);
test();
}
path = uri2.path;
tmp1 = tmp1.replace(path: path);
test();
if (uri2.hasQuery) {
query = uri2.query;
tmp1 = tmp1.replace(query: query);
test();
}
if (uri2.hasFragment) {
fragment = uri2.fragment;
tmp1 = tmp1.replace(fragment: fragment);
test();
}
}
}
// Regression test, http://dartbug.com/20814
var uri = Uri.parse("/no-authority/");
uri = uri.replace(fragment: "fragment");
Expect.isFalse(uri.hasAuthority);
uri = new Uri(scheme: "foo", path: "bar");
uri = uri.replace(
queryParameters: {
"x": ["42", "37"],
"y": ["43", "38"],
},
);
var params = uri.queryParametersAll;
Expect.equals(2, params.length);
Expect.listEquals(["42", "37"], params["x"]!);
Expect.listEquals(["43", "38"], params["y"]!);
}
void testPackageUris() {
// A URI is recognized as a package URI if it has:
// * "package" as scheme
// * no authority
// * a first path segment
// * containing no `%`,
// * which is not all "." characters,
// * and which ends with a `/`.
//
// If so, the package name is unaffected by path resolution.
var uri = Uri.parse("package:foo/bar/baz"); // Simple base URI.
Expect.stringEquals(
"package:foo/qux", // Resolve simple URI.
uri.resolve("../../qux").toString(),
);
Expect.stringEquals("package:foo/qux", uri.resolve("/qux").toString());
Expect.stringEquals(
"package:foo/qux?%2F", // Resolve non-simple URI.
uri.resolve("../../qux?%2F").toString(),
);
Expect.stringEquals(
"package:foo/qux?%2F",
uri.resolve("/qux?%2F").toString(),
);
uri = Uri.parse("package:foo/%62ar/baz"); // Non-simple base URI.
Expect.stringEquals(
"package:foo/qux", // Resolve simple URI.
uri.resolve("../../qux").toString(),
);
Expect.stringEquals("package:foo/qux", uri.resolve("/qux").toString());
Expect.stringEquals(
"package:foo/qux?%2F", // Resolve non-simple URI.
uri.resolve("../../qux?%2F").toString(),
);
Expect.stringEquals(
"package:foo/qux?%2F",
uri.resolve("/qux?%2F").toString(),
);
// The following base URIs are not recognized as package URIs:
uri = Uri.parse("puckage:foo/bar/baz"); // Not "package" scheme.
Expect.stringEquals("puckage:/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("puckage:/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package://foo/bar/baz"); // Has authority.
Expect.stringEquals("package://foo/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package://foo/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package:/foo/bar/baz"); // Has empty package name.
Expect.stringEquals("package:/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package:/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package:f%2fo/bar/baz"); // Has escape in package name.
Expect.stringEquals("package:/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package:/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package:f:o/bar/baz"); // Has colon in package name.
Expect.stringEquals("package:/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package:/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package:.../bar/baz"); // Has only '.' in package name.
Expect.stringEquals("package:/qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package:/qux", uri.resolve("/qux").toString());
uri = Uri.parse("package:foo?/"); // Has no `/` after package name.
// Resolving relative against non-absolute path gives
// a non-absolute path again.
// TODO(lrn): Is this a bug?
Expect.stringEquals("package:qux", uri.resolve("../../qux").toString());
Expect.stringEquals("package:/qux", uri.resolve("/qux").toString());
}
void testBackslashes() {
// Tests change which makes `\` be treated as `/` in
// authority 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(),
);
}
void main() {
testUri("http:", true);
testUri("file:///", true);
testUri("file", false);
testUri("http://user@example.com:8080/fisk?query=89&hest=silas", true);
testUri(
"http://user@example.com:8080/fisk?query=89&hest=silas#fragment",
false,
);
Expect.stringEquals(
"http://user@example.com/a/b/c?query#fragment",
new Uri(
scheme: "http",
userInfo: "user",
host: "example.com",
port: 80,
path: "/a/b/c",
query: "query",
fragment: "fragment",
).toString(),
);
Expect.stringEquals(
"/a/b/c/",
new Uri(
scheme: null,
userInfo: null,
host: null,
port: 0,
path: "/a/b/c/",
query: null,
fragment: null,
).toString(),
);
Expect.stringEquals("file:///", Uri.parse("file:").toString());
testResolvePath("/a/g", "/a/b/c/./../../g");
testResolvePath("/a/g", "/a/b/c/./../../g");
testResolvePath("/mid/6", "mid/content=5/../6");
testResolvePath("/a/b/e", "a/b/c/d/../../e");
testResolvePath("/a/b/e", "../a/b/c/d/../../e");
testResolvePath("/a/b/e", "./a/b/c/d/../../e");
testResolvePath("/a/b/e", "../a/b/./c/d/../../e");
testResolvePath("/a/b/e", "./a/b/./c/d/../../e");
testResolvePath("/a/b/e/", "./a/b/./c/d/../../e/.");
testResolvePath("/a/b/e/", "./a/b/./c/d/../../e/./.");
testResolvePath("/a/b/e/", "./a/b/./c/d/../../e/././.");
testUriPerRFCs();
Expect.stringEquals(
"http://example.com",
Uri.parse("http://example.com/a/b/c").origin,
);
Expect.stringEquals(
"https://example.com",
Uri.parse("https://example.com/a/b/c").origin,
);
Expect.stringEquals(
"http://example.com:1234",
Uri.parse("http://example.com:1234/a/b/c").origin,
);
Expect.stringEquals(
"https://example.com:1234",
Uri.parse("https://example.com:1234/a/b/c").origin,
);
Expect.throwsStateError(
() => Uri.parse("http:").origin,
"origin for uri with empty host should fail",
);
Expect.throwsStateError(
() => new Uri(
scheme: "http",
userInfo: null,
host: "",
port: 80,
path: "/a/b/c",
query: "query",
fragment: "fragment",
).origin,
"origin for uri with empty host should fail",
);
Expect.throwsStateError(
() => new Uri(
scheme: null,
userInfo: null,
host: "",
port: 80,
path: "/a/b/c",
query: "query",
fragment: "fragment",
).origin,
"origin for uri with empty scheme should fail",
);
Expect.throwsStateError(
() => new Uri(
scheme: "http",
userInfo: null,
host: null,
port: 80,
path: "/a/b/c",
query: "query",
fragment: "fragment",
).origin,
"origin for uri with empty host should fail",
);
Expect.throwsStateError(
() => Uri.parse("http://:80").origin,
"origin for uri with empty host should fail",
);
Expect.throwsStateError(
() => Uri.parse("file://localhost/test.txt").origin,
"origin for non-http/https uri should fail",
);
// URI encode tests
// Create a string with code point 0x10000 encoded as a surrogate pair.
var s = utf8.decode([0xf0, 0x90, 0x80, 0x80]);
Expect.stringEquals("\u{10000}", s);
testEncodeDecode("A + B", "A%20+%20B");
testEncodeDecode("\uFFFE", "%EF%BF%BE");
testEncodeDecode("\uFFFF", "%EF%BF%BF");
testEncodeDecode("\uFFFE", "%EF%BF%BE");
testEncodeDecode("\uFFFF", "%EF%BF%BF");
testEncodeDecode("\x7f", "%7F");
testEncodeDecode("\x80", "%C2%80");
testEncodeDecode("\u0800", "%E0%A0%80");
// All characters not escaped by encodeFull.
var unescapedFull =
r"abcdefghijklmnopqrstuvwxyz"
r"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
r"0123456789!#$&'()*+,-./:;=?@_~";
// ASCII characters escaped by encodeFull:
var escapedFull =
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
r' "%<>[\]^`{|}'
"\x7f";
var escapedTo =
"%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F"
"%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F"
"%20%22%25%3C%3E%5B%5C%5D%5E%60%7B%7C%7D%7F";
testEncodeDecode(unescapedFull, unescapedFull);
testEncodeDecode(escapedFull, escapedTo);
var nonAscii =
"\x80-\xff-\u{100}-\u{7ff}-\u{800}-\u{ffff}-\u{10000}-\u{10ffff}";
var nonAsciiEncoding =
"%C2%80-%C3%BF-%C4%80-%DF%BF-%E0%A0%80-%EF%BF%BF-"
"%F0%90%80%80-%F4%8F%BF%BF";
testEncodeDecode(nonAscii, nonAsciiEncoding);
testEncodeDecode(s, "%F0%90%80%80");
testEncodeDecodeComponent("A + B", "A%20%2B%20B");
testEncodeDecodeComponent("\uFFFE", "%EF%BF%BE");
testEncodeDecodeComponent("\uFFFF", "%EF%BF%BF");
testEncodeDecodeComponent("\uFFFE", "%EF%BF%BE");
testEncodeDecodeComponent("\uFFFF", "%EF%BF%BF");
testEncodeDecodeComponent("\x7f", "%7F");
testEncodeDecodeComponent("\x80", "%C2%80");
testEncodeDecodeComponent("\u0800", "%E0%A0%80");
testEncodeDecodeComponent(":/@',;?&=+\$", "%3A%2F%40'%2C%3B%3F%26%3D%2B%24");
testEncodeDecodeComponent(s, "%F0%90%80%80");
testEncodeDecodeQueryComponent("A + B", "A+%2B+B", "A+%2B+B", "A+%2B+B");
testEncodeDecodeQueryComponent(
"æ ø å",
"%C3%A6+%C3%B8+%C3%A5",
"%E6+%F8+%E5",
null,
);
testEncodeDecodeComponent(nonAscii, nonAsciiEncoding);
// Invalid URI - : and @ is swapped, port ("host") should be numeric.
Expect.throwsFormatException(
() => Uri.parse("file://user@password:host/path"),
);
testValidCharacters();
testInvalidUrls();
testNormalization();
testReplace();
testPackageUris();
testBackslashes();
testNonBmpEncodingRegression();
testDotSlashSlashRegression();
}
void testDotSlashSlashRegression() {
// Regression test.
//
// A relative path must not normalize to one starting with an empty segment,
// because that makes the result look like an absolute path.
// To avoid that, prepend `./` before the leading empty segment.
//
// The `_normalizeRelativePath` misbehaved on inputs like `.//`, removing
// the initial `.` segment and returning `/`.
// A replacement based implementation of path normalization, for comparison.
//
// (^|/).(/|$) -> (^|/)
// (^|/)<segment>/..(/|$) -> (^|/)
//
// That is: Remove a `/./` or a `/segment/../` by turning it into a `/`,
// unless it's at the start, then don't insert a leading `/`.
// Add leading `./` if starting with `/` (starting with empty segment)
// or has become empty.
// Add final `/` if ending in `..` segment (and then also starting with
// `..` segment).
var normalizeRE = RegExp(r'(/|^)(?:\.|(?!\.\.(?:/|$))[^/]*/\.\.)(?:/|$)');
String normalizeByReplacement(String path) {
while (true) {
var newPath = path.replaceAllMapped(normalizeRE, (m) => m[1]!);
if (newPath == path) break;
path = newPath;
}
// If ends with `..` segment, add `/` after it.
if (path == '..' || path.endsWith('/..')) {
path += '/';
}
// Don't return an absolute or empty path.
if (path.isEmpty || path.startsWith('/')) path = "./$path";
return path;
}
// Check that `Uri.parse` gives the expected result.
// Check that the expected result is what normalization by replacement
// would give.
void checkNormalize(String expectedPath, String input) {
Expect.stringEquals(
expectedPath,
Uri.parse(input).toString(),
"Uri.parse('$input')",
);
Expect.stringEquals(
expectedPath,
normalizeByReplacement(input),
"NormByReplace('$input')",
);
}
var twoSlashRE = RegExp(r'(?<=/)(?=/)');
void testNormalize(String expectedPath, String input) {
checkNormalize(expectedPath, input);
// Change all empty segments, `//`, to `/x/`, in expectation and result.
var nonEmptyExpect = expectedPath.replaceAll(twoSlashRE, 'x');
if (nonEmptyExpect.startsWith('./x/')) {
// Only differs if empty segment would have been first.
nonEmptyExpect = nonEmptyExpect.substring(2);
}
checkNormalize(nonEmptyExpect, input.replaceAll(twoSlashRE, 'x'));
}
const paths = [
// (input, expected outout)
('a/..', './'),
('a/../', './'),
('a/..//', './/'),
('a/..//.', './/'),
('a/..//./', './/'),
('.', './'),
('./', './'),
('.//', './/'),
('.//.', './/'),
('.//./', './/'),
('.//./.', './/'),
('.//././', './/'),
('.//./..', './'),
('.//./../', './'),
('.//.//.', './//'),
('.//.//./', './//'),
('.//', './/'),
('.//a', './/a'),
('.//..', './'),
('.//../', './'),
('.//../a', 'a'),
('.//../a/', 'a/'),
('.//.././', './'),
('.//.././/', './/'),
('.//../..', '../'),
('.//../../', '../'),
('.//../.././/', '..//'),
('.//../..//', '..//'),
('.//../..//..', '../'),
('.//../..//../', '../'),
('.//..//', './/'),
('.//..//..', './'),
('.//..//../', './'),
('.///', './//'),
('.///a', './//a'),
('.///.', './//'),
('.///./', './//'),
('.///../..', './'),
('.///../../', './'),
// Not broken, extra sanity checks in case the fix broke it.
('..', '../'),
('../', '../'),
('../a', '../a'),
('../a/', '../a/'),
('../.', '../'),
('.././', '../'),
('../..', '../../'),
('../../', '../../'),
('..//', '..//'),
('..//..', '../'),
('..//../', '../'),
('.././..', '../../'),
('.././/', '..//'),
];
for (var (input, expected) in paths) {
assert(!input.startsWith('/'));
testNormalize(expected, input);
testNormalize(expected, './$input'); // Prefixing with `./` changes nothing.
}
}
void testNonBmpEncodingRegression() {
// Regression test for bug in encoding of some non-BMP characters
// in host names. The failing character has to be one that doesn't have
// the 0x10000 bit set in its code point, and which is not in the BMP.
const char = "\u{2003E}"; // CJK Unified Ideo­graph.
const echar = "%F0%A0%80%BE"; // UTF-8 encoding of page2Char, %-encoded.
var nonBmpUri = Uri.parse(
"http://$char.example.com/x${char}x?y${char}y#z${char}z",
);
Expect.equals(
"http://$echar.example.com/x${echar}x?y${echar}y#z${echar}z",
nonBmpUri.toString(),
);
Expect.equals("$echar.example.com", nonBmpUri.host);
Expect.equals("$char.example.com", Uri.decodeComponent(nonBmpUri.host));
Expect.equals("/x${echar}x", nonBmpUri.path);
Expect.equals("/x${char}x", Uri.decodeComponent(nonBmpUri.path));
Expect.equals("y${echar}y", nonBmpUri.query);
Expect.equals("y${char}y", Uri.decodeComponent(nonBmpUri.query));
Expect.equals("z${echar}z", nonBmpUri.fragment);
Expect.equals("z${char}z", Uri.decodeComponent(nonBmpUri.fragment));
}
String dump(Uri uri) {
return "URI: $uri\n"
" Scheme: ${uri.scheme} #${uri.scheme.length}\n"
" User-info: ${uri.userInfo} #${uri.userInfo.length}\n"
" Host: ${uri.host} #${uri.host.length}\n"
" Port: ${uri.port}\n"
" Path: ${uri.path} #${uri.path.length}\n"
" Query: ${uri.query} #${uri.query.length}\n"
" Fragment: ${uri.fragment} #${uri.fragment.length}\n";
}