1235 lines
30 KiB
JavaScript
1235 lines
30 KiB
JavaScript
|
"use strict";
|
||
|
const tr46 = require("tr46");
|
||
|
|
||
|
const infra = require("./infra");
|
||
|
const { utf8DecodeWithoutBOM } = require("./encoding");
|
||
|
const { percentDecodeString, utf8PercentEncodeCodePoint, utf8PercentEncodeString, isC0ControlPercentEncode,
|
||
|
isFragmentPercentEncode, isQueryPercentEncode, isSpecialQueryPercentEncode, isPathPercentEncode,
|
||
|
isUserinfoPercentEncode } = require("./percent-encoding");
|
||
|
|
||
|
function p(char) {
|
||
|
return char.codePointAt(0);
|
||
|
}
|
||
|
|
||
|
const specialSchemes = {
|
||
|
ftp: 21,
|
||
|
file: null,
|
||
|
http: 80,
|
||
|
https: 443,
|
||
|
ws: 80,
|
||
|
wss: 443
|
||
|
};
|
||
|
|
||
|
const failure = Symbol("failure");
|
||
|
|
||
|
function countSymbols(str) {
|
||
|
return [...str].length;
|
||
|
}
|
||
|
|
||
|
function at(input, idx) {
|
||
|
const c = input[idx];
|
||
|
return isNaN(c) ? undefined : String.fromCodePoint(c);
|
||
|
}
|
||
|
|
||
|
function isSingleDot(buffer) {
|
||
|
return buffer === "." || buffer.toLowerCase() === "%2e";
|
||
|
}
|
||
|
|
||
|
function isDoubleDot(buffer) {
|
||
|
buffer = buffer.toLowerCase();
|
||
|
return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e";
|
||
|
}
|
||
|
|
||
|
function isWindowsDriveLetterCodePoints(cp1, cp2) {
|
||
|
return infra.isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
|
||
|
}
|
||
|
|
||
|
function isWindowsDriveLetterString(string) {
|
||
|
return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|");
|
||
|
}
|
||
|
|
||
|
function isNormalizedWindowsDriveLetterString(string) {
|
||
|
return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && string[1] === ":";
|
||
|
}
|
||
|
|
||
|
function containsForbiddenHostCodePoint(string) {
|
||
|
return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|<|>|\?|@|\[|\\|\]|\^|\|/u) !== -1;
|
||
|
}
|
||
|
|
||
|
function containsForbiddenHostCodePointExcludingPercent(string) {
|
||
|
return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|<|>|\?|@|\[|\\|\]|\^|\|/u) !== -1;
|
||
|
}
|
||
|
|
||
|
function isSpecialScheme(scheme) {
|
||
|
return specialSchemes[scheme] !== undefined;
|
||
|
}
|
||
|
|
||
|
function isSpecial(url) {
|
||
|
return isSpecialScheme(url.scheme);
|
||
|
}
|
||
|
|
||
|
function isNotSpecial(url) {
|
||
|
return !isSpecialScheme(url.scheme);
|
||
|
}
|
||
|
|
||
|
function defaultPort(scheme) {
|
||
|
return specialSchemes[scheme];
|
||
|
}
|
||
|
|
||
|
function parseIPv4Number(input) {
|
||
|
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) {
|
||
|
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) {
|
||
|
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(input) {
|
||
|
const address = [0, 0, 0, 0, 0, 0, 0, 0];
|
||
|
let pieceIndex = 0;
|
||
|
let compress = null;
|
||
|
let pointer = 0;
|
||
|
|
||
|
input = Array.from(input, 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 && infra.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 (!infra.isASCIIDigit(input[pointer])) {
|
||
|
return failure;
|
||
|
}
|
||
|
|
||
|
while (infra.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) {
|
||
|
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, 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 (containsForbiddenHostCodePoint(asciiDomain)) {
|
||
|
return failure;
|
||
|
}
|
||
|
|
||
|
if (endsInANumber(asciiDomain)) {
|
||
|
return parseIPv4(asciiDomain);
|
||
|
}
|
||
|
|
||
|
return asciiDomain;
|
||
|
}
|
||
|
|
||
|
function endsInANumber(input) {
|
||
|
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) {
|
||
|
if (containsForbiddenHostCodePointExcludingPercent(input)) {
|
||
|
return failure;
|
||
|
}
|
||
|
|
||
|
return utf8PercentEncodeString(input, isC0ControlPercentEncode);
|
||
|
}
|
||
|
|
||
|
function findLongestZeroSequence(arr) {
|
||
|
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) {
|
||
|
if (typeof host === "number") {
|
||
|
return serializeIPv4(host);
|
||
|
}
|
||
|
|
||
|
// IPv6 serializer
|
||
|
if (host instanceof Array) {
|
||
|
return `[${serializeIPv6(host)}]`;
|
||
|
}
|
||
|
|
||
|
return host;
|
||
|
}
|
||
|
|
||
|
function domainToASCII(domain, beStrict = false) {
|
||
|
const result = tr46.toASCII(domain, {
|
||
|
checkBidi: true,
|
||
|
checkHyphens: false,
|
||
|
checkJoiners: true,
|
||
|
useSTD3ASCIIRules: beStrict,
|
||
|
verifyDNSLength: beStrict
|
||
|
});
|
||
|
if (result === null || result === "") {
|
||
|
return failure;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
function trimControlChars(url) {
|
||
|
return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/ug, "");
|
||
|
}
|
||
|
|
||
|
function trimTabAndNewline(url) {
|
||
|
return url.replace(/\u0009|\u000A|\u000D/ug, "");
|
||
|
}
|
||
|
|
||
|
function shortenPath(url) {
|
||
|
const { path } = url;
|
||
|
if (path.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
path.pop();
|
||
|
}
|
||
|
|
||
|
function includesCredentials(url) {
|
||
|
return url.username !== "" || url.password !== "";
|
||
|
}
|
||
|
|
||
|
function cannotHaveAUsernamePasswordPort(url) {
|
||
|
return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file";
|
||
|
}
|
||
|
|
||
|
function isNormalizedWindowsDriveLetter(string) {
|
||
|
return /^[A-Za-z]:$/u.test(string);
|
||
|
}
|
||
|
|
||
|
function URLStateMachine(input, base, encodingOverride, url, stateOverride) {
|
||
|
this.pointer = 0;
|
||
|
this.input = input;
|
||
|
this.base = base || null;
|
||
|
this.encodingOverride = encodingOverride || "utf-8";
|
||
|
this.stateOverride = stateOverride;
|
||
|
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,
|
||
|
|
||
|
cannotBeABaseURL: false
|
||
|
};
|
||
|
|
||
|
const res = trimControlChars(this.input);
|
||
|
if (res !== this.input) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
this.input = res;
|
||
|
}
|
||
|
|
||
|
const res = trimTabAndNewline(this.input);
|
||
|
if (res !== this.input) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
this.input = res;
|
||
|
|
||
|
this.state = stateOverride || "scheme start";
|
||
|
|
||
|
this.buffer = "";
|
||
|
this.atFlag = false;
|
||
|
this.arrFlag = false;
|
||
|
this.passwordTokenSeenFlag = false;
|
||
|
|
||
|
this.input = Array.from(this.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[`parse ${this.state}`](c, cStr);
|
||
|
if (!ret) {
|
||
|
break; // terminate algorithm
|
||
|
} else if (ret === failure) {
|
||
|
this.failure = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) {
|
||
|
if (infra.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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) {
|
||
|
if (infra.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.cannotBeABaseURL = true;
|
||
|
this.url.path.push("");
|
||
|
this.state = "cannot-be-a-base-URL path";
|
||
|
}
|
||
|
} else if (!this.stateOverride) {
|
||
|
this.buffer = "";
|
||
|
this.state = "no scheme";
|
||
|
this.pointer = -1;
|
||
|
} else {
|
||
|
this.parseError = true;
|
||
|
return failure;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) {
|
||
|
if (this.base === null || (this.base.cannotBeABaseURL && c !== p("#"))) {
|
||
|
return failure;
|
||
|
} else if (this.base.cannotBeABaseURL && c === p("#")) {
|
||
|
this.url.scheme = this.base.scheme;
|
||
|
this.url.path = this.base.path.slice();
|
||
|
this.url.query = this.base.query;
|
||
|
this.url.fragment = "";
|
||
|
this.url.cannotBeABaseURL = true;
|
||
|
this.state = "fragment";
|
||
|
} else if (this.base.scheme === "file") {
|
||
|
this.state = "file";
|
||
|
--this.pointer;
|
||
|
} else {
|
||
|
this.state = "relative";
|
||
|
--this.pointer;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) {
|
||
|
if (c === p("/")) {
|
||
|
this.state = "authority";
|
||
|
} else {
|
||
|
this.state = "path";
|
||
|
--this.pointer;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse relative"] = function parseRelative(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) {
|
||
|
if (c !== p("/") && c !== p("\\")) {
|
||
|
this.state = "authority";
|
||
|
--this.pointer;
|
||
|
} else {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse hostname"] =
|
||
|
URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) {
|
||
|
if (infra.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;
|
||
|
};
|
||
|
|
||
|
const fileOtherwiseCodePoints = new Set([p("/"), p("\\"), p("?"), p("#")]);
|
||
|
|
||
|
function startsWithWindowsDriveLetter(input, pointer) {
|
||
|
const length = input.length - pointer;
|
||
|
return length >= 2 &&
|
||
|
isWindowsDriveLetterCodePoints(input[pointer], input[pointer + 1]) &&
|
||
|
(length === 2 || fileOtherwiseCodePoints.has(input[pointer + 2]));
|
||
|
}
|
||
|
|
||
|
URLStateMachine.prototype["parse file"] = function parseFile(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) {
|
||
|
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;
|
||
|
|
||
|
if (this.stateOverride) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
this.buffer = "";
|
||
|
this.state = "path start";
|
||
|
}
|
||
|
} else {
|
||
|
this.buffer += cStr;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse path start"] = function parsePathStart(c) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse path"] = function parsePath(c) {
|
||
|
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("%") &&
|
||
|
(!infra.isASCIIHex(this.input[this.pointer + 1]) ||
|
||
|
!infra.isASCIIHex(this.input[this.pointer + 2]))) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
|
||
|
this.buffer += utf8PercentEncodeCodePoint(c, isPathPercentEncode);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) {
|
||
|
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("%") &&
|
||
|
(!infra.isASCIIHex(this.input[this.pointer + 1]) ||
|
||
|
!infra.isASCIIHex(this.input[this.pointer + 2]))) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
|
||
|
if (!isNaN(c)) {
|
||
|
this.url.path[0] += utf8PercentEncodeCodePoint(c, isC0ControlPercentEncode);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) {
|
||
|
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("%") &&
|
||
|
(!infra.isASCIIHex(this.input[this.pointer + 1]) ||
|
||
|
!infra.isASCIIHex(this.input[this.pointer + 2]))) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
|
||
|
this.buffer += cStr;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
URLStateMachine.prototype["parse fragment"] = function parseFragment(c) {
|
||
|
if (!isNaN(c)) {
|
||
|
// TODO: If c is not a URL code point and not "%", parse error.
|
||
|
if (c === p("%") &&
|
||
|
(!infra.isASCIIHex(this.input[this.pointer + 1]) ||
|
||
|
!infra.isASCIIHex(this.input[this.pointer + 2]))) {
|
||
|
this.parseError = true;
|
||
|
}
|
||
|
|
||
|
this.url.fragment += utf8PercentEncodeCodePoint(c, isFragmentPercentEncode);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
function serializeURL(url, excludeFragment) {
|
||
|
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.cannotBeABaseURL) {
|
||
|
output += url.path[0];
|
||
|
} else {
|
||
|
if (url.host === null && url.path.length > 1 && url.path[0] === "") {
|
||
|
output += "/.";
|
||
|
}
|
||
|
for (const segment of url.path) {
|
||
|
output += `/${segment}`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (url.query !== null) {
|
||
|
output += `?${url.query}`;
|
||
|
}
|
||
|
|
||
|
if (!excludeFragment && url.fragment !== null) {
|
||
|
output += `#${url.fragment}`;
|
||
|
}
|
||
|
|
||
|
return output;
|
||
|
}
|
||
|
|
||
|
function serializeOrigin(tuple) {
|
||
|
let result = `${tuple.scheme}://`;
|
||
|
result += serializeHost(tuple.host);
|
||
|
|
||
|
if (tuple.port !== null) {
|
||
|
result += `:${tuple.port}`;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
module.exports.serializeURL = serializeURL;
|
||
|
|
||
|
module.exports.serializeURLOrigin = function (url) {
|
||
|
// https://url.spec.whatwg.org/#concept-url-origin
|
||
|
switch (url.scheme) {
|
||
|
case "blob":
|
||
|
try {
|
||
|
return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0]));
|
||
|
} 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";
|
||
|
}
|
||
|
};
|
||
|
|
||
|
module.exports.basicURLParse = function (input, options) {
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
module.exports.setTheUsername = function (url, username) {
|
||
|
url.username = utf8PercentEncodeString(username, isUserinfoPercentEncode);
|
||
|
};
|
||
|
|
||
|
module.exports.setThePassword = function (url, password) {
|
||
|
url.password = utf8PercentEncodeString(password, isUserinfoPercentEncode);
|
||
|
};
|
||
|
|
||
|
module.exports.serializeHost = serializeHost;
|
||
|
|
||
|
module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort;
|
||
|
|
||
|
module.exports.serializeInteger = function (integer) {
|
||
|
return String(integer);
|
||
|
};
|
||
|
|
||
|
module.exports.parseURL = function (input, options) {
|
||
|
if (options === undefined) {
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
// We don't handle blobs, so this just delegates:
|
||
|
return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride });
|
||
|
};
|