| "use strict"; |
| const punycode = require("punycode"); |
| const tr46 = require("tr46"); |
| |
| const specialSchemes = { |
| ftp: 21, |
| file: null, |
| gopher: 70, |
| http: 80, |
| https: 443, |
| ws: 80, |
| wss: 443 |
| }; |
| |
| const failure = Symbol("failure"); |
| |
| function countSymbols(str) { |
| return punycode.ucs2.decode(str).length; |
| } |
| |
| function at(input, idx) { |
| const c = input[idx]; |
| return isNaN(c) ? undefined : String.fromCodePoint(c); |
| } |
| |
| function isASCIIDigit(c) { |
| return c >= 0x30 && c <= 0x39; |
| } |
| |
| function isASCIIAlpha(c) { |
| return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); |
| } |
| |
| function isASCIIAlphanumeric(c) { |
| return isASCIIAlpha(c) || isASCIIDigit(c); |
| } |
| |
| function isASCIIHex(c) { |
| return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); |
| } |
| |
| 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 isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); |
| } |
| |
| function isWindowsDriveLetterString(string) { |
| return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); |
| } |
| |
| function isNormalizedWindowsDriveLetterString(string) { |
| return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; |
| } |
| |
| function containsForbiddenHostCodePoint(string) { |
| return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; |
| } |
| |
| function containsForbiddenHostCodePointExcludingPercent(string) { |
| return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; |
| } |
| |
| function isSpecialScheme(scheme) { |
| return specialSchemes[scheme] !== undefined; |
| } |
| |
| function isSpecial(url) { |
| return isSpecialScheme(url.scheme); |
| } |
| |
| function defaultPort(scheme) { |
| return specialSchemes[scheme]; |
| } |
| |
| function percentEncode(c) { |
| let hex = c.toString(16).toUpperCase(); |
| if (hex.length === 1) { |
| hex = "0" + hex; |
| } |
| |
| return "%" + hex; |
| } |
| |
| function utf8PercentEncode(c) { |
| const buf = new Buffer(c); |
| |
| let str = ""; |
| |
| for (let i = 0; i < buf.length; ++i) { |
| str += percentEncode(buf[i]); |
| } |
| |
| return str; |
| } |
| |
| function utf8PercentDecode(str) { |
| const input = new Buffer(str); |
| const output = []; |
| for (let i = 0; i < input.length; ++i) { |
| if (input[i] !== 37) { |
| output.push(input[i]); |
| } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { |
| output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); |
| i += 2; |
| } else { |
| output.push(input[i]); |
| } |
| } |
| return new Buffer(output).toString(); |
| } |
| |
| function isC0ControlPercentEncode(c) { |
| return c <= 0x1F || c > 0x7E; |
| } |
| |
| const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); |
| function isPathPercentEncode(c) { |
| return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); |
| } |
| |
| const extraUserinfoPercentEncodeSet = |
| new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); |
| function isUserinfoPercentEncode(c) { |
| return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); |
| } |
| |
| function percentEncodeChar(c, encodeSetPredicate) { |
| const cStr = String.fromCodePoint(c); |
| |
| if (encodeSetPredicate(c)) { |
| return utf8PercentEncode(cStr); |
| } |
| |
| return cStr; |
| } |
| |
| 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; |
| } |
| |
| const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); |
| 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] >= Math.pow(256, 5 - numbers.length)) { |
| return failure; |
| } |
| |
| let ipv4 = numbers.pop(); |
| let counter = 0; |
| |
| for (const n of numbers) { |
| ipv4 += n * Math.pow(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 && 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 (!isASCIIDigit(input[pointer])) { |
| return failure; |
| } |
| |
| while (isASCIIDigit(input[pointer])) { |
| const number = parseInt(at(input, pointer)); |
| if (ipv4Piece === null) { |
| ipv4Piece = number; |
| } else if (ipv4Piece === 0) { |
| return failure; |
| } else { |
| ipv4Piece = ipv4Piece * 10 + number; |
| } |
| if (ipv4Piece > 255) { |
| return failure; |
| } |
| ++pointer; |
| } |
| |
| address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; |
| |
| ++numbersSeen; |
| |
| if (numbersSeen === 2 || numbersSeen === 4) { |
| ++pieceIndex; |
| } |
| } |
| |
| if (numbersSeen !== 4) { |
| return failure; |
| } |
| |
| break; |
| } else if (input[pointer] === 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 seqResult = findLongestZeroSequence(address); |
| const compress = seqResult.idx; |
| 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, isSpecialArg) { |
| if (input[0] === "[") { |
| if (input[input.length - 1] !== "]") { |
| return failure; |
| } |
| |
| return parseIPv6(input.substring(1, input.length - 1)); |
| } |
| |
| if (!isSpecialArg) { |
| return parseOpaqueHost(input); |
| } |
| |
| const domain = utf8PercentDecode(input); |
| const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); |
| if (asciiDomain === null) { |
| 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; |
| } |
| |
| let output = ""; |
| const decoded = punycode.ucs2.decode(input); |
| for (let i = 0; i < decoded.length; ++i) { |
| output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); |
| } |
| return output; |
| } |
| |
| 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) { |
| maxIdx = currStart; |
| maxLen = currLen; |
| } |
| |
| return { |
| idx: maxIdx, |
| len: maxLen |
| }; |
| } |
| |
| function serializeHost(host) { |
| if (typeof host === "number") { |
| return serializeIPv4(host); |
| } |
| |
| // IPv6 serializer |
| if (host instanceof Array) { |
| return "[" + serializeIPv6(host) + "]"; |
| } |
| |
| return host; |
| } |
| |
| function trimControlChars(url) { |
| return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); |
| } |
| |
| function trimTabAndNewline(url) { |
| return url.replace(/\u0009|\u000A|\u000D/g, ""); |
| } |
| |
| function shortenPath(url) { |
| const path = url.path; |
| 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]:$/.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 (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 (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 === "" || this.url.host === null)) { |
| return false; |
| } |
| } |
| this.url.scheme = this.buffer; |
| this.buffer = ""; |
| if (this.stateOverride) { |
| return false; |
| } |
| 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 (isNaN(c)) { |
| 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; |
| } else if (c === 47) { |
| this.state = "relative slash"; |
| } else if (c === 63) { |
| 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.state = "query"; |
| } else if (c === 35) { |
| 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; |
| this.url.fragment = ""; |
| this.state = "fragment"; |
| } 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(0, this.base.path.length - 1); |
| |
| 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 = percentEncodeChar(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; |
| } |
| |
| const host = parseHost(this.buffer, isSpecial(this.url)); |
| if (host === failure) { |
| return failure; |
| } |
| |
| this.url.host = host; |
| this.buffer = ""; |
| this.state = "port"; |
| if (this.stateOverride === "hostname") { |
| return false; |
| } |
| } 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, isSpecial(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 (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 > Math.pow(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]); |
| |
| URLStateMachine.prototype["parse file"] = function parseFile(c) { |
| this.url.scheme = "file"; |
| |
| if (c === 47 || c === 92) { |
| if (c === 92) { |
| this.parseError = true; |
| } |
| this.state = "file slash"; |
| } else if (this.base !== null && this.base.scheme === "file") { |
| if (isNaN(c)) { |
| this.url.host = this.base.host; |
| this.url.path = this.base.path.slice(); |
| this.url.query = this.base.query; |
| } else if (c === 63) { |
| this.url.host = this.base.host; |
| this.url.path = this.base.path.slice(); |
| this.url.query = ""; |
| this.state = "query"; |
| } else if (c === 35) { |
| this.url.host = this.base.host; |
| this.url.path = this.base.path.slice(); |
| this.url.query = this.base.query; |
| this.url.fragment = ""; |
| this.state = "fragment"; |
| } else { |
| if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points |
| !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || |
| (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points |
| !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { |
| this.url.host = this.base.host; |
| this.url.path = this.base.path.slice(); |
| shortenPath(this.url); |
| } else { |
| this.parseError = true; |
| } |
| |
| 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 (isNormalizedWindowsDriveLetterString(this.base.path[0])) { |
| this.url.path.push(this.base.path[0]); |
| } else { |
| 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, isSpecial(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; |
| } |
| } |
| |
| 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)) { |
| if (this.url.host !== "" && this.url.host !== null) { |
| this.parseError = true; |
| this.url.host = ""; |
| } |
| this.buffer = this.buffer[0] + ":"; |
| } |
| this.url.path.push(this.buffer); |
| } |
| this.buffer = ""; |
| if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { |
| while (this.url.path.length > 1 && this.url.path[0] === "") { |
| this.parseError = true; |
| this.url.path.shift(); |
| } |
| } |
| 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 && |
| (!isASCIIHex(this.input[this.pointer + 1]) || |
| !isASCIIHex(this.input[this.pointer + 2]))) { |
| this.parseError = true; |
| } |
| |
| this.buffer += percentEncodeChar(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 && |
| (!isASCIIHex(this.input[this.pointer + 1]) || |
| !isASCIIHex(this.input[this.pointer + 2]))) { |
| this.parseError = true; |
| } |
| |
| if (!isNaN(c)) { |
| this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); |
| } |
| } |
| |
| return true; |
| }; |
| |
| URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { |
| if (isNaN(c) || (!this.stateOverride && c === 35)) { |
| if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { |
| this.encodingOverride = "utf-8"; |
| } |
| |
| const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead |
| for (let i = 0; i < buffer.length; ++i) { |
| if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || |
| buffer[i] === 0x3C || buffer[i] === 0x3E) { |
| this.url.query += percentEncode(buffer[i]); |
| } else { |
| this.url.query += String.fromCodePoint(buffer[i]); |
| } |
| } |
| |
| this.buffer = ""; |
| 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 && |
| (!isASCIIHex(this.input[this.pointer + 1]) || |
| !isASCIIHex(this.input[this.pointer + 2]))) { |
| this.parseError = true; |
| } |
| |
| this.buffer += cStr; |
| } |
| |
| return true; |
| }; |
| |
| URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { |
| if (isNaN(c)) { // do nothing |
| } else if (c === 0x0) { |
| this.parseError = true; |
| } else { |
| // TODO: If c is not a URL code point and not "%", parse error. |
| if (c === 37 && |
| (!isASCIIHex(this.input[this.pointer + 1]) || |
| !isASCIIHex(this.input[this.pointer + 2]))) { |
| this.parseError = true; |
| } |
| |
| this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); |
| } |
| |
| 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; |
| } |
| } else if (url.host === null && url.scheme === "file") { |
| output += "//"; |
| } |
| |
| if (url.cannotBeABaseURL) { |
| output += url.path[0]; |
| } else { |
| for (const string of url.path) { |
| output += "/" + string; |
| } |
| } |
| |
| 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 "gopher": |
| case "http": |
| case "https": |
| case "ws": |
| case "wss": |
| return serializeOrigin({ |
| scheme: url.scheme, |
| host: url.host, |
| port: url.port |
| }); |
| case "file": |
| // spec says "exercise to the reader", chrome says "file://" |
| return "file://"; |
| 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 "failure"; |
| } |
| |
| return usm.url; |
| }; |
| |
| module.exports.setTheUsername = function (url, username) { |
| url.username = ""; |
| const decoded = punycode.ucs2.decode(username); |
| for (let i = 0; i < decoded.length; ++i) { |
| url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); |
| } |
| }; |
| |
| module.exports.setThePassword = function (url, password) { |
| url.password = ""; |
| const decoded = punycode.ucs2.decode(password); |
| for (let i = 0; i < decoded.length; ++i) { |
| url.password += percentEncodeChar(decoded[i], 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 }); |
| }; |