You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1210 lines
29 KiB
1210 lines
29 KiB
"use strict"; |
|
const punycode = require("punycode"); |
|
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"); |
|
|
|
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 === 58 || cp2 === 124); |
|
} |
|
|
|
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) { |
|
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 input; |
|
} |
|
|
|
const numbers = []; |
|
for (const part of parts) { |
|
if (part === "") { |
|
return input; |
|
} |
|
const n = parseIPv4Number(part); |
|
if (n === failure) { |
|
return input; |
|
} |
|
|
|
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 = punycode.ucs2.decode(input); |
|
|
|
if (input[pointer] === 58) { |
|
if (input[pointer + 1] !== 58) { |
|
return failure; |
|
} |
|
|
|
pointer += 2; |
|
++pieceIndex; |
|
compress = pieceIndex; |
|
} |
|
|
|
while (pointer < input.length) { |
|
if (pieceIndex === 8) { |
|
return failure; |
|
} |
|
|
|
if (input[pointer] === 58) { |
|
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] === 46) { |
|
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] === 46 && 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] === 58) { |
|
++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; |
|
} |
|
|
|
const ipv4Host = parseIPv4(asciiDomain); |
|
if (typeof ipv4Host === "number" || ipv4Host === failure) { |
|
return ipv4Host; |
|
} |
|
|
|
return asciiDomain; |
|
} |
|
|
|
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 = punycode.ucs2.decode(this.input); |
|
|
|
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 === 43 || c === 45 || c === 46) { |
|
this.buffer += cStr.toLowerCase(); |
|
} else if (c === 58) { |
|
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] !== 47 || this.input[this.pointer + 2] !== 47) { |
|
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] === 47) { |
|
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 !== 35)) { |
|
return failure; |
|
} else if (this.base.cannotBeABaseURL && c === 35) { |
|
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 === 47 && this.input[this.pointer + 1] === 47) { |
|
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 === 47) { |
|
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 === 47) { |
|
this.state = "relative slash"; |
|
} else if (isSpecial(this.url) && c === 92) { |
|
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 === 63) { |
|
this.url.query = ""; |
|
this.state = "query"; |
|
} else if (c === 35) { |
|
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 === 47 || c === 92)) { |
|
if (c === 92) { |
|
this.parseError = true; |
|
} |
|
this.state = "special authority ignore slashes"; |
|
} else if (c === 47) { |
|
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 === 47 && this.input[this.pointer + 1] === 47) { |
|
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 !== 47 && c !== 92) { |
|
this.state = "authority"; |
|
--this.pointer; |
|
} else { |
|
this.parseError = true; |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { |
|
if (c === 64) { |
|
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 === 58 && !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 === 47 || c === 63 || c === 35 || |
|
(isSpecial(this.url) && c === 92)) { |
|
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 === 58 && !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 === 47 || c === 63 || c === 35 || |
|
(isSpecial(this.url) && c === 92)) { |
|
--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 === 91) { |
|
this.arrFlag = true; |
|
} else if (c === 93) { |
|
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 === 47 || c === 63 || c === 35 || |
|
(isSpecial(this.url) && c === 92) || |
|
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([47, 92, 63, 35]); |
|
|
|
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 === 47 || c === 92) { |
|
if (c === 92) { |
|
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 === 63) { |
|
this.url.query = ""; |
|
this.state = "query"; |
|
} else if (c === 35) { |
|
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 === 47 || c === 92) { |
|
if (c === 92) { |
|
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 === 47 || c === 92 || c === 63 || c === 35) { |
|
--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 === 92) { |
|
this.parseError = true; |
|
} |
|
this.state = "path"; |
|
|
|
if (c !== 47 && c !== 92) { |
|
--this.pointer; |
|
} |
|
} else if (!this.stateOverride && c === 63) { |
|
this.url.query = ""; |
|
this.state = "query"; |
|
} else if (!this.stateOverride && c === 35) { |
|
this.url.fragment = ""; |
|
this.state = "fragment"; |
|
} else if (c !== undefined) { |
|
this.state = "path"; |
|
if (c !== 47) { |
|
--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 === 47 || (isSpecial(this.url) && c === 92) || |
|
(!this.stateOverride && (c === 63 || c === 35))) { |
|
if (isSpecial(this.url) && c === 92) { |
|
this.parseError = true; |
|
} |
|
|
|
if (isDoubleDot(this.buffer)) { |
|
shortenPath(this.url); |
|
if (c !== 47 && !(isSpecial(this.url) && c === 92)) { |
|
this.url.path.push(""); |
|
} |
|
} else if (isSingleDot(this.buffer) && c !== 47 && |
|
!(isSpecial(this.url) && c === 92)) { |
|
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 === 63) { |
|
this.url.query = ""; |
|
this.state = "query"; |
|
} |
|
if (c === 35) { |
|
this.url.fragment = ""; |
|
this.state = "fragment"; |
|
} |
|
} else { |
|
// TODO: If c is not a URL code point and not "%", parse error. |
|
|
|
if (c === 37 && |
|
(!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 === 63) { |
|
this.url.query = ""; |
|
this.state = "query"; |
|
} else if (c === 35) { |
|
this.url.fragment = ""; |
|
this.state = "fragment"; |
|
} else { |
|
// TODO: Add: not a URL code point |
|
if (!isNaN(c) && c !== 37) { |
|
this.parseError = true; |
|
} |
|
|
|
if (c === 37 && |
|
(!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 === 35) || isNaN(c)) { |
|
const queryPercentEncodePredicate = isSpecial(this.url) ? isSpecialQueryPercentEncode : isQueryPercentEncode; |
|
this.url.query += utf8PercentEncodeString(this.buffer, queryPercentEncodePredicate); |
|
|
|
this.buffer = ""; |
|
|
|
if (c === 35) { |
|
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 === 37 && |
|
(!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 === 37 && |
|
(!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 }); |
|
};
|
|
|