import * as querystring from "node:querystring"; import * as punnycode from "./punycode.mjs"; import { encodeStr, hexTable } from "./internal/querystring/querystring.mjs"; import { spliceOne } from "./internal/url/util.mjs"; import { ERR_INVALID_ARG_TYPE, ERR_INVALID_URL } from "./internal/url/errors.mjs"; import { pathToFileURL as _pathToFileURL, fileURLToPath, unsafeProtocol, hostlessProtocol, slashedProtocol, urlToHttpOptions } from "./internal/url/url.mjs"; import { CHAR_SPACE, CHAR_TAB, CHAR_CARRIAGE_RETURN, CHAR_LINE_FEED, CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE, CHAR_HASH, CHAR_FORWARD_SLASH, CHAR_LEFT_SQUARE_BRACKET, CHAR_RIGHT_SQUARE_BRACKET, CHAR_LEFT_ANGLE_BRACKET, CHAR_RIGHT_ANGLE_BRACKET, CHAR_LEFT_CURLY_BRACKET, CHAR_RIGHT_CURLY_BRACKET, CHAR_QUESTION_MARK, CHAR_DOUBLE_QUOTE, CHAR_SINGLE_QUOTE, CHAR_PERCENT, CHAR_SEMICOLON, CHAR_BACKWARD_SLASH, CHAR_CIRCUMFLEX_ACCENT, CHAR_GRAVE_ACCENT, CHAR_VERTICAL_LINE, CHAR_AT, CHAR_COLON } from "./internal/url/constants.mjs"; class Url { auth = null; hash = null; host = null; hostname = null; href = null; path = null; pathname = null; protocol = null; search = null; slashes = null; port = null; query = null; parse(url, parseQueryString, slashesDenoteHost) { if (typeof url !== "string") { throw new ERR_INVALID_ARG_TYPE("url", "string", url); } let hasHash = false; let hasAt = false; let start = -1; let end = -1; let rest = ""; let lastPos = 0; for (let i = 0, inWs = false, split = false; i < url.length; ++i) { const code = url.charCodeAt(i); const isWs = code < 33 || code === CHAR_NO_BREAK_SPACE || code === CHAR_ZERO_WIDTH_NOBREAK_SPACE; if (start === -1) { if (isWs) continue; lastPos = start = i; } else if (inWs) { if (!isWs) { end = -1; inWs = false; } } else if (isWs) { end = i; inWs = true; } if (!split) { switch (code) { case CHAR_AT: hasAt = true; break; case CHAR_HASH: hasHash = true; case CHAR_QUESTION_MARK: split = true; break; case CHAR_BACKWARD_SLASH: if (i - lastPos > 0) rest += url.slice(lastPos, i); rest += "/"; lastPos = i + 1; break; } } else if (!hasHash && code === CHAR_HASH) { hasHash = true; } } if (start !== -1) { if (lastPos === start) { if (end === -1) { if (start === 0) rest = url; else rest = url.slice(start); } else { rest = url.slice(start, end); } } else if (end === -1 && lastPos < url.length) { rest += url.slice(lastPos); } else if (end !== -1 && lastPos < end) { rest += url.slice(lastPos, end); } } if (!slashesDenoteHost && !hasHash && !hasAt) { const simplePath = simplePathPattern.exec(rest); if (simplePath) { this.path = rest; this.href = rest; this.pathname = simplePath[1]; if (simplePath[2]) { this.search = simplePath[2]; if (parseQueryString) { this.query = querystring.parse(this.search.slice(1)); } else { this.query = this.search.slice(1); } } else if (parseQueryString) { this.search = null; this.query = { __proto__: null }; } return this; } } const protoMatch = protocolPattern.exec(rest); let proto, lowerProto; if (protoMatch) { proto = protoMatch[0]; lowerProto = proto.toLowerCase(); this.protocol = lowerProto; rest = rest.slice(proto.length); } let slashes; if (slashesDenoteHost || proto || hostPattern.test(rest)) { slashes = rest.charCodeAt(0) === CHAR_FORWARD_SLASH && rest.charCodeAt(1) === CHAR_FORWARD_SLASH; if (slashes && !(proto && hostlessProtocol.has(lowerProto))) { rest = rest.slice(2); this.slashes = true; } } if (!hostlessProtocol.has(lowerProto) && (slashes || proto && !slashedProtocol.has(proto))) { let hostEnd = -1; let atSign = -1; let nonHost = -1; for (let i = 0; i < rest.length; ++i) { switch (rest.charCodeAt(i)) { case CHAR_TAB: case CHAR_LINE_FEED: case CHAR_CARRIAGE_RETURN: rest = rest.slice(0, i) + rest.slice(i + 1); i -= 1; break; case CHAR_SPACE: case CHAR_DOUBLE_QUOTE: case CHAR_PERCENT: case CHAR_SINGLE_QUOTE: case CHAR_SEMICOLON: case CHAR_LEFT_ANGLE_BRACKET: case CHAR_RIGHT_ANGLE_BRACKET: case CHAR_BACKWARD_SLASH: case CHAR_CIRCUMFLEX_ACCENT: case CHAR_GRAVE_ACCENT: case CHAR_LEFT_CURLY_BRACKET: case CHAR_VERTICAL_LINE: case CHAR_RIGHT_CURLY_BRACKET: if (nonHost === -1) nonHost = i; break; case CHAR_HASH: case CHAR_FORWARD_SLASH: case CHAR_QUESTION_MARK: if (nonHost === -1) nonHost = i; hostEnd = i; break; case CHAR_AT: atSign = i; nonHost = -1; break; } if (hostEnd !== -1) break; } start = 0; if (atSign !== -1) { this.auth = decodeURIComponent(rest.slice(0, atSign)); start = atSign + 1; } if (nonHost === -1) { this.host = rest.slice(start); rest = ""; } else { this.host = rest.slice(start, nonHost); rest = rest.slice(nonHost); } this.parseHost(); if (typeof this.hostname !== "string") this.hostname = ""; const hostname = this.hostname; const ipv6Hostname = isIpv6Hostname(hostname); if (!ipv6Hostname) { rest = getHostname(this, rest, hostname, url); } if (this.hostname.length > hostnameMaxLen) { this.hostname = ""; } else { this.hostname = this.hostname.toLowerCase(); } if (this.hostname !== "") { if (ipv6Hostname) { if (forbiddenHostCharsIpv6.test(this.hostname)) { throw new ERR_INVALID_URL(url); } } else { this.hostname = punnycode.toASCII(this.hostname); if (this.hostname === "" || forbiddenHostChars.test(this.hostname)) { throw new ERR_INVALID_URL(url); } } } const p = this.port ? ":" + this.port : ""; const h = this.hostname || ""; this.host = h + p; if (ipv6Hostname) { this.hostname = this.hostname.slice(1, -1); if (rest[0] !== "/") { rest = "/" + rest; } } } if (!unsafeProtocol.has(lowerProto)) { rest = autoEscapeStr(rest); } let questionIdx = -1; let hashIdx = -1; for (let i = 0; i < rest.length; ++i) { const code = rest.charCodeAt(i); if (code === CHAR_HASH) { this.hash = rest.slice(i); hashIdx = i; break; } else if (code === CHAR_QUESTION_MARK && questionIdx === -1) { questionIdx = i; } } if (questionIdx !== -1) { if (hashIdx === -1) { this.search = rest.slice(questionIdx); this.query = rest.slice(questionIdx + 1); } else { this.search = rest.slice(questionIdx, hashIdx); this.query = rest.slice(questionIdx + 1, hashIdx); } if (parseQueryString) { this.query = querystring.parse(this.query); } } else if (parseQueryString) { this.search = null; this.query = { __proto__: null }; } const useQuestionIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx); const firstIdx = useQuestionIdx ? questionIdx : hashIdx; if (firstIdx === -1) { if (rest.length > 0) this.pathname = rest; } else if (firstIdx > 0) { this.pathname = rest.slice(0, firstIdx); } if (slashedProtocol.has(lowerProto) && this.hostname && !this.pathname) { this.pathname = "/"; } if (this.pathname || this.search) { const p = this.pathname || ""; const s = this.search || ""; this.path = p + s; } this.href = this.format(); return this; } format() { let auth = this.auth || ""; if (auth) { auth = encodeStr(auth, noEscapeAuth, hexTable); auth += "@"; } let protocol = this.protocol || ""; let pathname = this.pathname || ""; let hash = this.hash || ""; let host = ""; let query = ""; if (this.host) { host = auth + this.host; } else if (this.hostname) { host = auth + (this.hostname.includes(":") && !isIpv6Hostname(this.hostname) ? "[" + this.hostname + "]" : this.hostname); if (this.port) { host += ":" + this.port; } } if (this.query !== null && typeof this.query === "object") { query = querystring.stringify(this.query); } let search = this.search || query && "?" + query || ""; if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58) protocol += ":"; let newPathname = ""; let lastPos = 0; for (let i = 0; i < pathname.length; ++i) { switch (pathname.charCodeAt(i)) { case CHAR_HASH: if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i); newPathname += "%23"; lastPos = i + 1; break; case CHAR_QUESTION_MARK: if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i); newPathname += "%3F"; lastPos = i + 1; break; } } if (lastPos > 0) { if (lastPos === pathname.length) { pathname = newPathname; } else { pathname = newPathname + pathname.slice(lastPos); } } if (this.slashes || slashedProtocol.has(protocol)) { if (this.slashes || host) { if (pathname && pathname.charCodeAt(0) !== CHAR_FORWARD_SLASH) pathname = "/" + pathname; host = "//" + host; } else if (protocol.length >= 4 && protocol.charCodeAt(0) === 102 && protocol.charCodeAt(1) === 105 && protocol.charCodeAt(2) === 108 && protocol.charCodeAt(3) === 101) { host = "//"; } } search = search.replace(/#/g, "%23"); if (hash && hash.charCodeAt(0) !== CHAR_HASH) hash = "#" + hash; if (search && search.charCodeAt(0) !== CHAR_QUESTION_MARK) search = "?" + search; return protocol + host + pathname + search + hash; } resolve(relative) { return this.resolveObject(urlParse(relative, false, true)).format(); } resolveObject(relative) { if (typeof relative === "string") { const rel = new Url(); rel.parse(relative, false, true); relative = rel; } const result = new Url(); Object.assign(result, this); result.hash = relative.hash; if (relative.href === "") { result.href = result.format(); return result; } if (relative.slashes && !relative.protocol) { const relativeWithoutProtocol = Object.keys(relative).reduce((acc, key) => { if (key !== "protocol") { acc[key] = relative[key]; } return acc; }, {}); Object.assign(result, relativeWithoutProtocol); if (slashedProtocol.has(result.protocol) && result.hostname && !result.pathname) { result.path = result.pathname = "/"; } result.href = result.format(); return result; } if (relative.protocol && relative.protocol !== result.protocol) { if (!slashedProtocol.has(relative.protocol)) { Object.assign(result, relative); result.href = result.format(); return result; } result.protocol = relative.protocol; if (!relative.host && !/^file:?$/.test(relative.protocol) && !hostlessProtocol.has(relative.protocol)) { const relPath = (relative.pathname || "").split("/"); while (relPath.length > 0 && !(relative.host = relPath.shift())); if (!relative.host) relative.host = ""; if (!relative.hostname) relative.hostname = ""; if (relPath[0] !== "") relPath.unshift(""); if (relPath.length < 2) relPath.unshift(""); result.pathname = relPath.join("/"); } else { result.pathname = relative.pathname; } result.search = relative.search; result.query = relative.query; result.host = relative.host || ""; result.auth = relative.auth; result.hostname = relative.hostname || relative.host; result.port = relative.port; if (result.pathname || result.search) { const p = result.pathname || ""; const s = result.search || ""; result.path = p + s; } result.slashes = result.slashes || relative.slashes; result.href = result.format(); return result; } const isSourceAbs = result.pathname && result.pathname.charAt(0) === "/"; const isRelAbs = relative.host || relative.pathname && relative.pathname.charAt(0) === "/"; let mustEndAbs = isRelAbs || isSourceAbs || result.host && relative.pathname; const removeAllDots = mustEndAbs; let srcPath = result.pathname && result.pathname.split("/") || []; const relPath = relative.pathname && relative.pathname.split("/") || []; const noLeadingSlashes = result.protocol && !slashedProtocol.has(result.protocol); if (noLeadingSlashes) { result.hostname = ""; result.port = null; if (result.host) { if (srcPath[0] === "") srcPath[0] = result.host; else srcPath.unshift(result.host); } result.host = ""; if (relative.protocol) { relative.hostname = null; relative.port = null; result.auth = null; if (relative.host) { if (relPath[0] === "") relPath[0] = relative.host; else relPath.unshift(relative.host); } relative.host = null; } mustEndAbs = mustEndAbs && (relPath[0] === "" || srcPath[0] === ""); } if (isRelAbs) { if (relative.host || relative.host === "") { if (result.host !== relative.host) result.auth = null; result.host = relative.host; result.port = relative.port; } if (relative.hostname || relative.hostname === "") { if (result.hostname !== relative.hostname) result.auth = null; result.hostname = relative.hostname; } result.search = relative.search; result.query = relative.query; srcPath = relPath; } else if (relPath.length > 0) { if (!srcPath) srcPath = []; srcPath.pop(); srcPath = srcPath.concat(relPath); result.search = relative.search; result.query = relative.query; } else if (relative.search !== null && relative.search !== undefined) { if (noLeadingSlashes) { result.hostname = result.host = srcPath.shift(); const authInHost = result.host && result.host.indexOf("@") > 0 && result.host.split("@"); if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); } } result.search = relative.search; result.query = relative.query; if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : "") + (result.search ? result.search : ""); } result.href = result.format(); return result; } if (srcPath.length === 0) { result.pathname = null; if (result.search) { result.path = "/" + result.search; } else { result.path = null; } result.href = result.format(); return result; } let last = srcPath.at(-1); const hasTrailingSlash = (result.host || relative.host || srcPath.length > 1) && (last === "." || last === "..") || last === ""; let up = 0; for (let i = srcPath.length - 1; i >= 0; i--) { last = srcPath[i]; if (last === ".") { spliceOne(srcPath, i); } else if (last === "..") { spliceOne(srcPath, i); up++; } else if (up) { spliceOne(srcPath, i); up--; } } if (!mustEndAbs && !removeAllDots) { while (up--) { srcPath.unshift(".."); } } if (mustEndAbs && srcPath[0] !== "" && (!srcPath[0] || srcPath[0].charAt(0) !== "/")) { srcPath.unshift(""); } if (hasTrailingSlash && srcPath.join("/").slice(-1) !== "/") { srcPath.push(""); } const isAbsolute = srcPath[0] === "" || srcPath[0] && srcPath[0].charAt(0) === "/"; if (noLeadingSlashes) { result.hostname = result.host = isAbsolute ? "" : srcPath.length > 0 ? srcPath.shift() : ""; const authInHost = result.host && result.host.indexOf("@") > 0 ? result.host.split("@") : false; if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); } } mustEndAbs = mustEndAbs || result.host && srcPath.length; if (mustEndAbs && !isAbsolute) { srcPath.unshift(""); } if (srcPath.length === 0) { result.pathname = null; result.path = null; } else { result.pathname = srcPath.join("/"); } if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : "") + (result.search ? result.search : ""); } result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; result.href = result.format(); return result; } parseHost() { let host = this.host; const portMatch = portPattern.exec(host); if (portMatch) { const port = portMatch[0]; if (port !== ":") { this.port = port.slice(1); } host = host.slice(0, host.length - port.length); } if (host) this.hostname = host; } } const protocolPattern = /^[\d+.a-z-]+:/i; const portPattern = /:\d*$/; const hostPattern = /^\/\/[^/@]+@[^/@]+/; const simplePathPattern = /^(\/\/?(?!\/)[^\s?]*)(\?\S*)?$/; const forbiddenHostChars = /[\0\t\n\r #%/:<>?@[\\\]^|]/; const forbiddenHostCharsIpv6 = /[\0\t\n\r #%/<>?@\\^|]/; const noEscapeAuth = new Int8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0 ]); const escapedCodes = [ "", "", "", "", "", "", "", "", "", "%09", "%0A", "", "", "%0D", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "%20", "", "%22", "", "", "", "", "%27", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "%3C", "", "%3E", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "%5C", "", "%5E", "", "%60", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "%7B", "%7C", "%7D" ]; const hostnameMaxLen = 255; let urlParseWarned = false; function urlParse(url, parseQueryString, slashesDenoteHost) { if (!urlParseWarned) { urlParseWarned = true; console.warn("[DeprecationWarning] [unenv] [node:url] DEP0169: `url.parse()` behavior is not standardized and prone to " + "errors that have security implications. Use the WHATWG URL API " + "instead. CVEs are not issued for `url.parse()` vulnerabilities."); } if (url instanceof Url) return url; const urlObject = new Url(); urlObject.parse(url, parseQueryString, slashesDenoteHost); return urlObject; } function isIpv6Hostname(hostname) { return String.prototype.charCodeAt.call(hostname, 0) === CHAR_LEFT_SQUARE_BRACKET && String.prototype.charCodeAt.call(hostname, hostname.length - 1) === CHAR_RIGHT_SQUARE_BRACKET; } let warnInvalidPort = true; function getHostname(self, rest, hostname, url) { for (let i = 0; i < hostname.length; ++i) { const code = hostname.charCodeAt(i); const isValid = code !== CHAR_FORWARD_SLASH && code !== CHAR_BACKWARD_SLASH && code !== CHAR_HASH && code !== CHAR_QUESTION_MARK && code !== CHAR_COLON; if (!isValid) { if (warnInvalidPort && code === CHAR_COLON) { console.warn(`[DeprecationWarning] [unenv] [node:url] DEP0170: The URL ${url} is invalid. Future versions of Node.js will throw an error.`); warnInvalidPort = false; } self.hostname = hostname.slice(0, i); return `/${hostname.slice(i)}${rest}`; } } return rest; } function autoEscapeStr(rest) { let escaped = ""; let lastEscapedPos = 0; for (let i = 0; i < rest.length; ++i) { const escapedChar = escapedCodes[rest.charCodeAt(i)]; if (escapedChar) { if (i > lastEscapedPos) escaped += rest.slice(lastEscapedPos, i); escaped += escapedChar; lastEscapedPos = i + 1; } } if (lastEscapedPos === 0) return rest; if (lastEscapedPos < rest.length) escaped += rest.slice(lastEscapedPos); return escaped; } function urlFormat(urlObject, options) { if (typeof urlObject === "string") { urlObject = urlParse(urlObject); } else if (typeof urlObject !== "object" || urlObject === null) { throw new ERR_INVALID_ARG_TYPE("urlObject", ["Object", "string"], urlObject); } else if (urlObject instanceof URL) { let fragment = true; let unicode = false; let search = true; let auth = true; if (options) { if (options.fragment != null) { fragment = Boolean(options.fragment); } if (options.unicode != null) { unicode = Boolean(options.unicode); } if (options.search != null) { search = Boolean(options.search); } if (options.auth != null) { auth = Boolean(options.auth); } } const _url = new URL(urlObject.href); if (!fragment) _url.hash = ""; if (!search) _url.search = ""; if (!auth) _url.username = _url.password = ""; if (unicode) { return Url.prototype.format.call(_url); } return _url.href; } return Url.prototype.format.call(urlObject); } function urlResolve(source, relative) { return urlParse(source, false, true).resolve(relative); } function urlResolveObject(source, relative) { if (!source) return relative; return urlParse(source, false, true).resolveObject(relative); } function pathToFileURL(path, options) { return _pathToFileURL(path, options); } const URL = globalThis.URL; const URLSearchParams = globalThis.URLSearchParams; const domainToASCII = punnycode.toASCII; const domainToUnicode = punnycode.toUnicode; export { Url, urlParse as parse, urlResolve as resolve, urlResolveObject as resolveObject, urlFormat as format, URL, URLSearchParams, domainToASCII, domainToUnicode, pathToFileURL, fileURLToPath, urlToHttpOptions }; export default { Url, parse: urlParse, resolve: urlResolve, resolveObject: urlResolveObject, format: urlFormat, URL, URLSearchParams, domainToASCII, domainToUnicode, pathToFileURL, fileURLToPath, urlToHttpOptions };