summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/whatwg-url.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/whatwg-url.ts')
-rw-r--r--packages/taler-util/src/whatwg-url.ts2126
1 files changed, 2126 insertions, 0 deletions
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
new file mode 100644
index 000000000..13abf5397
--- /dev/null
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -0,0 +1,2126 @@
+/*
+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 {
+ //Include URL type for "url" and "base" params.
+ constructor(url: string | URL, base?: string | URL) {
+ let parsedBase = null;
+ if (base !== undefined) {
+ if (base instanceof URL) {
+ base = base.href;
+ }
+ parsedBase = basicURLParse(base);
+ if (parsedBase === null) {
+ throw new TypeError(`Invalid base URL: ${base}`);
+ }
+ }
+
+ if (url instanceof URL) {
+ url = url.href;
+ }
+ 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;
+}