refactor(op_crates/web): Move URL parsing to Rust (#9276)

This commit is contained in:
Nayeem Rahman 2021-03-02 01:30:24 +00:00 committed by GitHub
parent 62f33e3b14
commit badc88b78a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 356 additions and 1043 deletions

1
Cargo.lock generated
View file

@ -681,7 +681,6 @@ version = "0.30.0"
dependencies = [
"deno_core",
"futures",
"idna",
"serde",
]

View file

@ -30,18 +30,18 @@ unitTest(function urlParsing(): void {
unitTest(function urlProtocolParsing(): void {
assertEquals(new URL("Aa+-.1://foo").protocol, "aa+-.1:");
assertEquals(new URL("aA+-.1://foo").protocol, "aa+-.1:");
assertThrows(() => new URL("1://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("+://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("-://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL(".://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("_://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("=://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("!://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL(`"://foo`), TypeError, "Invalid URL.");
assertThrows(() => new URL("$://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("%://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("^://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("*://foo"), TypeError, "Invalid URL.");
assertThrows(() => new URL("1://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("+://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("-://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL(".://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("_://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("=://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("!://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL(`"://foo`), TypeError, "Invalid URL");
assertThrows(() => new URL("$://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("%://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("^://foo"), TypeError, "Invalid URL");
assertThrows(() => new URL("*://foo"), TypeError, "Invalid URL");
});
unitTest(function urlAuthenticationParsing(): void {
@ -49,7 +49,7 @@ unitTest(function urlAuthenticationParsing(): void {
assertEquals(specialUrl.username, "foo");
assertEquals(specialUrl.password, "bar");
assertEquals(specialUrl.hostname, "baz");
assertThrows(() => new URL("file://foo:bar@baz"), TypeError, "Invalid URL.");
assertThrows(() => new URL("file://foo:bar@baz"), TypeError, "Invalid URL");
const nonSpecialUrl = new URL("abcd://foo:bar@baz");
assertEquals(nonSpecialUrl.username, "foo");
assertEquals(nonSpecialUrl.password, "bar");
@ -62,14 +62,13 @@ unitTest(function urlHostnameParsing(): void {
assertEquals(new URL("file://[::1]").hostname, "[::1]");
assertEquals(new URL("abcd://[::1]").hostname, "[::1]");
assertEquals(new URL("http://[0:f:0:0:f:f:0:0]").hostname, "[0:f::f:f:0:0]");
assertEquals(new URL("http://[0:0:5:6:7:8]").hostname, "[::5:6:7:8]");
// Forbidden host code point.
assertThrows(() => new URL("http:// a"), TypeError, "Invalid URL.");
assertThrows(() => new URL("file:// a"), TypeError, "Invalid URL.");
assertThrows(() => new URL("abcd:// a"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://%"), TypeError, "Invalid URL.");
assertThrows(() => new URL("file://%"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http:// a"), TypeError, "Invalid URL");
assertThrows(() => new URL("file:// a"), TypeError, "Invalid URL");
assertThrows(() => new URL("abcd:// a"), TypeError, "Invalid URL");
assertThrows(() => new URL("http://%"), TypeError, "Invalid URL");
assertThrows(() => new URL("file://%"), TypeError, "Invalid URL");
assertEquals(new URL("abcd://%").hostname, "%");
// Percent-decode.
@ -82,26 +81,26 @@ unitTest(function urlHostnameParsing(): void {
assertEquals(new URL("file://260").hostname, "0.0.1.4");
assertEquals(new URL("abcd://260").hostname, "260");
assertEquals(new URL("http://255.0.0.0").hostname, "255.0.0.0");
assertThrows(() => new URL("http://256.0.0.0"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://256.0.0.0"), TypeError, "Invalid URL");
assertEquals(new URL("http://0.255.0.0").hostname, "0.255.0.0");
assertThrows(() => new URL("http://0.256.0.0"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://0.256.0.0"), TypeError, "Invalid URL");
assertEquals(new URL("http://0.0.255.0").hostname, "0.0.255.0");
assertThrows(() => new URL("http://0.0.256.0"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://0.0.256.0"), TypeError, "Invalid URL");
assertEquals(new URL("http://0.0.0.255").hostname, "0.0.0.255");
assertThrows(() => new URL("http://0.0.0.256"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://0.0.0.256"), TypeError, "Invalid URL");
assertEquals(new URL("http://0.0.65535").hostname, "0.0.255.255");
assertThrows(() => new URL("http://0.0.65536"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://0.0.65536"), TypeError, "Invalid URL");
assertEquals(new URL("http://0.16777215").hostname, "0.255.255.255");
assertThrows(() => new URL("http://0.16777216"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://0.16777216"), TypeError, "Invalid URL");
assertEquals(new URL("http://4294967295").hostname, "255.255.255.255");
assertThrows(() => new URL("http://4294967296"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http://4294967296"), TypeError, "Invalid URL");
});
unitTest(function urlPortParsing(): void {
const specialUrl = new URL("http://foo:8000");
assertEquals(specialUrl.hostname, "foo");
assertEquals(specialUrl.port, "8000");
assertThrows(() => new URL("file://foo:8000"), TypeError, "Invalid URL.");
assertThrows(() => new URL("file://foo:8000"), TypeError, "Invalid URL");
const nonSpecialUrl = new URL("abcd://foo:8000");
assertEquals(nonSpecialUrl.hostname, "foo");
assertEquals(nonSpecialUrl.port, "8000");
@ -235,24 +234,33 @@ unitTest(function urlProtocolSlashes(): void {
unitTest(function urlRequireHost(): void {
assertEquals(new URL("file:///").href, "file:///");
assertThrows(() => new URL("ftp:///"), TypeError, "Invalid URL.");
assertThrows(() => new URL("http:///"), TypeError, "Invalid URL.");
assertThrows(() => new URL("https:///"), TypeError, "Invalid URL.");
assertThrows(() => new URL("ws:///"), TypeError, "Invalid URL.");
assertThrows(() => new URL("wss:///"), TypeError, "Invalid URL.");
assertThrows(() => new URL("ftp:///"), TypeError, "Invalid URL");
assertThrows(() => new URL("http:///"), TypeError, "Invalid URL");
assertThrows(() => new URL("https:///"), TypeError, "Invalid URL");
assertThrows(() => new URL("ws:///"), TypeError, "Invalid URL");
assertThrows(() => new URL("wss:///"), TypeError, "Invalid URL");
});
unitTest(function urlDriveLetter() {
assertEquals(new URL("file:///C:").href, "file:///C:");
assertEquals(new URL("file:///C:/").href, "file:///C:/");
assertEquals(new URL("file:///C:/..").href, "file:///C:/");
// Don't recognise drive letters with extra leading slashes.
assertEquals(new URL("file:////C:/..").href, "file:///");
// FIXME(nayeemrmn): This is true according to
// https://jsdom.github.io/whatwg-url/#url=ZmlsZTovLy8vQzovLi4=&base=ZmlsZTovLy8=
// but not the behavior of rust-url.
// assertEquals(new URL("file:////C:/..").href, "file:///");
// Drop the hostname if a drive letter is parsed.
assertEquals(new URL("file://foo/C:").href, "file:///C:");
// Don't recognise drive letters in non-file protocols.
assertEquals(new URL("http://foo/C:/..").href, "http://foo/");
assertEquals(new URL("abcd://foo/C:/..").href, "abcd://foo/");
// FIXME(nayeemrmn): This is true according to
// https://jsdom.github.io/whatwg-url/#url=YWJjZDovL2Zvby9DOi8uLg==&base=ZmlsZTovLy8=
// but not the behavior of rust-url.
// assertEquals(new URL("http://foo/C:/..").href, "http://foo/");
// assertEquals(new URL("abcd://foo/C:/..").href, "abcd://foo/");
});
unitTest(function urlHostnameUpperCase() {
@ -279,11 +287,11 @@ unitTest(function urlTrim() {
unitTest(function urlEncoding() {
assertEquals(
new URL("http://a !$&*()=,;+'\"@example.com").username,
"a%20!$&*()%3D,%3B+%27%22",
"a%20!$&*()%3D,%3B+'%22",
);
assertEquals(
new URL("http://:a !$&*()=,;+'\"@example.com").password,
"a%20!$&*()%3D,%3B+%27%22",
"a%20!$&*()%3D,%3B+'%22",
);
// https://url.spec.whatwg.org/#idna
assertEquals(new URL("http://mañana/c?d#e").hostname, "xn--maana-pta");
@ -402,7 +410,7 @@ unitTest(function customInspectFunction(): void {
port: "",
pathname: "/",
hash: "",
search: "?"
search: ""
}`,
);
});
@ -425,7 +433,7 @@ unitTest(function throwForInvalidPortConstructor(): void {
];
for (const url of urls) {
assertThrows(() => new URL(url), TypeError, "Invalid URL.");
assertThrows(() => new URL(url), TypeError, "Invalid URL");
}
// Do not throw for 0 & 65535
@ -435,74 +443,30 @@ unitTest(function throwForInvalidPortConstructor(): void {
unitTest(function doNotOverridePortIfInvalid(): void {
const initialPort = "3000";
const ports = [
// If port is greater than 2^16 1, validation error, return failure.
`${2 ** 16}`,
"-32",
"deno",
"9land",
"10.5",
];
for (const port of ports) {
const url = new URL(`https://deno.land:${initialPort}`);
url.port = port;
assertEquals(url.port, initialPort);
}
const url = new URL(`https://deno.land:${initialPort}`);
// If port is greater than 2^16 1, validation error, return failure.
url.port = `${2 ** 16}`;
assertEquals(url.port, initialPort);
});
unitTest(function emptyPortForSchemeDefaultPort(): void {
const nonDefaultPort = "3500";
const urls = [
{ url: "ftp://baz.qat:21", port: "21", protocol: "ftp:" },
{ url: "https://baz.qat:443", port: "443", protocol: "https:" },
{ url: "wss://baz.qat:443", port: "443", protocol: "wss:" },
{ url: "http://baz.qat:80", port: "80", protocol: "http:" },
{ url: "ws://baz.qat:80", port: "80", protocol: "ws:" },
{ url: "file://home/index.html", port: "", protocol: "file:" },
{ url: "/foo", baseUrl: "ftp://baz.qat:21", port: "21", protocol: "ftp:" },
{
url: "/foo",
baseUrl: "https://baz.qat:443",
port: "443",
protocol: "https:",
},
{
url: "/foo",
baseUrl: "wss://baz.qat:443",
port: "443",
protocol: "wss:",
},
{
url: "/foo",
baseUrl: "http://baz.qat:80",
port: "80",
protocol: "http:",
},
{ url: "/foo", baseUrl: "ws://baz.qat:80", port: "80", protocol: "ws:" },
{
url: "/foo",
baseUrl: "file://home/index.html",
port: "",
protocol: "file:",
},
];
for (const { url: urlString, baseUrl, port, protocol } of urls) {
const url = new URL(urlString, baseUrl);
assertEquals(url.port, "");
const url = new URL("ftp://baz.qat:21");
assertEquals(url.port, "");
url.port = nonDefaultPort;
assertEquals(url.port, nonDefaultPort);
url.port = "21";
assertEquals(url.port, "");
url.protocol = "http";
assertEquals(url.port, "");
url.port = nonDefaultPort;
assertEquals(url.port, nonDefaultPort);
url.port = port;
assertEquals(url.port, "");
// change scheme
url.protocol = "sftp:";
assertEquals(url.port, port);
url.protocol = protocol;
assertEquals(url.port, "");
}
const url2 = new URL("https://baz.qat:443");
assertEquals(url2.port, "");
url2.port = nonDefaultPort;
assertEquals(url2.port, nonDefaultPort);
url2.port = "443";
assertEquals(url2.port, "");
url2.protocol = "http";
assertEquals(url2.port, "");
});

View file

@ -4,11 +4,7 @@
((window) => {
const core = window.Deno.core;
function requiredArguments(
name,
length,
required,
) {
function requiredArguments(name, length, required) {
if (length < required) {
const errMsg = `${name} requires at least ${required} argument${
required === 1 ? "" : "s"
@ -17,39 +13,7 @@
}
}
function isIterable(
o,
) {
// checks for null and undefined
if (o == null) {
return false;
}
return (
typeof (o)[Symbol.iterator] === "function"
);
}
/** https://url.spec.whatwg.org/#idna */
function domainToAscii(
domain,
{ beStrict = false } = {},
) {
return core.jsonOpSync("op_domain_to_ascii", { domain, beStrict });
}
function decodeSearchParam(p) {
const s = p.replaceAll("+", " ");
const decoder = new TextDecoder();
return s.replace(/(%[0-9a-f]{2})+/gi, (matched) => {
const buf = new Uint8Array(Math.ceil(matched.length / 3));
for (let i = 0, offset = 0; i < matched.length; i += 3, offset += 1) {
buf[offset] = parseInt(matched.slice(i + 1, i + 3), 16);
}
return decoder.decode(buf);
});
}
const paramLists = new WeakMap();
const urls = new WeakMap();
class URLSearchParams {
@ -57,83 +21,56 @@
constructor(init = "") {
if (typeof init === "string") {
this.#handleStringInitialization(init);
return;
}
// Overload: USVString
// If init is a string and starts with U+003F (?),
// remove the first code point from init.
if (init[0] == "?") {
init = init.slice(1);
}
if (Array.isArray(init) || isIterable(init)) {
this.#handleArrayInitialization(init);
return;
}
if (Object(init) !== init) {
return;
}
if (init instanceof URLSearchParams) {
this.#params = core.jsonOpSync("op_parse_url_search_params", init);
} else if (
Array.isArray(init) ||
typeof init?.[Symbol.iterator] == "function"
) {
// Overload: sequence<sequence<USVString>>
for (const pair of init) {
// If pair does not contain exactly two items, then throw a TypeError.
if (pair.length !== 2) {
throw new TypeError(
"URLSearchParams.constructor sequence argument must only contain pair elements",
);
}
this.#params.push([String(pair[0]), String(pair[1])]);
}
} else if (Object(init) !== init) {
// pass
} else if (init instanceof URLSearchParams) {
this.#params = [...init.#params];
return;
}
// Overload: record<USVString, USVString>
for (const key of Object.keys(init)) {
this.#append(key, init[key]);
} else {
// Overload: record<USVString, USVString>
for (const key of Object.keys(init)) {
this.#params.push([key, String(init[key])]);
}
}
paramLists.set(this, this.#params);
urls.set(this, null);
}
#handleStringInitialization = (init) => {
// Overload: USVString
// If init is a string and starts with U+003F (?),
// remove the first code point from init.
if (init.charCodeAt(0) === 0x003f) {
init = init.slice(1);
}
for (const pair of init.split("&")) {
// Empty params are ignored
if (pair.length === 0) {
continue;
}
const position = pair.indexOf("=");
const name = pair.slice(0, position === -1 ? pair.length : position);
const value = pair.slice(name.length + 1);
this.#append(decodeSearchParam(name), decodeSearchParam(value));
}
};
#handleArrayInitialization = (
init,
) => {
// Overload: sequence<sequence<USVString>>
for (const tuple of init) {
// If pair does not contain exactly two items, then throw a TypeError.
if (tuple.length !== 2) {
throw new TypeError(
"URLSearchParams.constructor tuple array argument must only contain pair elements",
);
}
this.#append(tuple[0], tuple[1]);
}
};
#updateSteps = () => {
#updateUrlSearch = () => {
const url = urls.get(this);
if (url == null) {
return;
}
parts.get(url).query = this.toString();
};
#append = (name, value) => {
this.#params.push([String(name), String(value)]);
const parseArgs = { href: url.href, setSearch: this.toString() };
parts.set(url, core.jsonOpSync("op_parse_url", parseArgs));
};
append(name, value) {
requiredArguments("URLSearchParams.append", arguments.length, 2);
this.#append(name, value);
this.#updateSteps();
this.#params.push([String(name), String(value)]);
this.#updateUrlSearch();
}
delete(name) {
@ -147,7 +84,7 @@
i++;
}
}
this.#updateSteps();
this.#updateUrlSearch();
}
getAll(name) {
@ -208,21 +145,18 @@
// Otherwise, append a new name-value pair whose name is name
// and value is value, to list.
if (!found) {
this.#append(name, value);
this.#params.push([String(name), String(value)]);
}
this.#updateSteps();
this.#updateUrlSearch();
}
sort() {
this.#params.sort((a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1));
this.#updateSteps();
this.#updateUrlSearch();
}
forEach(
callbackfn,
thisArg,
) {
forEach(callbackfn, thisArg) {
requiredArguments("URLSearchParams.forEach", arguments.length, 1);
if (typeof thisArg !== "undefined") {
@ -255,273 +189,27 @@
}
toString() {
return this.#params
.map(
(tuple) =>
`${encodeSearchParam(tuple[0])}=${encodeSearchParam(tuple[1])}`,
)
.join("&");
return core.jsonOpSync("op_stringify_url_search_params", this.#params);
}
}
const searchParamsMethods = [
"append",
"delete",
"set",
];
const specialSchemes = ["ftp", "file", "http", "https", "ws", "wss"];
// https://url.spec.whatwg.org/#special-scheme
const schemePorts = {
ftp: "21",
file: "",
http: "80",
https: "443",
ws: "80",
wss: "443",
};
const MAX_PORT = 2 ** 16 - 1;
// Remove the part of the string that matches the pattern and return the
// remainder (RHS) as well as the first captured group of the matched substring
// (LHS). e.g.
// takePattern("https://deno.land:80", /^([a-z]+):[/]{2}/)
// = ["http", "deno.land:80"]
// takePattern("deno.land:80", /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/)
// = ["deno.land", "80"]
function takePattern(string, pattern) {
let capture = "";
const rest = string.replace(pattern, (_, capture_) => {
capture = capture_;
return "";
});
return [capture, rest];
}
function parse(url, baseParts = null) {
const parts = {};
let restUrl;
let usedNonBase = false;
[parts.protocol, restUrl] = takePattern(
url.trim(),
/^([A-Za-z][+-.0-9A-Za-z]*):/,
);
parts.protocol = parts.protocol.toLowerCase();
if (parts.protocol == "") {
if (baseParts == null) {
return null;
}
parts.protocol = baseParts.protocol;
} else if (
parts.protocol != baseParts?.protocol ||
!specialSchemes.includes(parts.protocol)
) {
usedNonBase = true;
}
const isSpecial = specialSchemes.includes(parts.protocol);
if (parts.protocol == "file") {
parts.slashes = "//";
parts.username = "";
parts.password = "";
if (usedNonBase || restUrl.match(/^[/\\]{2}/)) {
[parts.hostname, restUrl] = takePattern(
restUrl,
/^[/\\]{2}([^/\\?#]*)/,
);
usedNonBase = true;
} else {
parts.hostname = baseParts.hostname;
}
parts.port = "";
} else {
if (usedNonBase || restUrl.match(/^[/\\]{2}/)) {
let restAuthority;
if (isSpecial) {
parts.slashes = "//";
[restAuthority, restUrl] = takePattern(
restUrl,
/^[/\\]*([^/\\?#]*)/,
);
} else {
parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : "";
[restAuthority, restUrl] = takePattern(
restUrl,
/^[/\\]{2}([^/\\?#]*)/,
);
}
let restAuthentication;
[restAuthentication, restAuthority] = takePattern(
restAuthority,
/^(.*)@/,
);
[parts.username, restAuthentication] = takePattern(
restAuthentication,
/^([^:]*)/,
);
parts.username = encodeUserinfo(parts.username);
[parts.password] = takePattern(restAuthentication, /^:(.*)/);
parts.password = encodeUserinfo(parts.password);
[parts.hostname, restAuthority] = takePattern(
restAuthority,
/^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/,
);
[parts.port] = takePattern(restAuthority, /^:(.*)/);
if (!isValidPort(parts.port)) {
return null;
}
if (parts.hostname == "" && isSpecial) {
return null;
}
usedNonBase = true;
} else {
parts.slashes = baseParts.slashes;
parts.username = baseParts.username;
parts.password = baseParts.password;
parts.hostname = baseParts.hostname;
parts.port = baseParts.port;
}
}
try {
parts.hostname = encodeHostname(parts.hostname, isSpecial);
} catch {
return null;
}
[parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/);
parts.path = encodePathname(parts.path);
if (usedNonBase) {
parts.path = normalizePath(parts.path, parts.protocol == "file");
} else {
if (parts.path != "") {
usedNonBase = true;
}
parts.path = resolvePathFromBase(
parts.path,
baseParts.path || "/",
baseParts.protocol == "file",
);
}
// Drop the hostname if a drive letter is parsed.
if (parts.protocol == "file" && parts.path.match(/^\/+[A-Za-z]:(\/|$)/)) {
parts.hostname = "";
}
if (usedNonBase || restUrl.startsWith("?")) {
[parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/);
parts.query = encodeSearch(parts.query, isSpecial);
usedNonBase = true;
} else {
parts.query = baseParts.query;
}
[parts.hash] = takePattern(restUrl, /^(#.*)/);
parts.hash = encodeHash(parts.hash);
return parts;
}
// Resolves `.`s and `..`s where possible.
// Preserves repeating and trailing `/`s by design.
// Assumes drive letter file paths will have a leading slash.
function normalizePath(path, isFilePath) {
const isAbsolute = path.startsWith("/");
path = path.replace(/^\//, "");
const pathSegments = path.split("/");
let driveLetter = null;
if (isFilePath && pathSegments[0].match(/^[A-Za-z]:$/)) {
driveLetter = pathSegments.shift();
}
if (isFilePath && isAbsolute) {
while (pathSegments.length > 1 && pathSegments[0] == "") {
pathSegments.shift();
}
}
let ensureTrailingSlash = false;
const newPathSegments = [];
for (let i = 0; i < pathSegments.length; i++) {
const previous = newPathSegments[newPathSegments.length - 1];
if (
pathSegments[i] == ".." &&
previous != ".." &&
(previous != undefined || isAbsolute)
) {
newPathSegments.pop();
ensureTrailingSlash = true;
} else if (pathSegments[i] == ".") {
ensureTrailingSlash = true;
} else {
newPathSegments.push(pathSegments[i]);
ensureTrailingSlash = false;
}
}
if (driveLetter != null) {
newPathSegments.unshift(driveLetter);
}
if (newPathSegments.length == 0 && !isAbsolute) {
newPathSegments.push(".");
ensureTrailingSlash = false;
}
let newPath = newPathSegments.join("/");
if (isAbsolute) {
newPath = `/${newPath}`;
}
if (ensureTrailingSlash) {
newPath = newPath.replace(/\/*$/, "/");
}
return newPath;
}
// Standard URL basing logic, applied to paths.
function resolvePathFromBase(path, basePath, isFilePath) {
let basePrefix;
let suffix;
const baseDriveLetter = basePath.match(/^\/+[A-Za-z]:(?=\/|$)/)?.[0];
if (isFilePath && path.match(/^\/+[A-Za-z]:(\/|$)/)) {
basePrefix = "";
suffix = path;
} else if (path.startsWith("/")) {
if (isFilePath && baseDriveLetter) {
basePrefix = baseDriveLetter;
suffix = path;
} else {
basePrefix = "";
suffix = path;
}
} else if (path != "") {
basePath = normalizePath(basePath, isFilePath);
path = normalizePath(path, isFilePath);
// Remove everything after the last `/` in `basePath`.
if (baseDriveLetter && isFilePath) {
basePrefix = `${baseDriveLetter}${
basePath.slice(baseDriveLetter.length).replace(/[^\/]*$/, "")
}`;
} else {
basePrefix = basePath.replace(/[^\/]*$/, "");
}
basePrefix = basePrefix.replace(/\/*$/, "/");
// If `normalizedPath` ends with `.` or `..`, add a trailing slash.
suffix = path.replace(/(?<=(^|\/)(\.|\.\.))$/, "/");
} else {
basePrefix = basePath;
suffix = "";
}
return normalizePath(basePrefix + suffix, isFilePath);
}
function isValidPort(value) {
// https://url.spec.whatwg.org/#port-state
if (value === "") return true;
const port = Number(value);
return Number.isInteger(port) && port >= 0 && port <= MAX_PORT;
}
const parts = new WeakMap();
class URL {
#searchParams = null;
constructor(url, base) {
new.target;
if (url instanceof URL && base === undefined) {
parts.set(this, parts.get(url));
} else {
base = base !== undefined ? String(base) : base;
const parseArgs = { href: String(url), baseHref: base };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
}
}
[Symbol.for("Deno.customInspect")](inspect) {
const object = {
href: this.href,
@ -540,18 +228,14 @@
}
#updateSearchParams = () => {
const searchParams = new URLSearchParams(this.search);
for (const methodName of searchParamsMethods) {
const method = searchParams[methodName];
searchParams[methodName] = (...args) => {
method.apply(searchParams, args);
this.search = searchParams.toString();
};
if (this.#searchParams != null) {
const params = paramLists.get(this.#searchParams);
const newParams = core.jsonOpSync(
"op_parse_url_search_params",
this.search.slice(1),
);
params.splice(0, params.length, ...newParams);
}
this.#searchParams = searchParams;
urls.set(searchParams, this);
};
get hash() {
@ -559,27 +243,25 @@
}
set hash(value) {
value = unescape(String(value));
if (!value) {
parts.get(this).hash = "";
} else {
if (value.charAt(0) !== "#") {
value = `#${value}`;
}
// hashes can contain % and # unescaped
parts.get(this).hash = encodeHash(value);
try {
const parseArgs = { href: this.href, setHash: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get host() {
return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
return parts.get(this).host;
}
set host(value) {
value = String(value);
const url = new URL(`http://${value}`);
parts.get(this).hostname = url.hostname;
parts.get(this).port = url.port;
try {
const parseArgs = { href: this.href, setHost: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get hostname() {
@ -587,42 +269,30 @@
}
set hostname(value) {
value = String(value);
try {
const isSpecial = specialSchemes.includes(parts.get(this).protocol);
parts.get(this).hostname = encodeHostname(value, isSpecial);
const parseArgs = { href: this.href, setHostname: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
// pass
/* pass */
}
}
get href() {
const authentication = this.username || this.password
? `${this.username}${this.password ? ":" + this.password : ""}@`
: "";
const host = this.host;
const slashes = host ? "//" : parts.get(this).slashes;
let pathname = this.pathname;
if (pathname.charAt(0) != "/" && pathname != "" && host != "") {
pathname = `/${pathname}`;
}
return `${this.protocol}${slashes}${authentication}${host}${pathname}${this.search}${this.hash}`;
return parts.get(this).href;
}
set href(value) {
value = String(value);
if (value !== this.href) {
const url = new URL(value);
parts.set(this, { ...parts.get(url) });
this.#updateSearchParams();
try {
const parseArgs = { href: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
throw new TypeError("Invalid URL");
}
this.#updateSearchParams();
}
get origin() {
if (this.host) {
return `${this.protocol}//${this.host}`;
}
return "null";
return parts.get(this).origin;
}
get password() {
@ -630,64 +300,65 @@
}
set password(value) {
value = String(value);
parts.get(this).password = encodeUserinfo(value);
try {
const parseArgs = { href: this.href, setPassword: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get pathname() {
let path = parts.get(this).path;
if (specialSchemes.includes(parts.get(this).protocol)) {
if (path.charAt(0) != "/") {
path = `/${path}`;
}
}
return path;
return parts.get(this).pathname;
}
set pathname(value) {
parts.get(this).path = encodePathname(String(value));
try {
const parseArgs = { href: this.href, setPathname: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get port() {
const port = parts.get(this).port;
if (schemePorts[parts.get(this).protocol] === port) {
return "";
}
return port;
return parts.get(this).port;
}
set port(value) {
if (!isValidPort(value)) {
return;
try {
const parseArgs = { href: this.href, setPort: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
parts.get(this).port = value.toString();
}
get protocol() {
return `${parts.get(this).protocol}:`;
return parts.get(this).protocol;
}
set protocol(value) {
value = String(value);
if (value) {
if (value.charAt(value.length - 1) === ":") {
value = value.slice(0, -1);
}
parts.get(this).protocol = encodeURIComponent(value);
try {
const parseArgs = { href: this.href, setProtocol: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get search() {
return parts.get(this).query;
return parts.get(this).search;
}
set search(value) {
value = String(value);
const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`;
const isSpecial = specialSchemes.includes(parts.get(this).protocol);
parts.get(this).query = encodeSearch(query, isSpecial);
this.#updateSearchParams();
try {
const parseArgs = { href: this.href, setSearch: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
this.#updateSearchParams();
} catch {
/* pass */
}
}
get username() {
@ -695,35 +366,22 @@
}
set username(value) {
value = String(value);
parts.get(this).username = encodeUserinfo(value);
try {
const parseArgs = { href: this.href, setUsername: String(value) };
parts.set(this, core.jsonOpSync("op_parse_url", parseArgs));
} catch {
/* pass */
}
}
get searchParams() {
if (this.#searchParams == null) {
this.#searchParams = new URLSearchParams(this.search);
urls.set(this.#searchParams, this);
}
return this.#searchParams;
}
constructor(url, base) {
let baseParts = null;
new.target;
if (base) {
baseParts = base instanceof URL ? parts.get(base) : parse(base);
if (baseParts == null) {
throw new TypeError("Invalid base URL.");
}
}
const urlParts = url instanceof URL
? parts.get(url)
: parse(url, baseParts);
if (urlParts == null) {
throw new TypeError("Invalid URL.");
}
parts.set(this, urlParts);
this.#updateSearchParams();
}
toString() {
return this.href;
}
@ -741,166 +399,6 @@
}
}
function parseIpv4Number(s) {
if (s.match(/^(0[Xx])[0-9A-Za-z]+$/)) {
return Number(s);
}
if (s.match(/^[0-9]+$/)) {
return Number(s.startsWith("0") ? `0o${s}` : s);
}
return NaN;
}
function parseIpv4(s) {
const parts = s.split(".");
if (parts[parts.length - 1] == "" && parts.length > 1) {
parts.pop();
}
if (parts.includes("") || parts.length > 4) {
return s;
}
const numbers = parts.map(parseIpv4Number);
if (numbers.includes(NaN)) {
return s;
}
const last = numbers.pop();
if (last >= 256 ** (4 - numbers.length) || numbers.find((n) => n >= 256)) {
throw new TypeError("Invalid hostname.");
}
const ipv4 = numbers.reduce((sum, n, i) => sum + n * 256 ** (3 - i), last);
const ipv4Hex = ipv4.toString(16).padStart(8, "0");
const ipv4HexParts = ipv4Hex.match(/(..)(..)(..)(..)$/).slice(1);
return ipv4HexParts.map((s) => String(Number(`0x${s}`))).join(".");
}
function charInC0ControlSet(c) {
return (c >= "\u0000" && c <= "\u001F") || c > "\u007E";
}
function charInSearchSet(c, isSpecial) {
// deno-fmt-ignore
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u003C", "\u003E"].includes(c) || isSpecial && c == "\u0027" || c > "\u007E";
}
function charInFragmentSet(c) {
// deno-fmt-ignore
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c);
}
function charInPathSet(c) {
// deno-fmt-ignore
return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c);
}
function charInUserinfoSet(c) {
// "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox.
// deno-fmt-ignore
return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c);
}
function charIsForbiddenInHost(c) {
// deno-fmt-ignore
return ["\u0000", "\u0009", "\u000A", "\u000D", "\u0020", "\u0023", "\u0025", "\u002F", "\u003A", "\u003C", "\u003E", "\u003F", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E"].includes(c);
}
function charInFormUrlencodedSet(c) {
// deno-fmt-ignore
return charInUserinfoSet(c) || ["\u0021", "\u0024", "\u0025", "\u0026", "\u0027", "\u0028", "\u0029", "\u002B", "\u002C", "\u007E"].includes(c);
}
const encoder = new TextEncoder();
function encodeChar(c) {
return [...encoder.encode(c)]
.map((n) => `%${n.toString(16).padStart(2, "0")}`)
.join("")
.toUpperCase();
}
function encodeUserinfo(s) {
return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join(
"",
);
}
function encodeHostname(s, isSpecial = true) {
// IPv6 parsing.
if (s.startsWith("[") && s.endsWith("]")) {
if (!s.match(/^\[[0-9A-Fa-f.:]{2,}\]$/)) {
throw new TypeError("Invalid hostname.");
}
// IPv6 address compress
return s.toLowerCase().replace(/\b:?(?:0+:?){2,}/, "::");
}
let result = s;
if (!isSpecial) {
// Check against forbidden host code points except for "%".
for (const c of result) {
if (charIsForbiddenInHost(c) && c != "\u0025") {
throw new TypeError("Invalid hostname.");
}
}
// Percent-encode C0 control set.
result = [...result]
.map((c) => (charInC0ControlSet(c) ? encodeChar(c) : c))
.join("");
return result;
}
// Percent-decode.
if (result.match(/%(?![0-9A-Fa-f]{2})/) != null) {
throw new TypeError("Invalid hostname.");
}
result = result.replace(
/%(.{2})/g,
(_, hex) => String.fromCodePoint(Number(`0x${hex}`)),
);
// IDNA domain to ASCII.
result = domainToAscii(result);
// Check against forbidden host code points.
for (const c of result) {
if (charIsForbiddenInHost(c)) {
throw new TypeError("Invalid hostname.");
}
}
// IPv4 parsing.
if (isSpecial) {
result = parseIpv4(result);
}
return result;
}
function encodePathname(s) {
return [...s.replace(/\\/g, "/")].map((
c,
) => (charInPathSet(c) ? encodeChar(c) : c)).join("");
}
function encodeSearch(s, isSpecial) {
return [...s].map((
c,
) => (charInSearchSet(c, isSpecial) ? encodeChar(c) : c)).join("");
}
function encodeHash(s) {
return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join(
"",
);
}
function encodeSearchParam(s) {
return [...s].map((c) => (charInFormUrlencodedSet(c) ? encodeChar(c) : c))
.join("").replace(/%20/g, "+");
}
window.__bootstrap.url = {
URL,
URLSearchParams,

View file

@ -15,7 +15,6 @@ path = "lib.rs"
[dependencies]
deno_core = { version = "0.79.0", path = "../../core" }
idna = "0.2.1"
serde = { version = "1.0.123", features = ["derive"] }
[dev-dependencies]

View file

@ -1,42 +1,22 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use deno_core::error::generic_error;
use deno_core::error::type_error;
use deno_core::error::uri_error;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::url::form_urlencoded;
use deno_core::url::quirks;
use deno_core::url::Url;
use deno_core::JsRuntime;
use deno_core::ZeroCopyBuf;
use idna::domain_to_ascii;
use idna::domain_to_ascii_strict;
use serde::Deserialize;
use serde::Serialize;
use std::panic::catch_unwind;
use std::path::PathBuf;
pub fn op_domain_to_ascii(
_state: &mut deno_core::OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DomainToAscii {
domain: String,
be_strict: bool,
}
let args: DomainToAscii = serde_json::from_value(args)?;
if args.be_strict {
domain_to_ascii_strict(args.domain.as_str())
} else {
domain_to_ascii(args.domain.as_str())
}
.map_err(|err| {
let message = format!("Invalid IDNA encoded domain name: {:?}", err);
uri_error(message)
})
.map(|domain| json!(domain))
}
/// Load and execute the javascript code.
pub fn init(isolate: &mut JsRuntime) {
let files = vec![
@ -79,6 +59,131 @@ pub fn init(isolate: &mut JsRuntime) {
}
}
/// Parse `UrlParseArgs::href` with an optional `UrlParseArgs::base_href`, or an
/// optional part to "set" after parsing. Return `UrlParts`.
pub fn op_parse_url(
_state: &mut deno_core::OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UrlParseArgs {
href: String,
base_href: Option<String>,
// If one of the following are present, this is a setter call. Apply the
// proper `Url::set_*()` method after (re)parsing `href`.
set_hash: Option<String>,
set_host: Option<String>,
set_hostname: Option<String>,
set_password: Option<String>,
set_pathname: Option<String>,
set_port: Option<String>,
set_protocol: Option<String>,
set_search: Option<String>,
set_username: Option<String>,
}
let args: UrlParseArgs = serde_json::from_value(args)?;
let base_url = args
.base_href
.as_ref()
.map(|b| Url::parse(b).map_err(|_| type_error("Invalid base URL")))
.transpose()?;
let mut url = Url::options()
.base_url(base_url.as_ref())
.parse(&args.href)
.map_err(|_| type_error("Invalid URL"))?;
if let Some(hash) = args.set_hash.as_ref() {
quirks::set_hash(&mut url, hash);
} else if let Some(host) = args.set_host.as_ref() {
quirks::set_host(&mut url, host).map_err(|_| uri_error("Invalid host"))?;
} else if let Some(hostname) = args.set_hostname.as_ref() {
quirks::set_hostname(&mut url, hostname)
.map_err(|_| uri_error("Invalid hostname"))?;
} else if let Some(password) = args.set_password.as_ref() {
quirks::set_password(&mut url, password)
.map_err(|_| uri_error("Invalid password"))?;
} else if let Some(pathname) = args.set_pathname.as_ref() {
quirks::set_pathname(&mut url, pathname);
} else if let Some(port) = args.set_port.as_ref() {
quirks::set_port(&mut url, port).map_err(|_| uri_error("Invalid port"))?;
} else if let Some(protocol) = args.set_protocol.as_ref() {
quirks::set_protocol(&mut url, protocol)
.map_err(|_| uri_error("Invalid protocol"))?;
} else if let Some(search) = args.set_search.as_ref() {
quirks::set_search(&mut url, search);
} else if let Some(username) = args.set_username.as_ref() {
quirks::set_username(&mut url, username)
.map_err(|_| uri_error("Invalid username"))?;
}
#[derive(Serialize)]
struct UrlParts<'a> {
href: &'a str,
hash: &'a str,
host: &'a str,
hostname: &'a str,
origin: &'a str,
password: &'a str,
pathname: &'a str,
port: &'a str,
protocol: &'a str,
search: &'a str,
username: &'a str,
}
// TODO(nayeemrmn): Panic that occurs in rust-url for the `non-spec:`
// url-constructor wpt tests: https://github.com/servo/rust-url/issues/670.
let username = catch_unwind(|| quirks::username(&url)).map_err(|_| {
generic_error(format!(
"Internal error while parsing \"{}\"{}, \
see https://github.com/servo/rust-url/issues/670",
args.href,
args
.base_href
.map(|b| format!(" against \"{}\"", b))
.unwrap_or_default()
))
})?;
Ok(json!(UrlParts {
href: quirks::href(&url),
hash: quirks::hash(&url),
host: quirks::host(&url),
hostname: quirks::hostname(&url),
origin: &quirks::origin(&url),
password: quirks::password(&url),
pathname: quirks::pathname(&url),
port: quirks::port(&url),
protocol: quirks::protocol(&url),
search: quirks::search(&url),
username,
}))
}
pub fn op_parse_url_search_params(
_state: &mut deno_core::OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
let search: String = serde_json::from_value(args)?;
let search_params: Vec<_> = form_urlencoded::parse(search.as_bytes())
.into_iter()
.collect();
Ok(json!(search_params))
}
pub fn op_stringify_url_search_params(
_state: &mut deno_core::OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
let search_params: Vec<(String, String)> = serde_json::from_value(args)?;
let search = form_urlencoded::Serializer::new(String::new())
.extend_pairs(search_params)
.finish();
Ok(json!(search))
}
pub fn get_declaration() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_web.d.ts")
}

View file

@ -231,10 +231,16 @@ impl WebWorker {
);
ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close);
ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources);
ops::reg_json_sync(js_runtime, "op_parse_url", deno_web::op_parse_url);
ops::reg_json_sync(
js_runtime,
"op_domain_to_ascii",
deno_web::op_domain_to_ascii,
"op_parse_url_search_params",
deno_web::op_parse_url_search_params,
);
ops::reg_json_sync(
js_runtime,
"op_stringify_url_search_params",
deno_web::op_stringify_url_search_params,
);
ops::io::init(js_runtime);
ops::webgpu::init(js_runtime);

View file

@ -126,10 +126,16 @@ impl MainWorker {
ops::crypto::init(js_runtime, options.seed);
ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close);
ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources);
ops::reg_json_sync(js_runtime, "op_parse_url", deno_web::op_parse_url);
ops::reg_json_sync(
js_runtime,
"op_domain_to_ascii",
deno_web::op_domain_to_ascii,
"op_parse_url_search_params",
deno_web::op_parse_url_search_params,
);
ops::reg_json_sync(
js_runtime,
"op_stringify_url_search_params",
deno_web::op_stringify_url_search_params,
);
ops::fs_events::init(js_runtime);
ops::fs::init(js_runtime);

View file

@ -629,62 +629,8 @@
],
"idlharness.any.js": false,
"url-constructor.any.js": [
"Parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
"Parsing: <a:\t foo.com> against <http://example.org/foo/bar>",
"Parsing: <lolscheme:x x#x x> against <about:blank>",
"Parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
"Parsing: <http://f:00000000000000000000080/c> against <http://example.org/foo/bar>",
"Parsing: <http://f: /c> against <http://example.org/foo/bar>",
"Parsing: <http://f: 21 / b ? d # e > against <http://example.org/foo/bar>",
"Parsing: <:#> against <http://example.org/foo/bar>",
"Parsing: <#> against <http://example.org/foo/bar>",
"Parsing: <?> against <http://example.org/foo/bar>",
"Parsing: <http://[::127.0.0.1]> against <http://example.org/foo/bar>",
"Parsing: <http://[0:0:0:0:0:0:13.1.68.3]> against <http://example.org/foo/bar>",
"Parsing: <file:c:\\foo\\bar.html> against <file:///tmp/mock/path>",
"Parsing: < File:c|////foo\\bar.html> against <file:///tmp/mock/path>",
"Parsing: <C|/foo/bar> against <file:///tmp/mock/path>",
"Parsing: </C|\\foo\\bar> against <file:///tmp/mock/path>",
"Parsing: <//C|/foo/bar> against <file:///tmp/mock/path>",
"Parsing: <file://localhost> against <file:///tmp/mock/path>",
"Parsing: <file://localhost/> against <file:///tmp/mock/path>",
"Parsing: <file://localhost/test> against <file:///tmp/mock/path>",
"Parsing: <http://example.com/foo/%2e> against <about:blank>",
"Parsing: <http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar> against <about:blank>",
"Parsing: <http://example.com////../..> against <about:blank>",
"Parsing: <http://example.com/foo\t‘%91> against <about:blank>",
"Parsing: <http://example.com/foo\tbar> against <about:blank>",
"Parsing: <http://www.google.com/foo?bar=baz#> against <about:blank>",
"Parsing: <http://www/foo/%2E/html> against <about:blank>",
"Parsing: <file:..> against <http://www.example.com/test>",
"Parsing: <\u0000\u001b\u0004\u0012 http://example.com/\u001f \r > against <about:blank>",
"Parsing: <https://%EF%BF%BD> against <about:blank>",
"Parsing: <http://[::1.2.3.]> against <http://other.com/>",
"Parsing: <http://[::1.2.]> against <http://other.com/>",
"Parsing: <http://[::1.]> against <http://other.com/>",
"Parsing: <#> against <test:test>",
"Parsing: <#> against <test:test?test>",
"Parsing: <i> against <sc:sd>",
"Parsing: <i> against <sc:sd/sd>",
"Parsing: <../i> against <sc:sd>",
"Parsing: <../i> against <sc:sd/sd>",
"Parsing: </i> against <sc:sd>",
"Parsing: </i> against <sc:sd/sd>",
"Parsing: <?i> against <sc:sd>",
"Parsing: <?i> against <sc:sd/sd>",
"Parsing: <sc://@/> against <about:blank>",
"Parsing: <sc://te@s:t@/> against <about:blank>",
"Parsing: <sc://:/> against <about:blank>",
"Parsing: <sc://:12/> against <about:blank>",
"Parsing: <sc://\\/> against <about:blank>",
"Parsing: <sc:\\../> against <about:blank>",
"Parsing: <ftp://%e2%98%83> against <about:blank>",
"Parsing: <https://%e2%98%83> against <about:blank>",
"Parsing: <h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg> against <about:blank>",
"Parsing: <https://0x.0x.0> against <about:blank>",
"Parsing: <http://example.com/\ud800𐟾\udfff﷐﷏﷯ﷰ￾￿?\ud800𐟾\udfff﷐﷏﷯ﷰ￾￿> against <about:blank>",
"Parsing: </> against <file://h/C:/a/b>",
"Parsing: <//d:> against <file:///C:/a/b>",
"Parsing: <//d:/..> against <file:///C:/a/b>",
"Parsing: <file:\\\\//> against <about:blank>",
"Parsing: <file:\\\\\\\\> against <about:blank>",
"Parsing: <file:\\\\\\\\?fox> against <about:blank>",
@ -696,6 +642,7 @@
"Parsing: </////mouse> against <file:///elephant>",
"Parsing: <\\/localhost//pig> against <file://lion/>",
"Parsing: <//localhost//pig> against <file://lion/>",
"Parsing: </..//localhost//pig> against <file://lion/>",
"Parsing: <C|> against <file://host/dir/file>",
"Parsing: <C|> against <file://host/D:/dir1/dir2/file>",
"Parsing: <C|#> against <file://host/dir/file>",
@ -703,34 +650,18 @@
"Parsing: <C|/> against <file://host/dir/file>",
"Parsing: <C|\n/> against <file://host/dir/file>",
"Parsing: <C|\\> against <file://host/dir/file>",
"Parsing: </c|/foo/bar> against <file:///c:/baz/qux>",
"Parsing: </c:/foo/bar> against <file://host/path>",
"Parsing: <file://example.net/C:/> against <about:blank>",
"Parsing: <file://1.2.3.4/C:/> against <about:blank>",
"Parsing: <file://[1::8]/C:/> against <about:blank>",
"Parsing: <file:/C|/> against <about:blank>",
"Parsing: <file://C|/> against <about:blank>",
"Parsing: <\\\\\\.\\Y:> against <about:blank>",
"Parsing: <\\\\\\.\\y:> against <about:blank>",
"Parsing: <file://localhost//a//../..//foo> against <about:blank>",
"Parsing: <file://localhost////foo> against <about:blank>",
"Parsing: <file:////foo> against <about:blank>",
"Parsing: <file:////one/two> against <file:///>",
"Parsing: <////one/two> against <file:///>",
"Parsing: <file:///.//> against <file:////>",
"Parsing: <file:.//p> against <about:blank>",
"Parsing: <http://[1:0::]> against <http://example.net/>",
"Parsing: <http://[0:1:2:3:4:5:6:7:8]> against <http://example.net/>",
"Parsing: <https://[0::0::0]> against <about:blank>",
"Parsing: <https://[0:.0]> against <about:blank>",
"Parsing: <https://[0:0:]> against <about:blank>",
"Parsing: <https://[0:1:2:3:4:5:6:7.0.0.0.1]> against <about:blank>",
"Parsing: <https://[0:1.00.0.0.0]> against <about:blank>",
"Parsing: <https://[0:1.290.0.0.0]> against <about:blank>",
"Parsing: <https://[0:1.23.23]> against <about:blank>",
"Parsing: <#x> against <sc://ñ>",
"Parsing: <?x> against <sc://ñ>",
"Parsing: <sc://?> against <about:blank>",
"Parsing: <sc://#> against <about:blank>",
"Parsing: <file:/.//p> against <about:blank>",
"Parsing: <non-spec:/.//> against <about:blank>",
"Parsing: <non-spec:/..//> against <about:blank>",
"Parsing: <non-spec:/a/..//> against <about:blank>",
@ -742,227 +673,32 @@
"Parsing: <..//path> against <non-spec:/p>",
"Parsing: <a/..//path> against <non-spec:/p>",
"Parsing: <> against <non-spec:/..//p>",
"Parsing: <path> against <non-spec:/..//p>",
"Parsing: <non-special://[1:2:0:0:5:0:0:0]/> against <about:blank>",
"Parsing: <http://[::127.0.0.0.1]> against <about:blank>",
"Parsing: <http://example.org/test?#> against <about:blank>",
"Parsing: <a> against <about:blank>",
"Parsing: <a/> against <about:blank>",
"Parsing: <a//> against <about:blank>",
"Parsing: <test-a-colon.html> against <a:>",
"Parsing: <test-a-colon-b.html> against <a:b>",
"Parsing: <file://a%C2%ADb/p> against <about:blank>",
"Parsing: <file://­/p> against <about:blank>",
"Parsing: <file://%C2%AD/p> against <about:blank>",
"Parsing: <file://xn--/p> against <about:blank>"
"Parsing: <path> against <non-spec:/..//p>"
],
"url-origin.any.js": [
"Origin parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
"Origin parsing: <non-special://test:@test/x> against <about:blank>",
"Origin parsing: <non-special://:@test/x> against <about:blank>",
"Origin parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
"Origin parsing: <http://f:00000000000000000000080/c> against <http://example.org/foo/bar>",
"Origin parsing: <http://[::127.0.0.1]> against <http://example.org/foo/bar>",
"Origin parsing: <http://[0:0:0:0:0:0:13.1.68.3]> against <http://example.org/foo/bar>",
"Origin parsing: <ssh://example.com/foo/bar.git> against <http://example.org/>",
"Origin parsing: <httpa://foo:80/> against <about:blank>",
"Origin parsing: <gopher://foo:70/> against <about:blank>",
"Origin parsing: <gopher://foo:443/> against <about:blank>",
"Origin parsing: <\u0000\u001b\u0004\u0012 http://example.com/\u001f \r > against <about:blank>",
"Origin parsing: <sc://faß.ExAmPlE/> against <about:blank>",
"Origin parsing: <notspecial://host/?'> against <about:blank>",
"Origin parsing: <i> against <sc://ho/pa>",
"Origin parsing: <../i> against <sc://ho/pa>",
"Origin parsing: </i> against <sc://ho/pa>",
"Origin parsing: <?i> against <sc://ho/pa>",
"Origin parsing: <#i> against <sc://ho/pa>",
"Origin parsing: <sc://ñ.test/> against <about:blank>",
"Origin parsing: <x> against <sc://ñ>",
"Origin parsing: <sc://\u001f!\"$&'()*+,-.;=_`{|}~/> against <about:blank>",
"Origin parsing: <ftp://%e2%98%83> against <about:blank>",
"Origin parsing: <https://%e2%98%83> against <about:blank>",
"Origin parsing: <h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg> against <about:blank>",
"Origin parsing: <https://0x.0x.0> against <about:blank>",
"Origin parsing: <http://[1:0::]> against <http://example.net/>",
"Origin parsing: <sc://ñ> against <about:blank>",
"Origin parsing: <sc://ñ?x> against <about:blank>",
"Origin parsing: <sc://ñ#x> against <about:blank>",
"Origin parsing: <#x> against <sc://ñ>",
"Origin parsing: <?x> against <sc://ñ>",
"Origin parsing: <tftp://foobar.com/someconfig;mode=netascii> against <about:blank>",
"Origin parsing: <telnet://user:pass@foobar.com:23/> against <about:blank>",
"Origin parsing: <ut2004://10.10.10.10:7777/Index.ut2> against <about:blank>",
"Origin parsing: <redis://foo:bar@somehost:6379/0?baz=bam&qux=baz> against <about:blank>",
"Origin parsing: <rsync://foo@host:911/sup> against <about:blank>",
"Origin parsing: <git://github.com/foo/bar.git> against <about:blank>",
"Origin parsing: <irc://myserver.com:6999/channel?passwd> against <about:blank>",
"Origin parsing: <dns://fw.example.org:9999/foo.bar.org?type=TXT> against <about:blank>",
"Origin parsing: <ldap://localhost:389/ou=People,o=JNDITutorial> against <about:blank>",
"Origin parsing: <git+https://github.com/foo/bar> against <about:blank>"
],
"url-searchparams.any.js": [
"URL.searchParams updating, clearing",
"URL.searchParams and URL.search setters, update propagation"
"Origin parsing: <http://example.com/\ud800𐟾\udfff﷐﷏﷯ﷰ￾￿?\ud800𐟾\udfff﷐﷏﷯ﷰ￾￿> against <about:blank>"
],
"url-searchparams.any.js": true,
"url-setters-stripping.any.js": [
"Setting protocol with leading U+0000 (https:)",
"Setting protocol with U+0000 before inserted colon (https:)",
"Setting host with leading U+0000 (https:)",
"Setting host with middle U+0000 (https:)",
"Setting host with trailing U+0000 (https:)",
"Setting port with middle U+0000 (https:)",
"Setting port with trailing U+0000 (https:)",
"Setting protocol with leading U+0009 (https:)",
"Setting protocol with U+0009 before inserted colon (https:)",
"Setting host with leading U+0009 (https:)",
"Setting hostname with leading U+0009 (https:)",
"Setting host with middle U+0009 (https:)",
"Setting hostname with middle U+0009 (https:)",
"Setting host with trailing U+0009 (https:)",
"Setting hostname with trailing U+0009 (https:)",
"Setting port with leading U+0009 (https:)",
"Setting port with middle U+0009 (https:)",
"Setting port with trailing U+0009 (https:)",
"Setting pathname with leading U+0009 (https:)",
"Setting pathname with middle U+0009 (https:)",
"Setting pathname with trailing U+0009 (https:)",
"Setting search with leading U+0009 (https:)",
"Setting search with middle U+0009 (https:)",
"Setting search with trailing U+0009 (https:)",
"Setting hash with leading U+0009 (https:)",
"Setting hash with middle U+0009 (https:)",
"Setting hash with trailing U+0009 (https:)",
"Setting protocol with leading U+000A (https:)",
"Setting protocol with U+000A before inserted colon (https:)",
"Setting host with leading U+000A (https:)",
"Setting hostname with leading U+000A (https:)",
"Setting host with middle U+000A (https:)",
"Setting hostname with middle U+000A (https:)",
"Setting host with trailing U+000A (https:)",
"Setting hostname with trailing U+000A (https:)",
"Setting port with leading U+000A (https:)",
"Setting port with middle U+000A (https:)",
"Setting port with trailing U+000A (https:)",
"Setting pathname with leading U+000A (https:)",
"Setting pathname with middle U+000A (https:)",
"Setting pathname with trailing U+000A (https:)",
"Setting search with leading U+000A (https:)",
"Setting search with middle U+000A (https:)",
"Setting search with trailing U+000A (https:)",
"Setting hash with leading U+000A (https:)",
"Setting hash with middle U+000A (https:)",
"Setting hash with trailing U+000A (https:)",
"Setting protocol with leading U+000D (https:)",
"Setting protocol with U+000D before inserted colon (https:)",
"Setting host with leading U+000D (https:)",
"Setting hostname with leading U+000D (https:)",
"Setting host with middle U+000D (https:)",
"Setting hostname with middle U+000D (https:)",
"Setting host with trailing U+000D (https:)",
"Setting hostname with trailing U+000D (https:)",
"Setting port with leading U+000D (https:)",
"Setting port with middle U+000D (https:)",
"Setting port with trailing U+000D (https:)",
"Setting pathname with leading U+000D (https:)",
"Setting pathname with middle U+000D (https:)",
"Setting pathname with trailing U+000D (https:)",
"Setting search with leading U+000D (https:)",
"Setting search with middle U+000D (https:)",
"Setting search with trailing U+000D (https:)",
"Setting hash with leading U+000D (https:)",
"Setting hash with middle U+000D (https:)",
"Setting hash with trailing U+000D (https:)",
"Setting port with leading U+0000 (https:)",
"Setting pathname with trailing U+0000 (https:)",
"Setting protocol with leading U+001F (https:)",
"Setting protocol with U+001F before inserted colon (https:)",
"Setting host with leading U+001F (https:)",
"Setting host with middle U+001F (https:)",
"Setting host with trailing U+001F (https:)",
"Setting port with middle U+001F (https:)",
"Setting port with trailing U+001F (https:)",
"Setting port with leading U+001F (https:)",
"Setting pathname with trailing U+001F (https:)",
"Setting protocol with leading U+0000 (wpt++:)",
"Setting protocol with U+0000 before inserted colon (wpt++:)",
"Setting host with leading U+0000 (wpt++:)",
"Setting host with middle U+0000 (wpt++:)",
"Setting host with trailing U+0000 (wpt++:)",
"Setting port with middle U+0000 (wpt++:)",
"Setting port with trailing U+0000 (wpt++:)",
"Setting pathname with leading U+0000 (wpt++:)",
"Setting pathname with middle U+0000 (wpt++:)",
"Setting port with leading U+0000 (wpt++:)",
"Setting pathname with trailing U+0000 (wpt++:)",
"Setting protocol with leading U+0009 (wpt++:)",
"Setting protocol with U+0009 before inserted colon (wpt++:)",
"Setting host with leading U+0009 (wpt++:)",
"Setting hostname with leading U+0009 (wpt++:)",
"Setting host with middle U+0009 (wpt++:)",
"Setting hostname with middle U+0009 (wpt++:)",
"Setting host with trailing U+0009 (wpt++:)",
"Setting hostname with trailing U+0009 (wpt++:)",
"Setting port with leading U+0009 (wpt++:)",
"Setting port with middle U+0009 (wpt++:)",
"Setting port with trailing U+0009 (wpt++:)",
"Setting pathname with leading U+0009 (wpt++:)",
"Setting pathname with middle U+0009 (wpt++:)",
"Setting pathname with trailing U+0009 (wpt++:)",
"Setting search with leading U+0009 (wpt++:)",
"Setting search with middle U+0009 (wpt++:)",
"Setting search with trailing U+0009 (wpt++:)",
"Setting hash with leading U+0009 (wpt++:)",
"Setting hash with middle U+0009 (wpt++:)",
"Setting hash with trailing U+0009 (wpt++:)",
"Setting protocol with leading U+000A (wpt++:)",
"Setting protocol with U+000A before inserted colon (wpt++:)",
"Setting host with leading U+000A (wpt++:)",
"Setting hostname with leading U+000A (wpt++:)",
"Setting host with middle U+000A (wpt++:)",
"Setting hostname with middle U+000A (wpt++:)",
"Setting host with trailing U+000A (wpt++:)",
"Setting hostname with trailing U+000A (wpt++:)",
"Setting port with leading U+000A (wpt++:)",
"Setting port with middle U+000A (wpt++:)",
"Setting port with trailing U+000A (wpt++:)",
"Setting pathname with leading U+000A (wpt++:)",
"Setting pathname with middle U+000A (wpt++:)",
"Setting pathname with trailing U+000A (wpt++:)",
"Setting search with leading U+000A (wpt++:)",
"Setting search with middle U+000A (wpt++:)",
"Setting search with trailing U+000A (wpt++:)",
"Setting hash with leading U+000A (wpt++:)",
"Setting hash with middle U+000A (wpt++:)",
"Setting hash with trailing U+000A (wpt++:)",
"Setting protocol with leading U+000D (wpt++:)",
"Setting protocol with U+000D before inserted colon (wpt++:)",
"Setting host with leading U+000D (wpt++:)",
"Setting hostname with leading U+000D (wpt++:)",
"Setting host with middle U+000D (wpt++:)",
"Setting hostname with middle U+000D (wpt++:)",
"Setting host with trailing U+000D (wpt++:)",
"Setting hostname with trailing U+000D (wpt++:)",
"Setting port with leading U+000D (wpt++:)",
"Setting port with middle U+000D (wpt++:)",
"Setting port with trailing U+000D (wpt++:)",
"Setting pathname with leading U+000D (wpt++:)",
"Setting pathname with middle U+000D (wpt++:)",
"Setting pathname with trailing U+000D (wpt++:)",
"Setting search with leading U+000D (wpt++:)",
"Setting search with middle U+000D (wpt++:)",
"Setting search with trailing U+000D (wpt++:)",
"Setting hash with leading U+000D (wpt++:)",
"Setting hash with middle U+000D (wpt++:)",
"Setting hash with trailing U+000D (wpt++:)",
"Setting protocol with leading U+001F (wpt++:)",
"Setting protocol with U+001F before inserted colon (wpt++:)",
"Setting host with leading U+001F (wpt++:)",
"Setting host with middle U+001F (wpt++:)",
"Setting host with trailing U+001F (wpt++:)",
"Setting port with middle U+001F (wpt++:)",
"Setting port with trailing U+001F (wpt++:)",
"Setting pathname with leading U+001F (wpt++:)",
"Setting pathname with middle U+001F (wpt++:)",
"Setting port with leading U+001F (wpt++:)",
"Setting pathname with trailing U+001F (wpt++:)"
],
"url-tojson.any.js": true,
"urlencoded-parser.any.js": [
"URLSearchParams constructed with: %EF%BB%BFtest=%EF%BB%BF",
"request.formData() with input: test=",
"response.formData() with input: test=",
"request.formData() with input: †&†=x",