wallet-core/packages/taler-util/src/whatwg-url.ts

2120 lines
48 KiB
TypeScript

/*
The MIT License (MIT)
Copyright (c) Sebastian Mayr
Copyright (c) 2022 Taler Systems S.A.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Vendored with modifications (TypeScript etc.) from https://github.com/jsdom/whatwg-url
const utf8Encoder = new TextEncoder();
const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
function utf8Encode(string: string | undefined) {
return utf8Encoder.encode(string);
}
function utf8DecodeWithoutBOM(
bytes: DataView | ArrayBuffer | null | undefined,
) {
return utf8Decoder.decode(bytes);
}
// https://url.spec.whatwg.org/#concept-urlencoded-parser
function parseUrlencoded(input: Uint8Array) {
const sequences = strictlySplitByteSequence(input, p("&"));
const output = [];
for (const bytes of sequences) {
if (bytes.length === 0) {
continue;
}
let name, value;
const indexOfEqual = bytes.indexOf(p("=")!);
if (indexOfEqual >= 0) {
name = bytes.slice(0, indexOfEqual);
value = bytes.slice(indexOfEqual + 1);
} else {
name = bytes;
value = new Uint8Array(0);
}
name = replaceByteInByteSequence(name, 0x2b, 0x20);
value = replaceByteInByteSequence(value, 0x2b, 0x20);
const nameString = utf8DecodeWithoutBOM(percentDecodeBytes(name));
const valueString = utf8DecodeWithoutBOM(percentDecodeBytes(value));
output.push([nameString, valueString]);
}
return output;
}
// https://url.spec.whatwg.org/#concept-urlencoded-string-parser
function parseUrlencodedString(input: string | undefined) {
return parseUrlencoded(utf8Encode(input));
}
// https://url.spec.whatwg.org/#concept-urlencoded-serializer
function serializeUrlencoded(tuples: any[], encodingOverride = undefined) {
let encoding = "utf-8";
if (encodingOverride !== undefined) {
// TODO "get the output encoding", i.e. handle encoding labels vs. names.
encoding = encodingOverride;
}
let output = "";
for (const [i, tuple] of tuples.entries()) {
// TODO: handle encoding override
const name = utf8PercentEncodeString(
tuple[0],
isURLEncodedPercentEncode,
true,
);
let value = tuple[1];
if (tuple.length > 2 && tuple[2] !== undefined) {
if (tuple[2] === "hidden" && name === "_charset_") {
value = encoding;
} else if (tuple[2] === "file") {
// value is a File object
value = value.name;
}
}
value = utf8PercentEncodeString(value, isURLEncodedPercentEncode, true);
if (i !== 0) {
output += "&";
}
output += `${name}=${value}`;
}
return output;
}
function strictlySplitByteSequence(buf: Uint8Array, cp: any) {
const list = [];
let last = 0;
let i = buf.indexOf(cp);
while (i >= 0) {
list.push(buf.slice(last, i));
last = i + 1;
i = buf.indexOf(cp, last);
}
if (last !== buf.length) {
list.push(buf.slice(last));
}
return list;
}
function replaceByteInByteSequence(buf: Uint8Array, from: number, to: number) {
let i = buf.indexOf(from);
while (i >= 0) {
buf[i] = to;
i = buf.indexOf(from, i + 1);
}
return buf;
}
function p(char: string) {
return char.codePointAt(0);
}
// https://url.spec.whatwg.org/#percent-encode
function percentEncode(c: number) {
let hex = c.toString(16).toUpperCase();
if (hex.length === 1) {
hex = `0${hex}`;
}
return `%${hex}`;
}
// https://url.spec.whatwg.org/#percent-decode
function percentDecodeBytes(input: Uint8Array) {
const output = new Uint8Array(input.byteLength);
let outputIndex = 0;
for (let i = 0; i < input.byteLength; ++i) {
const byte = input[i];
if (byte !== 0x25) {
output[outputIndex++] = byte;
} else if (
byte === 0x25 &&
(!isASCIIHex(input[i + 1]) || !isASCIIHex(input[i + 2]))
) {
output[outputIndex++] = byte;
} else {
const bytePoint = parseInt(
String.fromCodePoint(input[i + 1], input[i + 2]),
16,
);
output[outputIndex++] = bytePoint;
i += 2;
}
}
return output.slice(0, outputIndex);
}
// https://url.spec.whatwg.org/#string-percent-decode
function percentDecodeString(input: string) {
const bytes = utf8Encode(input);
return percentDecodeBytes(bytes);
}
// https://url.spec.whatwg.org/#c0-control-percent-encode-set
function isC0ControlPercentEncode(c: number) {
return c <= 0x1f || c > 0x7e;
}
// https://url.spec.whatwg.org/#fragment-percent-encode-set
const extraFragmentPercentEncodeSet = new Set([
p(" "),
p('"'),
p("<"),
p(">"),
p("`"),
]);
function isFragmentPercentEncode(c: number) {
return isC0ControlPercentEncode(c) || extraFragmentPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#query-percent-encode-set
const extraQueryPercentEncodeSet = new Set([
p(" "),
p('"'),
p("#"),
p("<"),
p(">"),
]);
function isQueryPercentEncode(c: number) {
return isC0ControlPercentEncode(c) || extraQueryPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#special-query-percent-encode-set
function isSpecialQueryPercentEncode(c: number) {
return isQueryPercentEncode(c) || c === p("'");
}
// https://url.spec.whatwg.org/#path-percent-encode-set
const extraPathPercentEncodeSet = new Set([p("?"), p("`"), p("{"), p("}")]);
function isPathPercentEncode(c: number) {
return isQueryPercentEncode(c) || extraPathPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#userinfo-percent-encode-set
const extraUserinfoPercentEncodeSet = new Set([
p("/"),
p(":"),
p(";"),
p("="),
p("@"),
p("["),
p("\\"),
p("]"),
p("^"),
p("|"),
]);
function isUserinfoPercentEncode(c: number) {
return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#component-percent-encode-set
const extraComponentPercentEncodeSet = new Set([
p("$"),
p("%"),
p("&"),
p("+"),
p(","),
]);
function isComponentPercentEncode(c: number) {
return isUserinfoPercentEncode(c) || extraComponentPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
const extraURLEncodedPercentEncodeSet = new Set([
p("!"),
p("'"),
p("("),
p(")"),
p("~"),
]);
function isURLEncodedPercentEncode(c: number) {
return isComponentPercentEncode(c) || extraURLEncodedPercentEncodeSet.has(c);
}
// https://url.spec.whatwg.org/#code-point-percent-encode-after-encoding
// https://url.spec.whatwg.org/#utf-8-percent-encode
// Assuming encoding is always utf-8 allows us to trim one of the logic branches. TODO: support encoding.
// The "-Internal" variant here has code points as JS strings. The external version used by other files has code points
// as JS numbers, like the rest of the codebase.
function utf8PercentEncodeCodePointInternal(
codePoint: string,
percentEncodePredicate: (arg0: number) => any,
) {
const bytes = utf8Encode(codePoint);
let output = "";
for (const byte of bytes) {
// Our percentEncodePredicate operates on bytes, not code points, so this is slightly different from the spec.
if (!percentEncodePredicate(byte)) {
output += String.fromCharCode(byte);
} else {
output += percentEncode(byte);
}
}
return output;
}
function utf8PercentEncodeCodePoint(
codePoint: number,
percentEncodePredicate: (arg0: number) => any,
) {
return utf8PercentEncodeCodePointInternal(
String.fromCodePoint(codePoint),
percentEncodePredicate,
);
}
// https://url.spec.whatwg.org/#string-percent-encode-after-encoding
// https://url.spec.whatwg.org/#string-utf-8-percent-encode
function utf8PercentEncodeString(
input: string,
percentEncodePredicate: {
(c: number): boolean;
(c: number): boolean;
(arg0: number): any;
},
spaceAsPlus = false,
) {
let output = "";
for (const codePoint of input) {
if (spaceAsPlus && codePoint === " ") {
output += "+";
} else {
output += utf8PercentEncodeCodePointInternal(
codePoint,
percentEncodePredicate,
);
}
}
return output;
}
// Note that we take code points as JS numbers, not JS strings.
function isASCIIDigit(c: number) {
return c >= 0x30 && c <= 0x39;
}
function isASCIIAlpha(c: number) {
return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
}
function isASCIIAlphanumeric(c: number) {
return isASCIIAlpha(c) || isASCIIDigit(c);
}
function isASCIIHex(c: number) {
return (
isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66)
);
}
export class URLSearchParamsImpl {
_list: any[];
_url: any;
constructor(init: any, { doNotStripQMark = false }: any = {}) {
this._list = [];
this._url = null;
if (!doNotStripQMark && typeof init === "string" && init[0] === "?") {
init = init.slice(1);
}
if (Array.isArray(init)) {
for (const pair of init) {
if (pair.length !== 2) {
throw new TypeError(
"Failed to construct 'URLSearchParams': parameter 1 sequence's element does not " +
"contain exactly two elements.",
);
}
this._list.push([pair[0], pair[1]]);
}
} else if (
typeof init === "object" &&
Object.getPrototypeOf(init) === null
) {
for (const name of Object.keys(init)) {
const value = init[name];
this._list.push([name, value]);
}
} else {
this._list = parseUrlencodedString(init);
}
}
_updateSteps() {
if (this._url !== null) {
let query: string | null = serializeUrlencoded(this._list);
if (query === "") {
query = null;
}
this._url._url.query = query;
}
}
append(name: string, value: string) {
this._list.push([name, value]);
this._updateSteps();
}
delete(name: string) {
let i = 0;
while (i < this._list.length) {
if (this._list[i][0] === name) {
this._list.splice(i, 1);
} else {
i++;
}
}
this._updateSteps();
}
get(name: string) {
for (const tuple of this._list) {
if (tuple[0] === name) {
return tuple[1];
}
}
return null;
}
getAll(name: string) {
const output = [];
for (const tuple of this._list) {
if (tuple[0] === name) {
output.push(tuple[1]);
}
}
return output;
}
forEach(
callbackfn: (
value: string,
key: string,
parent: URLSearchParamsImpl,
) => void,
thisArg?: any,
): void {
for (const tuple of this._list) {
callbackfn.call(thisArg, tuple[1], tuple[0], this);
}
}
has(name: string) {
for (const tuple of this._list) {
if (tuple[0] === name) {
return true;
}
}
return false;
}
set(name: string, value: string) {
let found = false;
let i = 0;
while (i < this._list.length) {
if (this._list[i][0] === name) {
if (found) {
this._list.splice(i, 1);
} else {
found = true;
this._list[i][1] = value;
i++;
}
} else {
i++;
}
}
if (!found) {
this._list.push([name, value]);
}
this._updateSteps();
}
sort() {
this._list.sort((a, b) => {
if (a[0] < b[0]) {
return -1;
}
if (a[0] > b[0]) {
return 1;
}
return 0;
});
this._updateSteps();
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
}
toString() {
return serializeUrlencoded(this._list);
}
}
const specialSchemes = {
ftp: 21,
file: null,
http: 80,
https: 443,
ws: 80,
wss: 443,
} as { [x: string]: number | null };
const failure = Symbol("failure");
function countSymbols(str: any) {
return [...str].length;
}
function at(input: any, idx: any) {
const c = input[idx];
return isNaN(c) ? undefined : String.fromCodePoint(c);
}
function isSingleDot(buffer: string) {
return buffer === "." || buffer.toLowerCase() === "%2e";
}
function isDoubleDot(buffer: string) {
buffer = buffer.toLowerCase();
return (
buffer === ".." ||
buffer === "%2e." ||
buffer === ".%2e" ||
buffer === "%2e%2e"
);
}
function isWindowsDriveLetterCodePoints(cp1: number, cp2: number) {
return isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
}
function isWindowsDriveLetterString(string: string) {
return (
string.length === 2 &&
isASCIIAlpha(string.codePointAt(0)!) &&
(string[1] === ":" || string[1] === "|")
);
}
function isNormalizedWindowsDriveLetterString(string: string) {
return (
string.length === 2 &&
isASCIIAlpha(string.codePointAt(0)!) &&
string[1] === ":"
);
}
function containsForbiddenHostCodePoint(string: string) {
return (
string.search(
/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|<|>|\?|@|\[|\\|\]|\^|\|/u,
) !== -1
);
}
function containsForbiddenDomainCodePoint(string: string) {
return (
containsForbiddenHostCodePoint(string) ||
string.search(/[\u0000-\u001F]|%|\u007F/u) !== -1
);
}
function isSpecialScheme(scheme: string) {
return specialSchemes[scheme] !== undefined;
}
function isSpecial(url: any) {
return isSpecialScheme(url.scheme);
}
function isNotSpecial(url: UrlObj) {
return !isSpecialScheme(url.scheme);
}
function defaultPort(scheme: string) {
return specialSchemes[scheme];
}
function parseIPv4Number(input: string) {
if (input === "") {
return failure;
}
let R = 10;
if (
input.length >= 2 &&
input.charAt(0) === "0" &&
input.charAt(1).toLowerCase() === "x"
) {
input = input.substring(2);
R = 16;
} else if (input.length >= 2 && input.charAt(0) === "0") {
input = input.substring(1);
R = 8;
}
if (input === "") {
return 0;
}
let regex = /[^0-7]/u;
if (R === 10) {
regex = /[^0-9]/u;
}
if (R === 16) {
regex = /[^0-9A-Fa-f]/u;
}
if (regex.test(input)) {
return failure;
}
return parseInt(input, R);
}
function parseIPv4(input: string) {
const parts = input.split(".");
if (parts[parts.length - 1] === "") {
if (parts.length > 1) {
parts.pop();
}
}
if (parts.length > 4) {
return failure;
}
const numbers = [];
for (const part of parts) {
const n = parseIPv4Number(part);
if (n === failure) {
return failure;
}
numbers.push(n);
}
for (let i = 0; i < numbers.length - 1; ++i) {
if (numbers[i] > 255) {
return failure;
}
}
if (numbers[numbers.length - 1] >= 256 ** (5 - numbers.length)) {
return failure;
}
let ipv4 = numbers.pop();
let counter = 0;
for (const n of numbers) {
ipv4! += n * 256 ** (3 - counter);
++counter;
}
return ipv4;
}
function serializeIPv4(address: number) {
let output = "";
let n = address;
for (let i = 1; i <= 4; ++i) {
output = String(n % 256) + output;
if (i !== 4) {
output = `.${output}`;
}
n = Math.floor(n / 256);
}
return output;
}
function parseIPv6(inputArg: string) {
const address = [0, 0, 0, 0, 0, 0, 0, 0];
let pieceIndex = 0;
let compress = null;
let pointer = 0;
const input = Array.from(inputArg, (c) => c.codePointAt(0));
if (input[pointer] === p(":")) {
if (input[pointer + 1] !== p(":")) {
return failure;
}
pointer += 2;
++pieceIndex;
compress = pieceIndex;
}
while (pointer < input.length) {
if (pieceIndex === 8) {
return failure;
}
if (input[pointer] === p(":")) {
if (compress !== null) {
return failure;
}
++pointer;
++pieceIndex;
compress = pieceIndex;
continue;
}
let value = 0;
let length = 0;
while (length < 4 && isASCIIHex(input[pointer]!)) {
value = value * 0x10 + parseInt(at(input, pointer)!, 16);
++pointer;
++length;
}
if (input[pointer] === p(".")) {
if (length === 0) {
return failure;
}
pointer -= length;
if (pieceIndex > 6) {
return failure;
}
let numbersSeen = 0;
while (input[pointer] !== undefined) {
let ipv4Piece = null;
if (numbersSeen > 0) {
if (input[pointer] === p(".") && numbersSeen < 4) {
++pointer;
} else {
return failure;
}
}
if (!isASCIIDigit(input[pointer]!)) {
return failure;
}
while (isASCIIDigit(input[pointer]!)) {
const number = parseInt(at(input, pointer)!);
if (ipv4Piece === null) {
ipv4Piece = number;
} else if (ipv4Piece === 0) {
return failure;
} else {
ipv4Piece = ipv4Piece * 10 + number;
}
if (ipv4Piece > 255) {
return failure;
}
++pointer;
}
address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece!;
++numbersSeen;
if (numbersSeen === 2 || numbersSeen === 4) {
++pieceIndex;
}
}
if (numbersSeen !== 4) {
return failure;
}
break;
} else if (input[pointer] === p(":")) {
++pointer;
if (input[pointer] === undefined) {
return failure;
}
} else if (input[pointer] !== undefined) {
return failure;
}
address[pieceIndex] = value;
++pieceIndex;
}
if (compress !== null) {
let swaps = pieceIndex - compress;
pieceIndex = 7;
while (pieceIndex !== 0 && swaps > 0) {
const temp = address[compress + swaps - 1];
address[compress + swaps - 1] = address[pieceIndex];
address[pieceIndex] = temp;
--pieceIndex;
--swaps;
}
} else if (compress === null && pieceIndex !== 8) {
return failure;
}
return address;
}
function serializeIPv6(address: any[]) {
let output = "";
const compress = findLongestZeroSequence(address);
let ignore0 = false;
for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) {
if (ignore0 && address[pieceIndex] === 0) {
continue;
} else if (ignore0) {
ignore0 = false;
}
if (compress === pieceIndex) {
const separator = pieceIndex === 0 ? "::" : ":";
output += separator;
ignore0 = true;
continue;
}
output += address[pieceIndex].toString(16);
if (pieceIndex !== 7) {
output += ":";
}
}
return output;
}
function parseHost(input: string, isNotSpecialArg = false) {
if (input[0] === "[") {
if (input[input.length - 1] !== "]") {
return failure;
}
return parseIPv6(input.substring(1, input.length - 1));
}
if (isNotSpecialArg) {
return parseOpaqueHost(input);
}
const domain = utf8DecodeWithoutBOM(percentDecodeString(input));
const asciiDomain = domainToASCII(domain);
if (asciiDomain === failure) {
return failure;
}
if (containsForbiddenDomainCodePoint(asciiDomain)) {
return failure;
}
if (endsInANumber(asciiDomain)) {
return parseIPv4(asciiDomain);
}
return asciiDomain;
}
function endsInANumber(input: string) {
const parts = input.split(".");
if (parts[parts.length - 1] === "") {
if (parts.length === 1) {
return false;
}
parts.pop();
}
const last = parts[parts.length - 1];
if (parseIPv4Number(last) !== failure) {
return true;
}
if (/^[0-9]+$/u.test(last)) {
return true;
}
return false;
}
function parseOpaqueHost(input: string) {
if (containsForbiddenHostCodePoint(input)) {
return failure;
}
return utf8PercentEncodeString(input, isC0ControlPercentEncode);
}
function findLongestZeroSequence(arr: number[]) {
let maxIdx = null;
let maxLen = 1; // only find elements > 1
let currStart = null;
let currLen = 0;
for (let i = 0; i < arr.length; ++i) {
if (arr[i] !== 0) {
if (currLen > maxLen) {
maxIdx = currStart;
maxLen = currLen;
}
currStart = null;
currLen = 0;
} else {
if (currStart === null) {
currStart = i;
}
++currLen;
}
}
// if trailing zeros
if (currLen > maxLen) {
return currStart;
}
return maxIdx;
}
function serializeHost(host: number | number[] | string) {
if (typeof host === "number") {
return serializeIPv4(host);
}
// IPv6 serializer
if (host instanceof Array) {
return `[${serializeIPv6(host)}]`;
}
return host;
}
import { punycode } from "./punycode.js";
function domainToASCII(domain: string, beStrict = false) {
// const result = tr46.toASCII(domain, {
// checkBidi: true,
// checkHyphens: false,
// checkJoiners: true,
// useSTD3ASCIIRules: beStrict,
// verifyDNSLength: beStrict,
// });
let result;
try {
result = punycode.toASCII(domain);
} catch (e) {
return failure;
}
if (result === null || result === "") {
return failure;
}
return result;
}
function trimControlChars(url: string) {
return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/gu, "");
}
function trimTabAndNewline(url: string) {
return url.replace(/\u0009|\u000A|\u000D/gu, "");
}
function shortenPath(url: UrlObj) {
const { path } = url;
if (path.length === 0) {
return;
}
if (
url.scheme === "file" &&
path.length === 1 &&
isNormalizedWindowsDriveLetter(path[0])
) {
return;
}
path.pop();
}
function includesCredentials(url: UrlObj) {
return url.username !== "" || url.password !== "";
}
function cannotHaveAUsernamePasswordPort(url: UrlObj) {
return url.host === null || url.host === "" || url.scheme === "file";
}
function hasAnOpaquePath(url: UrlObj) {
return typeof url.path === "string";
}
function isNormalizedWindowsDriveLetter(string: string) {
return /^[A-Za-z]:$/u.test(string);
}
export interface UrlObj {
scheme: string;
username: string;
password: string;
host: string | number[] | number | null | undefined;
port: number | null;
path: string[];
query: any;
fragment: any;
}
class URLStateMachine {
pointer: number;
input: number[];
base: any;
encodingOverride: string;
url: UrlObj;
state: string;
stateOverride: string;
failure: boolean;
parseError: boolean;
buffer: string;
atFlag: boolean;
arrFlag: boolean;
passwordTokenSeenFlag: boolean;
constructor(
input: string,
base: any,
encodingOverride: string,
url: UrlObj,
stateOverride: string,
) {
this.pointer = 0;
this.base = base || null;
this.encodingOverride = encodingOverride || "utf-8";
this.url = url;
this.failure = false;
this.parseError = false;
if (!this.url) {
this.url = {
scheme: "",
username: "",
password: "",
host: null,
port: null,
path: [],
query: null,
fragment: null,
};
const res = trimControlChars(input);
if (res !== input) {
this.parseError = true;
}
input = res;
}
const res = trimTabAndNewline(input);
if (res !== input) {
this.parseError = true;
}
input = res;
this.state = stateOverride || "scheme start";
this.buffer = "";
this.atFlag = false;
this.arrFlag = false;
this.passwordTokenSeenFlag = false;
this.input = Array.from(input, (c) => c.codePointAt(0)!);
for (; this.pointer <= this.input.length; ++this.pointer) {
const c = this.input[this.pointer];
const cStr = isNaN(c) ? undefined : String.fromCodePoint(c);
// exec state machine
const ret = this.table[`parse ${this.state}`].call(this, c, cStr!);
if (!ret) {
break; // terminate algorithm
} else if (ret === failure) {
this.failure = true;
break;
}
}
}
table = {
"parse scheme start": this.parseSchemeStart,
"parse scheme": this.parseScheme,
"parse no scheme": this.parseNoScheme,
"parse special relative or authority": this.parseSpecialRelativeOrAuthority,
"parse path or authority": this.parsePathOrAuthority,
"parse relative": this.parseRelative,
"parse relative slash": this.parseRelativeSlash,
"parse special authority slashes": this.parseSpecialAuthoritySlashes,
"parse special authority ignore slashes":
this.parseSpecialAuthorityIgnoreSlashes,
"parse authority": this.parseAuthority,
"parse host": this.parseHostName,
"parse hostname": this.parseHostName /* intentional duplication */,
"parse port": this.parsePort,
"parse file": this.parseFile,
"parse file slash": this.parseFileSlash,
"parse file host": this.parseFileHost,
"parse path start": this.parsePathStart,
"parse path": this.parsePath,
"parse opaque path": this.parseOpaquePath,
"parse query": this.parseQuery,
"parse fragment": this.parseFragment,
} as { [x: string]: (c: number, cStr: string) => any };
parseSchemeStart(c: number, cStr: string) {
if (isASCIIAlpha(c)) {
this.buffer += cStr.toLowerCase();
this.state = "scheme";
} else if (!this.stateOverride) {
this.state = "no scheme";
--this.pointer;
} else {
this.parseError = true;
return failure;
}
return true;
}
parseScheme(c: number, cStr: string) {
if (
isASCIIAlphanumeric(c) ||
c === p("+") ||
c === p("-") ||
c === p(".")
) {
this.buffer += cStr.toLowerCase();
} else if (c === p(":")) {
if (this.stateOverride) {
if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) {
return false;
}
if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) {
return false;
}
if (
(includesCredentials(this.url) || this.url.port !== null) &&
this.buffer === "file"
) {
return false;
}
if (this.url.scheme === "file" && this.url.host === "") {
return false;
}
}
this.url.scheme = this.buffer;
if (this.stateOverride) {
if (this.url.port === defaultPort(this.url.scheme)) {
this.url.port = null;
}
return false;
}
this.buffer = "";
if (this.url.scheme === "file") {
if (
this.input[this.pointer + 1] !== p("/") ||
this.input[this.pointer + 2] !== p("/")
) {
this.parseError = true;
}
this.state = "file";
} else if (
isSpecial(this.url) &&
this.base !== null &&
this.base.scheme === this.url.scheme
) {
this.state = "special relative or authority";
} else if (isSpecial(this.url)) {
this.state = "special authority slashes";
} else if (this.input[this.pointer + 1] === p("/")) {
this.state = "path or authority";
++this.pointer;
} else {
this.url.path = [""];
this.state = "opaque path";
}
} else if (!this.stateOverride) {
this.buffer = "";
this.state = "no scheme";
this.pointer = -1;
} else {
this.parseError = true;
return failure;
}
return true;
}
parseNoScheme(c: number) {
if (this.base === null || (hasAnOpaquePath(this.base) && c !== p("#"))) {
return failure;
} else if (hasAnOpaquePath(this.base) && c === p("#")) {
this.url.scheme = this.base.scheme;
this.url.path = this.base.path;
this.url.query = this.base.query;
this.url.fragment = "";
this.state = "fragment";
} else if (this.base.scheme === "file") {
this.state = "file";
--this.pointer;
} else {
this.state = "relative";
--this.pointer;
}
return true;
}
parseSpecialRelativeOrAuthority(c: number) {
if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
this.state = "special authority ignore slashes";
++this.pointer;
} else {
this.parseError = true;
this.state = "relative";
--this.pointer;
}
return true;
}
parsePathOrAuthority(c: number) {
if (c === p("/")) {
this.state = "authority";
} else {
this.state = "path";
--this.pointer;
}
return true;
}
parseRelative(c: number) {
this.url.scheme = this.base.scheme;
if (c === p("/")) {
this.state = "relative slash";
} else if (isSpecial(this.url) && c === p("\\")) {
this.parseError = true;
this.state = "relative slash";
} else {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.url.path = this.base.path.slice();
this.url.query = this.base.query;
if (c === p("?")) {
this.url.query = "";
this.state = "query";
} else if (c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
} else if (!isNaN(c)) {
this.url.query = null;
this.url.path.pop();
this.state = "path";
--this.pointer;
}
}
return true;
}
parseRelativeSlash(c: number) {
if (isSpecial(this.url) && (c === p("/") || c === p("\\"))) {
if (c === p("\\")) {
this.parseError = true;
}
this.state = "special authority ignore slashes";
} else if (c === p("/")) {
this.state = "authority";
} else {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.state = "path";
--this.pointer;
}
return true;
}
parseSpecialAuthoritySlashes(c: number) {
if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
this.state = "special authority ignore slashes";
++this.pointer;
} else {
this.parseError = true;
this.state = "special authority ignore slashes";
--this.pointer;
}
return true;
}
parseSpecialAuthorityIgnoreSlashes(c: number) {
if (c !== p("/") && c !== p("\\")) {
this.state = "authority";
--this.pointer;
} else {
this.parseError = true;
}
return true;
}
parseAuthority(c: number, cStr: string) {
if (c === p("@")) {
this.parseError = true;
if (this.atFlag) {
this.buffer = `%40${this.buffer}`;
}
this.atFlag = true;
// careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars
const len = countSymbols(this.buffer);
for (let pointer = 0; pointer < len; ++pointer) {
const codePoint = this.buffer.codePointAt(pointer);
if (codePoint === p(":") && !this.passwordTokenSeenFlag) {
this.passwordTokenSeenFlag = true;
continue;
}
const encodedCodePoints = utf8PercentEncodeCodePoint(
codePoint!,
isUserinfoPercentEncode,
);
if (this.passwordTokenSeenFlag) {
this.url.password += encodedCodePoints;
} else {
this.url.username += encodedCodePoints;
}
}
this.buffer = "";
} else if (
isNaN(c) ||
c === p("/") ||
c === p("?") ||
c === p("#") ||
(isSpecial(this.url) && c === p("\\"))
) {
if (this.atFlag && this.buffer === "") {
this.parseError = true;
return failure;
}
this.pointer -= countSymbols(this.buffer) + 1;
this.buffer = "";
this.state = "host";
} else {
this.buffer += cStr;
}
return true;
}
parseHostName(c: number, cStr: string) {
if (this.stateOverride && this.url.scheme === "file") {
--this.pointer;
this.state = "file host";
} else if (c === p(":") && !this.arrFlag) {
if (this.buffer === "") {
this.parseError = true;
return failure;
}
if (this.stateOverride === "hostname") {
return false;
}
const host = parseHost(this.buffer, isNotSpecial(this.url));
if (host === failure) {
return failure;
}
this.url.host = host;
this.buffer = "";
this.state = "port";
} else if (
isNaN(c) ||
c === p("/") ||
c === p("?") ||
c === p("#") ||
(isSpecial(this.url) && c === p("\\"))
) {
--this.pointer;
if (isSpecial(this.url) && this.buffer === "") {
this.parseError = true;
return failure;
} else if (
this.stateOverride &&
this.buffer === "" &&
(includesCredentials(this.url) || this.url.port !== null)
) {
this.parseError = true;
return false;
}
const host = parseHost(this.buffer, isNotSpecial(this.url));
if (host === failure) {
return failure;
}
this.url.host = host;
this.buffer = "";
this.state = "path start";
if (this.stateOverride) {
return false;
}
} else {
if (c === p("[")) {
this.arrFlag = true;
} else if (c === p("]")) {
this.arrFlag = false;
}
this.buffer += cStr;
}
return true;
}
parsePort(c: number, cStr: any) {
if (isASCIIDigit(c)) {
this.buffer += cStr;
} else if (
isNaN(c) ||
c === p("/") ||
c === p("?") ||
c === p("#") ||
(isSpecial(this.url) && c === p("\\")) ||
this.stateOverride
) {
if (this.buffer !== "") {
const port = parseInt(this.buffer);
if (port > 2 ** 16 - 1) {
this.parseError = true;
return failure;
}
this.url.port = port === defaultPort(this.url.scheme) ? null : port;
this.buffer = "";
}
if (this.stateOverride) {
return false;
}
this.state = "path start";
--this.pointer;
} else {
this.parseError = true;
return failure;
}
return true;
}
parseFile(c: number) {
this.url.scheme = "file";
this.url.host = "";
if (c === p("/") || c === p("\\")) {
if (c === p("\\")) {
this.parseError = true;
}
this.state = "file slash";
} else if (this.base !== null && this.base.scheme === "file") {
this.url.host = this.base.host;
this.url.path = this.base.path.slice();
this.url.query = this.base.query;
if (c === p("?")) {
this.url.query = "";
this.state = "query";
} else if (c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
} else if (!isNaN(c)) {
this.url.query = null;
if (!startsWithWindowsDriveLetter(this.input, this.pointer)) {
shortenPath(this.url);
} else {
this.parseError = true;
this.url.path = [];
}
this.state = "path";
--this.pointer;
}
} else {
this.state = "path";
--this.pointer;
}
return true;
}
parseFileSlash(c: number) {
if (c === p("/") || c === p("\\")) {
if (c === p("\\")) {
this.parseError = true;
}
this.state = "file host";
} else {
if (this.base !== null && this.base.scheme === "file") {
if (
!startsWithWindowsDriveLetter(this.input, this.pointer) &&
isNormalizedWindowsDriveLetterString(this.base.path[0])
) {
this.url.path.push(this.base.path[0]);
}
this.url.host = this.base.host;
}
this.state = "path";
--this.pointer;
}
return true;
}
parseFileHost(c: number, cStr: string) {
if (
isNaN(c) ||
c === p("/") ||
c === p("\\") ||
c === p("?") ||
c === p("#")
) {
--this.pointer;
if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) {
this.parseError = true;
this.state = "path";
} else if (this.buffer === "") {
this.url.host = "";
if (this.stateOverride) {
return false;
}
this.state = "path start";
} else {
let host = parseHost(this.buffer, isNotSpecial(this.url));
if (host === failure) {
return failure;
}
if (host === "localhost") {
host = "";
}
this.url.host = host as any;
if (this.stateOverride) {
return false;
}
this.buffer = "";
this.state = "path start";
}
} else {
this.buffer += cStr;
}
return true;
}
parsePathStart(c: number) {
if (isSpecial(this.url)) {
if (c === p("\\")) {
this.parseError = true;
}
this.state = "path";
if (c !== p("/") && c !== p("\\")) {
--this.pointer;
}
} else if (!this.stateOverride && c === p("?")) {
this.url.query = "";
this.state = "query";
} else if (!this.stateOverride && c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
} else if (c !== undefined) {
this.state = "path";
if (c !== p("/")) {
--this.pointer;
}
} else if (this.stateOverride && this.url.host === null) {
this.url.path.push("");
}
return true;
}
parsePath(c: number) {
if (
isNaN(c) ||
c === p("/") ||
(isSpecial(this.url) && c === p("\\")) ||
(!this.stateOverride && (c === p("?") || c === p("#")))
) {
if (isSpecial(this.url) && c === p("\\")) {
this.parseError = true;
}
if (isDoubleDot(this.buffer)) {
shortenPath(this.url);
if (c !== p("/") && !(isSpecial(this.url) && c === p("\\"))) {
this.url.path.push("");
}
} else if (
isSingleDot(this.buffer) &&
c !== p("/") &&
!(isSpecial(this.url) && c === p("\\"))
) {
this.url.path.push("");
} else if (!isSingleDot(this.buffer)) {
if (
this.url.scheme === "file" &&
this.url.path.length === 0 &&
isWindowsDriveLetterString(this.buffer)
) {
this.buffer = `${this.buffer[0]}:`;
}
this.url.path.push(this.buffer);
}
this.buffer = "";
if (c === p("?")) {
this.url.query = "";
this.state = "query";
}
if (c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
}
} else {
// TODO: If c is not a URL code point and not "%", parse error.
if (
c === p("%") &&
(!isASCIIHex(this.input[this.pointer + 1]) ||
!isASCIIHex(this.input[this.pointer + 2]))
) {
this.parseError = true;
}
this.buffer += utf8PercentEncodeCodePoint(c, isPathPercentEncode);
}
return true;
}
parseOpaquePath(c: number) {
if (c === p("?")) {
this.url.query = "";
this.state = "query";
} else if (c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
} else {
// TODO: Add: not a URL code point
if (!isNaN(c) && c !== p("%")) {
this.parseError = true;
}
if (
c === p("%") &&
(!isASCIIHex(this.input[this.pointer + 1]) ||
!isASCIIHex(this.input[this.pointer + 2]))
) {
this.parseError = true;
}
if (!isNaN(c)) {
// @ts-ignore
this.url.path += utf8PercentEncodeCodePoint(
c,
isC0ControlPercentEncode,
);
}
}
return true;
}
parseQuery(c: number, cStr: string) {
if (
!isSpecial(this.url) ||
this.url.scheme === "ws" ||
this.url.scheme === "wss"
) {
this.encodingOverride = "utf-8";
}
if ((!this.stateOverride && c === p("#")) || isNaN(c)) {
const queryPercentEncodePredicate = isSpecial(this.url)
? isSpecialQueryPercentEncode
: isQueryPercentEncode;
this.url.query += utf8PercentEncodeString(
this.buffer,
queryPercentEncodePredicate,
);
this.buffer = "";
if (c === p("#")) {
this.url.fragment = "";
this.state = "fragment";
}
} else if (!isNaN(c)) {
// TODO: If c is not a URL code point and not "%", parse error.
if (
c === p("%") &&
(!isASCIIHex(this.input[this.pointer + 1]) ||
!isASCIIHex(this.input[this.pointer + 2]))
) {
this.parseError = true;
}
this.buffer += cStr;
}
return true;
}
parseFragment(c: number) {
if (!isNaN(c)) {
// TODO: If c is not a URL code point and not "%", parse error.
if (
c === p("%") &&
(!isASCIIHex(this.input[this.pointer + 1]) ||
!isASCIIHex(this.input[this.pointer + 2]))
) {
this.parseError = true;
}
this.url.fragment += utf8PercentEncodeCodePoint(
c,
isFragmentPercentEncode,
);
}
return true;
}
}
const fileOtherwiseCodePoints = new Set([p("/"), p("\\"), p("?"), p("#")]);
function startsWithWindowsDriveLetter(input: number[], pointer: number) {
const length = input.length - pointer;
return (
length >= 2 &&
isWindowsDriveLetterCodePoints(input[pointer], input[pointer + 1]) &&
(length === 2 || fileOtherwiseCodePoints.has(input[pointer + 2]))
);
}
function serializeURL(url: any, excludeFragment?: boolean) {
let output = `${url.scheme}:`;
if (url.host !== null) {
output += "//";
if (url.username !== "" || url.password !== "") {
output += url.username;
if (url.password !== "") {
output += `:${url.password}`;
}
output += "@";
}
output += serializeHost(url.host);
if (url.port !== null) {
output += `:${url.port}`;
}
}
if (
url.host === null &&
!hasAnOpaquePath(url) &&
url.path.length > 1 &&
url.path[0] === ""
) {
output += "/.";
}
output += serializePath(url);
if (url.query !== null) {
output += `?${url.query}`;
}
if (!excludeFragment && url.fragment !== null) {
output += `#${url.fragment}`;
}
return output;
}
function serializeOrigin(tuple: {
scheme: string;
port: number;
host: number | number[] | string;
}) {
let result = `${tuple.scheme}://`;
result += serializeHost(tuple.host);
if (tuple.port !== null) {
result += `:${tuple.port}`;
}
return result;
}
function serializePath(url: UrlObj): string {
if (typeof url.path === "string") {
return url.path;
}
let output = "";
for (const segment of url.path) {
output += `/${segment}`;
}
return output;
}
function serializeURLOrigin(url: any): any {
// https://url.spec.whatwg.org/#concept-url-origin
switch (url.scheme) {
case "blob":
try {
return serializeURLOrigin(parseURL(serializePath(url)));
} catch (e) {
// serializing an opaque origin returns "null"
return "null";
}
case "ftp":
case "http":
case "https":
case "ws":
case "wss":
return serializeOrigin({
scheme: url.scheme,
host: url.host,
port: url.port,
});
case "file":
// The spec says:
// > Unfortunate as it is, this is left as an exercise to the reader. When in doubt, return a new opaque origin.
// Browsers tested so far:
// - Chrome says "file://", but treats file: URLs as cross-origin for most (all?) purposes; see e.g.
// https://bugs.chromium.org/p/chromium/issues/detail?id=37586
// - Firefox says "null", but treats file: URLs as same-origin sometimes based on directory stuff; see
// https://developer.mozilla.org/en-US/docs/Archive/Misc_top_level/Same-origin_policy_for_file:_URIs
return "null";
default:
// serializing an opaque origin returns "null"
return "null";
}
}
export function basicURLParse(input: string, options?: any) {
if (options === undefined) {
options = {};
}
const usm = new URLStateMachine(
input,
options.baseURL,
options.encodingOverride,
options.url,
options.stateOverride,
);
if (usm.failure) {
return null;
}
return usm.url;
}
function setTheUsername(url: UrlObj, username: string) {
url.username = utf8PercentEncodeString(username, isUserinfoPercentEncode);
}
function setThePassword(url: UrlObj, password: string) {
url.password = utf8PercentEncodeString(password, isUserinfoPercentEncode);
}
function serializeInteger(integer: number) {
return String(integer);
}
function parseURL(
input: any,
options?: { baseURL?: any; encodingOverride?: any },
) {
if (options === undefined) {
options = {};
}
// We don't handle blobs, so this just delegates:
return basicURLParse(input, {
baseURL: options.baseURL,
encodingOverride: options.encodingOverride,
});
}
export class URLImpl {
constructor(url: string, base?: string) {
let parsedBase = null;
if (base !== undefined) {
parsedBase = basicURLParse(base);
if (parsedBase === null) {
throw new TypeError(`Invalid base URL: ${base}`);
}
}
const parsedURL = basicURLParse(url, { baseURL: parsedBase });
if (parsedURL === null) {
throw new TypeError(`Invalid URL: ${url}`);
}
const query = parsedURL.query !== null ? parsedURL.query : "";
this._url = parsedURL;
// We cannot invoke the "new URLSearchParams object" algorithm without going through the constructor, which strips
// question mark by default. Therefore the doNotStripQMark hack is used.
this._query = new URLSearchParamsImpl(query, {
doNotStripQMark: true,
});
this._query._url = this;
}
get href() {
return serializeURL(this._url);
}
set href(v) {
const parsedURL = basicURLParse(v);
if (parsedURL === null) {
throw new TypeError(`Invalid URL: ${v}`);
}
this._url = parsedURL;
this._query._list.splice(0);
const { query } = parsedURL;
if (query !== null) {
this._query._list = parseUrlencodedString(query);
}
}
get origin() {
return serializeURLOrigin(this._url);
}
get protocol() {
return `${this._url.scheme}:`;
}
set protocol(v) {
basicURLParse(`${v}:`, {
url: this._url,
stateOverride: "scheme start",
});
}
get username() {
return this._url.username;
}
set username(v) {
if (cannotHaveAUsernamePasswordPort(this._url)) {
return;
}
setTheUsername(this._url, v);
}
get password() {
return this._url.password;
}
set password(v) {
if (cannotHaveAUsernamePasswordPort(this._url)) {
return;
}
setThePassword(this._url, v);
}
get host() {
const url = this._url;
if (url.host === null) {
return "";
}
if (url.port === null) {
return serializeHost(url.host);
}
return `${serializeHost(url.host)}:${serializeInteger(url.port)}`;
}
set host(v) {
if (hasAnOpaquePath(this._url)) {
return;
}
basicURLParse(v, { url: this._url, stateOverride: "host" });
}
get hostname() {
if (this._url.host === null) {
return "";
}
return serializeHost(this._url.host);
}
set hostname(v) {
if (hasAnOpaquePath(this._url)) {
return;
}
basicURLParse(v, { url: this._url, stateOverride: "hostname" });
}
get port() {
if (this._url.port === null) {
return "";
}
return serializeInteger(this._url.port);
}
set port(v) {
if (cannotHaveAUsernamePasswordPort(this._url)) {
return;
}
if (v === "") {
this._url.port = null;
} else {
basicURLParse(v, { url: this._url, stateOverride: "port" });
}
}
get pathname() {
return serializePath(this._url);
}
set pathname(v: string) {
if (hasAnOpaquePath(this._url)) {
return;
}
this._url.path = [];
basicURLParse(v, { url: this._url, stateOverride: "path start" });
}
get search() {
if (this._url.query === null || this._url.query === "") {
return "";
}
return `?${this._url.query}`;
}
set search(v) {
const url = this._url;
if (v === "") {
url.query = null;
this._query._list = [];
return;
}
const input = v[0] === "?" ? v.substring(1) : v;
url.query = "";
basicURLParse(input, { url, stateOverride: "query" });
this._query._list = parseUrlencodedString(input);
}
get searchParams() {
return this._query;
}
get hash() {
if (this._url.fragment === null || this._url.fragment === "") {
return "";
}
return `#${this._url.fragment}`;
}
set hash(v) {
if (v === "") {
this._url.fragment = null;
return;
}
const input = v[0] === "#" ? v.substring(1) : v;
this._url.fragment = "";
basicURLParse(input, { url: this._url, stateOverride: "fragment" });
}
toJSON() {
return this.href;
}
// FIXME: type!
_url: any;
_query: any;
}