mirror of
https://github.com/denoland/deno.git
synced 2024-12-28 18:19:08 -05:00
a8f74aa381
- Fix protocol regex. - Truncate repeated leading slashes in file paths. - Make drive letter support platform-independent. - Drop the hostname if a drive letter is parsed. - Fix drive letter normalization and basing. - Allow basing over the host. - Fix same-protocol basing. - Remove Windows UNC path support. - Reverts #6418. This is non-standard. Wouldn't be too much of a problem but it makes other parts of the spec hard to realize.
884 lines
23 KiB
JavaScript
884 lines
23 KiB
JavaScript
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
|
|
|
((window) => {
|
|
const { getRandomValues } = window.__bootstrap.crypto;
|
|
const { customInspect } = window.__bootstrap.console;
|
|
const { sendSync } = window.__bootstrap.dispatchJson;
|
|
const { isIterable, requiredArguments } = window.__bootstrap.webUtil;
|
|
|
|
/** https://url.spec.whatwg.org/#idna */
|
|
function domainToAscii(
|
|
domain,
|
|
{ beStrict = false } = {},
|
|
) {
|
|
return sendSync("op_domain_to_ascii", { domain, beStrict });
|
|
}
|
|
|
|
const urls = new WeakMap();
|
|
|
|
class URLSearchParams {
|
|
#params = [];
|
|
|
|
constructor(init = "") {
|
|
if (typeof init === "string") {
|
|
this.#handleStringInitialization(init);
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(init) || isIterable(init)) {
|
|
this.#handleArrayInitialization(init);
|
|
return;
|
|
}
|
|
|
|
if (Object(init) !== init) {
|
|
return;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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(decodeURIComponent(name), decodeURIComponent(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 = () => {
|
|
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)]);
|
|
};
|
|
|
|
append(name, value) {
|
|
requiredArguments("URLSearchParams.append", arguments.length, 2);
|
|
this.#append(name, value);
|
|
this.#updateSteps();
|
|
}
|
|
|
|
delete(name) {
|
|
requiredArguments("URLSearchParams.delete", arguments.length, 1);
|
|
name = String(name);
|
|
let i = 0;
|
|
while (i < this.#params.length) {
|
|
if (this.#params[i][0] === name) {
|
|
this.#params.splice(i, 1);
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
this.#updateSteps();
|
|
}
|
|
|
|
getAll(name) {
|
|
requiredArguments("URLSearchParams.getAll", arguments.length, 1);
|
|
name = String(name);
|
|
const values = [];
|
|
for (const entry of this.#params) {
|
|
if (entry[0] === name) {
|
|
values.push(entry[1]);
|
|
}
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
get(name) {
|
|
requiredArguments("URLSearchParams.get", arguments.length, 1);
|
|
name = String(name);
|
|
for (const entry of this.#params) {
|
|
if (entry[0] === name) {
|
|
return entry[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
has(name) {
|
|
requiredArguments("URLSearchParams.has", arguments.length, 1);
|
|
name = String(name);
|
|
return this.#params.some((entry) => entry[0] === name);
|
|
}
|
|
|
|
set(name, value) {
|
|
requiredArguments("URLSearchParams.set", arguments.length, 2);
|
|
|
|
// If there are any name-value pairs whose name is name, in list,
|
|
// set the value of the first such name-value pair to value
|
|
// and remove the others.
|
|
name = String(name);
|
|
value = String(value);
|
|
let found = false;
|
|
let i = 0;
|
|
while (i < this.#params.length) {
|
|
if (this.#params[i][0] === name) {
|
|
if (!found) {
|
|
this.#params[i][1] = value;
|
|
found = true;
|
|
i++;
|
|
} else {
|
|
this.#params.splice(i, 1);
|
|
}
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Otherwise, append a new name-value pair whose name is name
|
|
// and value is value, to list.
|
|
if (!found) {
|
|
this.#append(name, value);
|
|
}
|
|
|
|
this.#updateSteps();
|
|
}
|
|
|
|
sort() {
|
|
this.#params.sort((a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1));
|
|
this.#updateSteps();
|
|
}
|
|
|
|
forEach(
|
|
callbackfn,
|
|
thisArg,
|
|
) {
|
|
requiredArguments("URLSearchParams.forEach", arguments.length, 1);
|
|
|
|
if (typeof thisArg !== "undefined") {
|
|
callbackfn = callbackfn.bind(thisArg);
|
|
}
|
|
|
|
for (const [key, value] of this.#params) {
|
|
callbackfn(value, key, this);
|
|
}
|
|
}
|
|
|
|
*keys() {
|
|
for (const [key] of this.#params) {
|
|
yield key;
|
|
}
|
|
}
|
|
|
|
*values() {
|
|
for (const [, value] of this.#params) {
|
|
yield value;
|
|
}
|
|
}
|
|
|
|
*entries() {
|
|
yield* this.#params;
|
|
}
|
|
|
|
*[Symbol.iterator]() {
|
|
yield* this.#params;
|
|
}
|
|
|
|
toString() {
|
|
return this.#params
|
|
.map(
|
|
(tuple) =>
|
|
`${encodeURIComponent(tuple[0])}=${encodeURIComponent(tuple[1])}`,
|
|
)
|
|
.join("&");
|
|
}
|
|
}
|
|
|
|
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.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.replace(/\\/g, "/"));
|
|
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);
|
|
usedNonBase = true;
|
|
} else {
|
|
parts.query = baseParts.query;
|
|
}
|
|
[parts.hash] = takePattern(restUrl, /^(#.*)/);
|
|
parts.hash = encodeHash(parts.hash);
|
|
return parts;
|
|
}
|
|
|
|
// Based on https://github.com/kelektiv/node-uuid
|
|
// TODO(kevinkassimo): Use deno_std version once possible.
|
|
function generateUUID() {
|
|
return "00000000-0000-4000-8000-000000000000".replace(/[0]/g, () =>
|
|
// random integer from 0 to 15 as a hex digit.
|
|
(getRandomValues(new Uint8Array(1))[0] % 16).toString(16));
|
|
}
|
|
|
|
// Keep it outside of URL to avoid any attempts of access.
|
|
const blobURLMap = new Map();
|
|
|
|
// 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;
|
|
|
|
[customInspect]() {
|
|
const keys = [
|
|
"href",
|
|
"origin",
|
|
"protocol",
|
|
"username",
|
|
"password",
|
|
"host",
|
|
"hostname",
|
|
"port",
|
|
"pathname",
|
|
"hash",
|
|
"search",
|
|
];
|
|
const objectString = keys
|
|
.map((key) => `${key}: "${this[key] || ""}"`)
|
|
.join(", ");
|
|
return `URL { ${objectString} }`;
|
|
}
|
|
|
|
#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();
|
|
};
|
|
}
|
|
this.#searchParams = searchParams;
|
|
|
|
urls.set(searchParams, this);
|
|
};
|
|
|
|
get hash() {
|
|
return parts.get(this).hash;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
get host() {
|
|
return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
|
|
}
|
|
|
|
set host(value) {
|
|
value = String(value);
|
|
const url = new URL(`http://${value}`);
|
|
parts.get(this).hostname = url.hostname;
|
|
parts.get(this).port = url.port;
|
|
}
|
|
|
|
get hostname() {
|
|
return parts.get(this).hostname;
|
|
}
|
|
|
|
set hostname(value) {
|
|
value = String(value);
|
|
try {
|
|
const isSpecial = specialSchemes.includes(parts.get(this).protocol);
|
|
parts.get(this).hostname = encodeHostname(value, isSpecial);
|
|
} catch {}
|
|
}
|
|
|
|
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}`;
|
|
}
|
|
|
|
set href(value) {
|
|
value = String(value);
|
|
if (value !== this.href) {
|
|
const url = new URL(value);
|
|
parts.set(this, { ...parts.get(url) });
|
|
this.#updateSearchParams();
|
|
}
|
|
}
|
|
|
|
get origin() {
|
|
if (this.host) {
|
|
return `${this.protocol}//${this.host}`;
|
|
}
|
|
return "null";
|
|
}
|
|
|
|
get password() {
|
|
return parts.get(this).password;
|
|
}
|
|
|
|
set password(value) {
|
|
value = String(value);
|
|
parts.get(this).password = encodeUserinfo(value);
|
|
}
|
|
|
|
get pathname() {
|
|
let path = parts.get(this).path;
|
|
if (specialSchemes.includes(parts.get(this).protocol)) {
|
|
if (path.charAt(0) != "/") {
|
|
path = `/${path}`;
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
set pathname(value) {
|
|
parts.get(this).path = encodePathname(String(value));
|
|
}
|
|
|
|
get port() {
|
|
const port = parts.get(this).port;
|
|
if (schemePorts[parts.get(this).protocol] === port) {
|
|
return "";
|
|
}
|
|
|
|
return port;
|
|
}
|
|
|
|
set port(value) {
|
|
if (!isValidPort(value)) {
|
|
return;
|
|
}
|
|
parts.get(this).port = value.toString();
|
|
}
|
|
|
|
get 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);
|
|
}
|
|
}
|
|
|
|
get search() {
|
|
return parts.get(this).query;
|
|
}
|
|
|
|
set search(value) {
|
|
value = String(value);
|
|
const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`;
|
|
parts.get(this).query = encodeSearch(query);
|
|
this.#updateSearchParams();
|
|
}
|
|
|
|
get username() {
|
|
return parts.get(this).username;
|
|
}
|
|
|
|
set username(value) {
|
|
value = String(value);
|
|
parts.get(this).username = encodeUserinfo(value);
|
|
}
|
|
|
|
get searchParams() {
|
|
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;
|
|
}
|
|
|
|
toJSON() {
|
|
return this.href;
|
|
}
|
|
|
|
// TODO(kevinkassimo): implement MediaSource version in the future.
|
|
static createObjectURL(blob) {
|
|
const origin = "http://deno-opaque-origin";
|
|
const key = `blob:${origin}/${generateUUID()}`;
|
|
blobURLMap.set(key, blob);
|
|
return key;
|
|
}
|
|
|
|
static revokeObjectURL(url) {
|
|
let urlObject;
|
|
try {
|
|
urlObject = new URL(url);
|
|
} catch {
|
|
throw new TypeError("Provided URL string is not valid");
|
|
}
|
|
if (urlObject.protocol !== "blob:") {
|
|
return;
|
|
}
|
|
// Origin match check seems irrelevant for now, unless we implement
|
|
// persisten storage for per globalThis.location.origin at some point.
|
|
blobURLMap.delete(url);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// deno-fmt-ignore
|
|
return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || 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);
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
function encodeChar(c) {
|
|
return [...encoder.encode(c)]
|
|
.map((n) => `%${n.toString(16)}`)
|
|
.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].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join("");
|
|
}
|
|
|
|
function encodeSearch(s) {
|
|
return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join("");
|
|
}
|
|
|
|
function encodeHash(s) {
|
|
return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join(
|
|
"",
|
|
);
|
|
}
|
|
|
|
window.__bootstrap.url = {
|
|
URL,
|
|
URLSearchParams,
|
|
blobURLMap,
|
|
};
|
|
})(this);
|