Spaces:
Paused
Paused
| ; | |
| require("@exodus/bytes/encoding.js"); // for legacy multi-byte encodings | |
| const { percentEncodeAfterEncoding } = require("@exodus/bytes/whatwg.js"); | |
| const tr46 = require("tr46"); | |
| const infra = require("./infra"); | |
| const { utf8DecodeWithoutBOM } = require("./encoding"); | |
| const { percentDecodeString, utf8PercentEncodeCodePoint, utf8PercentEncodeString, | |
| isC0ControlPercentEncode, isFragmentPercentEncode, | |
| extraQueryPercentEncodeChars, extraSpecialQueryPercentEncodeChars, | |
| 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 containsForbiddenDomainCodePoint(string) { | |
| return containsForbiddenHostCodePoint(string) || string.search(/[\u0000-\u001F]|%|\u007F/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 = findTheIPv6AddressCompressedPieceIndex(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, isOpaque = false) { | |
| if (input[0] === "[") { | |
| if (input[input.length - 1] !== "]") { | |
| return failure; | |
| } | |
| return parseIPv6(input.substring(1, input.length - 1)); | |
| } | |
| if (isOpaque) { | |
| return parseOpaqueHost(input); | |
| } | |
| const domain = utf8DecodeWithoutBOM(percentDecodeString(input)); | |
| const asciiDomain = domainToASCII(domain); | |
| if (asciiDomain === failure) { | |
| 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 (containsForbiddenHostCodePoint(input)) { | |
| return failure; | |
| } | |
| return utf8PercentEncodeString(input, isC0ControlPercentEncode); | |
| } | |
| function findTheIPv6AddressCompressedPieceIndex(address) { | |
| let longestIndex = null; | |
| let longestSize = 1; // only find elements > 1 | |
| let foundIndex = null; | |
| let foundSize = 0; | |
| for (let pieceIndex = 0; pieceIndex < address.length; ++pieceIndex) { | |
| if (address[pieceIndex] !== 0) { | |
| if (foundSize > longestSize) { | |
| longestIndex = foundIndex; | |
| longestSize = foundSize; | |
| } | |
| foundIndex = null; | |
| foundSize = 0; | |
| } else { | |
| if (foundIndex === null) { | |
| foundIndex = pieceIndex; | |
| } | |
| ++foundSize; | |
| } | |
| } | |
| if (foundSize > longestSize) { | |
| return foundIndex; | |
| } | |
| return longestIndex; | |
| } | |
| 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, { | |
| checkHyphens: beStrict, | |
| checkBidi: true, | |
| checkJoiners: true, | |
| useSTD3ASCIIRules: beStrict, | |
| transitionalProcessing: false, | |
| verifyDNSLength: beStrict, | |
| ignoreInvalidPunycode: false | |
| }); | |
| if (result === null) { | |
| return failure; | |
| } | |
| if (!beStrict) { | |
| if (result === "") { | |
| return failure; | |
| } | |
| if (containsForbiddenDomainCodePoint(result)) { | |
| return failure; | |
| } | |
| } | |
| return result; | |
| } | |
| function trimControlChars(string) { | |
| // Avoid using regexp because of this V8 bug: https://issues.chromium.org/issues/42204424 | |
| let start = 0; | |
| let end = string.length; | |
| for (; start < end; ++start) { | |
| if (string.charCodeAt(start) > 0x20) { | |
| break; | |
| } | |
| } | |
| for (; end > start; --end) { | |
| if (string.charCodeAt(end - 1) > 0x20) { | |
| break; | |
| } | |
| } | |
| return string.substring(start, end); | |
| } | |
| 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.scheme === "file"; | |
| } | |
| function hasAnOpaquePath(url) { | |
| return typeof url.path === "string"; | |
| } | |
| function isNormalizedWindowsDriveLetter(string) { | |
| return /^[A-Za-z]:$/u.test(string); | |
| } | |
| function URLStateMachine(input, base, encoding, url, stateOverride) { | |
| this.pointer = 0; | |
| this.input = input; | |
| this.base = base || null; | |
| this.encoding = encoding || "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 | |
| }; | |
| 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.atSignSeen = false; | |
| this.insideBrackets = false; | |
| this.passwordTokenSeen = 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.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; | |
| }; | |
| URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { | |
| 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; | |
| }; | |
| 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.atSignSeen) { | |
| this.buffer = `%40${this.buffer}`; | |
| } | |
| this.atSignSeen = 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.passwordTokenSeen) { | |
| this.passwordTokenSeen = true; | |
| continue; | |
| } | |
| const encodedCodePoints = utf8PercentEncodeCodePoint(codePoint, isUserinfoPercentEncode); | |
| if (this.passwordTokenSeen) { | |
| 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.atSignSeen && 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.insideBrackets) { | |
| if (this.buffer === "") { | |
| this.parseError = true; | |
| return failure; | |
| } | |
| if (this.stateOverride === "hostname") { | |
| return failure; | |
| } | |
| 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 failure; | |
| } | |
| 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.insideBrackets = true; | |
| } else if (c === p("]")) { | |
| this.insideBrackets = 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; | |
| } | |
| } | |
| if (this.stateOverride) { | |
| return failure; | |
| } | |
| 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 opaque path"] = function parseOpaquePath(c) { | |
| if (c === p("?")) { | |
| this.url.query = ""; | |
| this.state = "query"; | |
| } else if (c === p("#")) { | |
| this.url.fragment = ""; | |
| this.state = "fragment"; | |
| } else if (c === p(" ")) { | |
| const remaining = this.input[this.pointer + 1]; | |
| if (remaining === p("?") || remaining === p("#")) { | |
| this.url.path += "%20"; | |
| } else { | |
| this.url.path += " "; | |
| } | |
| } 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 += 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.encoding = "utf-8"; | |
| } | |
| if ((!this.stateOverride && c === p("#")) || isNaN(c)) { | |
| const percentEncodeSet = isSpecial(this.url) ? extraSpecialQueryPercentEncodeChars : extraQueryPercentEncodeChars; | |
| this.url.query += percentEncodeAfterEncoding( | |
| this.encoding, | |
| this.buffer, | |
| percentEncodeSet | |
| ); | |
| 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.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) { | |
| let result = `${tuple.scheme}://`; | |
| result += serializeHost(tuple.host); | |
| if (tuple.port !== null) { | |
| result += `:${tuple.port}`; | |
| } | |
| return result; | |
| } | |
| function serializePath(url) { | |
| if (hasAnOpaquePath(url)) { | |
| return url.path; | |
| } | |
| let output = ""; | |
| for (const segment of url.path) { | |
| output += `/${segment}`; | |
| } | |
| return output; | |
| } | |
| module.exports.serializeURL = serializeURL; | |
| module.exports.serializePath = serializePath; | |
| module.exports.serializeURLOrigin = function (url) { | |
| // https://url.spec.whatwg.org/#concept-url-origin | |
| switch (url.scheme) { | |
| case "blob": { | |
| const pathURL = module.exports.parseURL(serializePath(url)); | |
| if (pathURL === null) { | |
| return "null"; | |
| } | |
| if (pathURL.scheme !== "http" && pathURL.scheme !== "https") { | |
| return "null"; | |
| } | |
| return module.exports.serializeURLOrigin(pathURL); | |
| } | |
| 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.encoding, 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.hasAnOpaquePath = hasAnOpaquePath; | |
| 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, encoding: options.encoding }); | |
| }; | |