UNPKG

whatwg-url

Version:

An implementation of the WHATWG URL Standard's URL API and parsing machinery

222 lines (183 loc) 5.61 kB
"use strict"; const tr46 = require("tr46"); const infra = require("./infra"); function p(char) { return char.codePointAt(0); } const failure = Symbol("failure"); const specialSchemes = { ftp: 21, file: null, http: 80, https: 443, ws: 80, wss: 443 }; const urlCodePoints = new Set([ p("!"), p("$"), p("&"), p("'"), p("("), p(")"), p("*"), p("+"), p(","), p("-"), p("."), p("/"), p(":"), p(";"), p("="), p("?"), p("@"), p("_"), p("~") ]); const forbiddenHostCodePoints = new Set([ 0x00, 0x09, 0x0A, 0x0D, 0x20, p("#"), p("/"), p(":"), p("<"), p(">"), p("?"), p("@"), p("["), p("\\"), p("]"), p("^"), p("|") ]); function isURLCodePoint(c) { return infra.isASCIIAlphanumeric(c) || urlCodePoints.has(c) || (c >= 0xA0 && c <= 0x10FFFD && (c < 0xD800 || c > 0xDFFF) && !infra.isNoncharacter(c)); } function isInvalidURLCodePoint(c) { return !isURLCodePoint(c) && c !== p("%"); } function isPercentEncodedByteAt(input, index) { return index + 2 < input.length && input[index] === "%" && infra.isASCIIHex(input.charCodeAt(index + 1)) && infra.isASCIIHex(input.charCodeAt(index + 2)); } function containsPercentEncodedByte(input) { return /%[0-9A-Fa-f]{2}/u.test(input); } function containsForbiddenHostCodePoint(string) { return [...string].some(c => forbiddenHostCodePoints.has(c.codePointAt(0))); } function containsForbiddenDomainCodePoint(string) { return [...string].some(c => { const cp = c.codePointAt(0); return forbiddenHostCodePoints.has(cp) || (cp >= 0x00 && cp <= 0x1F) || cp === p("%") || cp === 0x7F; }); } function domainParserToASCII(domain, beStrict) { return tr46.toASCII(domain, { checkHyphens: beStrict, checkBidi: true, checkJoiners: true, useSTD3ASCIIRules: beStrict, transitionalProcessing: false, verifyDNSLength: beStrict, ignoreInvalidPunycode: false }); } function parseIPv4Number(input, validationErrors = null) { if (input === "") { return failure; } let validationErrorSeen = false; let R = 10; if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { validationErrorSeen = true; input = input.substring(2); R = 16; } else if (input.length >= 2 && input.charAt(0) === "0") { validationErrorSeen = true; input = input.substring(1); R = 8; } if (input === "") { validationErrors?.push("IPv4-non-decimal-part"); 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; } if (validationErrorSeen) { validationErrors?.push("IPv4-non-decimal-part"); } return parseInt(input, R); } 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 (/^[0-9]+$/u.test(last)) { return true; } if (parseIPv4Number(last) !== failure) { return true; } return false; } function domainParser(domain, validationErrors = null, beStrict = false) { // A domain-to-ASCII validation error is reported whenever the strict Unicode ToASCII (CheckHyphens, // UseSTD3ASCIIRules, and VerifyDnsLength all true) fails, even when the relaxed parameters used // for non-strict parsing succeed. This step does not itself fail the algorithm. // // Spec divergence (performance): the URL Standard runs this strict pass unconditionally, but when // beStrict is false its only effect is that validation error, so we skip it when we are neither // being strict nor collecting validation errors. if (beStrict || validationErrors) { const strictResult = domainParserToASCII(domain, true); if (strictResult === null) { validationErrors?.push("domain-to-ASCII"); } if (beStrict) { return strictResult === null ? failure : strictResult; } } let result; if (infra.isASCIIString(domain)) { // For web compatibility an ASCII domain is returned lowercased regardless of ToASCII's outcome. result = domain.toLowerCase(); } else { result = domainParserToASCII(domain, false); if (result === null) { return failure; } } if (result === "") { return failure; } if (containsForbiddenDomainCodePoint(result)) { return failure; } return result; } function isSpecialScheme(scheme) { return specialSchemes[scheme.toLowerCase()] !== undefined; } function isSpecialSchemeExceptFile(scheme) { return isSpecialScheme(scheme) && scheme.toLowerCase() !== "file"; } function defaultPort(scheme) { return specialSchemes[scheme.toLowerCase()]; } 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] === ":"; } module.exports = { containsForbiddenHostCodePoint, containsPercentEncodedByte, defaultPort, domainParser, endsInANumber, failure, forbiddenHostCodePoints, isInvalidURLCodePoint, isNormalizedWindowsDriveLetterString, isPercentEncodedByteAt, parseIPv4Number, isSpecialScheme, isSpecialSchemeExceptFile, isURLCodePoint, isWindowsDriveLetterCodePoints, isWindowsDriveLetterString, p };