UNPKG

whatwg-url

Version:

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

439 lines (338 loc) 11.5 kB
"use strict"; const infra = require("./infra"); const { domainParser, endsInANumber, failure, forbiddenHostCodePoints, isPercentEncodedByteAt, isSpecialSchemeExceptFile, isURLCodePoint, p } = require("./url-miscellaneous"); const { hasAnOpaquePath } = require("./url-state-machine"); const pathSegmentExcludedCodePoints = new Set([p("/"), p("?")]); const opaquePathExcludedCodePoints = new Set([p("?")]); function splitOffFragment(input) { const fragmentStart = input.indexOf("#"); if (fragmentStart === -1) { return { beforeFragment: input, fragment: null }; } return { beforeFragment: input.substring(0, fragmentStart), fragment: input.substring(fragmentStart + 1) }; } function splitOffQuery(input) { const queryStart = input.indexOf("?"); if (queryStart === -1) { return { beforeQuery: input, query: null }; } return { beforeQuery: input.substring(0, queryStart), query: input.substring(queryStart + 1) }; } function isValidURLString(input, options = {}) { const baseURL = options.baseURL ?? null; return isValidAbsoluteURLWithFragmentString(input) || (baseURL !== null && isValidRelativeURLWithFragmentString(input, baseURL)); } function isValidAbsoluteURLWithFragmentString(input) { const { beforeFragment, fragment } = splitOffFragment(input); return (fragment === null || isValidURLFragmentString(fragment)) && isValidAbsoluteURLString(beforeFragment); } function isValidAbsoluteURLString(input) { const { beforeQuery, query } = splitOffQuery(input); if (query !== null && !isValidURLQueryString(query)) { return false; } const schemeMatch = /^([A-Za-z][A-Za-z0-9+.-]*):/u.exec(beforeQuery); if (schemeMatch === null) { return false; } const scheme = schemeMatch[1].toLowerCase(); const afterScheme = beforeQuery.substring(schemeMatch[0].length); if (isSpecialSchemeExceptFile(scheme)) { return isValidSchemeRelativeSpecialURLString(afterScheme); } if (scheme === "file") { return isValidSchemeRelativeFileURLString(afterScheme); } return isValidSchemeRelativeURLString(afterScheme) || isValidPathAbsoluteNonAuthorityURLString(afterScheme) || isValidOpaquePathURLString(afterScheme); } function isValidRelativeURLWithFragmentString(input, baseURL) { if (hasAnOpaquePath(baseURL)) { return input.startsWith("#") && isValidURLFragmentString(input.substring(1)); } const { beforeFragment, fragment } = splitOffFragment(input); return isValidRelativeURLString(beforeFragment, baseURL) && (fragment === null || isValidURLFragmentString(fragment)); } function isValidRelativeURLString(input, baseURL) { const { beforeQuery, query } = splitOffQuery(input); const hasValidQuery = query === null || isValidURLQueryString(query); if (isValidPathAbsoluteNonAuthorityURLString(beforeQuery)) { return hasValidQuery; } if (isValidPathRelativeSchemeLessURLString(beforeQuery)) { return hasValidQuery; } if (isSpecialSchemeExceptFile(baseURL.scheme)) { return isValidSchemeRelativeSpecialURLString(beforeQuery) && hasValidQuery; } if (baseURL.scheme === "file") { return isValidSchemeRelativeFileURLString(beforeQuery) && hasValidQuery; } return isValidSchemeRelativeURLString(beforeQuery) && hasValidQuery; } function isValidSchemeRelativeSpecialURLString(input) { if (!input.startsWith("//")) { return false; } const { authority, path } = splitOffPath(input.substring(2)); return isValidHostAndPortString(authority, isValidHostString) && (path === null || isValidPathAbsoluteURLString(path)); } function isValidSchemeRelativeURLString(input) { if (!input.startsWith("//")) { return false; } const { authority, path } = splitOffPath(input.substring(2)); return isValidOpaqueHostAndPortString(authority) && (path === null || isValidPathAbsoluteURLString(path)); } function isValidSchemeRelativeFileURLString(input) { if (!input.startsWith("//")) { return false; } const afterSlashes = input.substring(2); return afterSlashes === "" || isValidHostAndPathAbsoluteURLString(afterSlashes) || isValidPathAbsoluteURLString(afterSlashes); } function isValidHostAndPathAbsoluteURLString(input) { const { authority, path } = splitOffPath(input); return isValidHostString(authority) && (path === null || isValidPathAbsoluteURLString(path)); } function splitOffPath(input) { const pathStart = input.indexOf("/"); if (pathStart === -1) { return { authority: input, path: null }; } return { authority: input.substring(0, pathStart), path: input.substring(pathStart) }; } function isValidHostAndPortString(input, isValidHost) { const parsed = splitHostAndPort(input); return parsed !== null && parsed.host !== "" && isValidHost(parsed.host) && (parsed.port === null || isValidURLPortString(parsed.port)); } function isValidOpaqueHostAndPortString(input) { if (input === "") { return true; } const parsed = splitHostAndPort(input); return parsed !== null && parsed.host !== "" && isValidOpaqueHostString(parsed.host) && (parsed.port === null || isValidURLPortString(parsed.port)); } function splitHostAndPort(input) { if (input.startsWith("[")) { const hostEnd = input.indexOf("]"); if (hostEnd === -1) { return null; } const rest = input.substring(hostEnd + 1); if (rest !== "" && !rest.startsWith(":")) { return null; } return { host: input.substring(0, hostEnd + 1), port: rest === "" ? null : rest.substring(1) }; } const portStart = input.indexOf(":"); if (portStart === -1) { return { host: input, port: null }; } return { host: input.substring(0, portStart), port: input.substring(portStart + 1) }; } function isValidHostString(input) { return isValidDomainString(input) || isValidIPv4AddressString(input) || isValidBracketedIPv6AddressString(input); } function isValidDomainString(input) { const domain = domainParser(input, null, true); return domain !== failure && !endsInANumber(domain); } function isValidIPv4AddressString(input) { const pieces = input.split("."); return pieces.length === 4 && pieces.every(isValidIPv4AddressPieceString); } function isValidIPv4AddressPieceString(input) { if (input === "" || input.length > 3) { return false; } for (let i = 0; i < input.length; ++i) { if (!infra.isASCIIDigit(input.codePointAt(i))) { return false; } } if (input.length > 1 && input[0] === "0") { return false; } return Number(input) <= 255; } function isValidBracketedIPv6AddressString(input) { return input.startsWith("[") && input.endsWith("]") && isValidIPv6AddressString(input.substring(1, input.length - 1)); } function isValidIPv6AddressString(input) { return getIPv6PiecesStringEffectiveLength(input) === 8 || getIPv6PiecesAndIPv4StringEffectiveLength(input) === 8 || isValidCompressedIPv6AddressString(input); } function isValidCompressedIPv6AddressString(input) { const compressionIndex = input.indexOf("::"); if (compressionIndex === -1 || compressionIndex !== input.lastIndexOf("::")) { return false; } const preceding = input.substring(0, compressionIndex); const following = input.substring(compressionIndex + 2); const precedingLength = getOptionalIPv6PiecesStringEffectiveLength(preceding); const followingLength = getOptionalIPv6PiecesOrPiecesAndIPv4StringEffectiveLength(following); if (precedingLength === failure || followingLength === failure) { return false; } return precedingLength + followingLength <= 7; } function getOptionalIPv6PiecesStringEffectiveLength(input) { return input === "" ? 0 : getIPv6PiecesStringEffectiveLength(input); } function getOptionalIPv6PiecesOrPiecesAndIPv4StringEffectiveLength(input) { return input === "" ? 0 : getIPv6PiecesOrPiecesAndIPv4StringEffectiveLength(input); } function getIPv6PiecesOrPiecesAndIPv4StringEffectiveLength(input) { if (isValidIPv6PiecesString(input)) { return getIPv6PiecesStringEffectiveLength(input); } if (isValidIPv6PiecesAndIPv4String(input)) { return getIPv6PiecesAndIPv4StringEffectiveLength(input); } return failure; } function isValidIPv6PiecesAndIPv4String(input) { if (isValidIPv4AddressString(input)) { return true; } const ipv4SeparatorIndex = input.lastIndexOf(":"); if (ipv4SeparatorIndex === -1) { return false; } return isValidIPv6PiecesString(input.substring(0, ipv4SeparatorIndex)) && isValidIPv4AddressString(input.substring(ipv4SeparatorIndex + 1)); } function getIPv6PiecesAndIPv4StringEffectiveLength(input) { if (!isValidIPv6PiecesAndIPv4String(input)) { return failure; } const ipv4SeparatorIndex = input.lastIndexOf(":"); if (ipv4SeparatorIndex === -1) { return 2; } const piecesLength = getIPv6PiecesStringEffectiveLength(input.substring(0, ipv4SeparatorIndex)); return piecesLength + 2; } function isValidIPv6PiecesString(input) { if (input === "") { return false; } const pieces = input.split(":"); return pieces.every(isValidIPv6PieceString); } function getIPv6PiecesStringEffectiveLength(input) { return isValidIPv6PiecesString(input) ? input.split(":").length : failure; } function isValidIPv6PieceString(input) { if (input === "" || input.length > 4) { return false; } for (let i = 0; i < input.length; ++i) { if (!infra.isASCIIHex(input.codePointAt(i))) { return false; } } const value = parseInt(input, 16); return value <= 0xFFFF && (input.length === 1 || input[0] !== "0"); } function isValidOpaqueHostString(input) { return isValidBracketedIPv6AddressString(input) || (input !== "" && isValidURLUnits(input, forbiddenHostCodePoints)); } function isValidURLPortString(input) { if (input === "") { return true; } if (!/^[0-9]+$/u.test(input)) { return false; } return Number(input) <= 65535; } function isValidPathAbsoluteURLString(input) { return input.startsWith("/") && input.substring(1).split("/").every(isValidURLPathSegmentString); } function isValidPathAbsoluteNonAuthorityURLString(input) { return isValidPathAbsoluteURLString(input) && !input.startsWith("//"); } function isValidPathRelativeURLString(input) { return !input.startsWith("/") && input.split("/").every(isValidURLPathSegmentString); } function isValidURLPathSegmentString(input) { return isValidURLUnits(input, pathSegmentExcludedCodePoints); } function isValidPathRelativeSchemeLessURLString(input) { return isValidPathRelativeURLString(input) && !/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(input); } function isValidOpaquePathURLString(input) { return (input === "" || input[0] !== "/") && isValidURLUnits(input, opaquePathExcludedCodePoints); } function isValidURLQueryString(input) { return isValidURLUnits(input); } function isValidURLFragmentString(input) { return isValidURLUnits(input); } function isValidURLUnits(input, excludedCodePoints = new Set()) { for (let i = 0; i < input.length;) { if (input[i] === "%") { if (!isPercentEncodedByteAt(input, i)) { return false; } i += 3; continue; } const codePoint = input.codePointAt(i); if (!isURLCodePoint(codePoint) || excludedCodePoints.has(codePoint)) { return false; } i += codePoint > 0xFFFF ? 2 : 1; } return true; } module.exports = { isValidURLString };