nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
1,264 lines (1,163 loc) • 32.9 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/url.js
import { toASCII } from "nstdlib/lib/internal/idna";
import { encodeStr, hexTable } from "nstdlib/lib/internal/querystring";
import * as querystring from "nstdlib/lib/querystring";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import {
validateString,
validateObject,
} from "nstdlib/lib/internal/validators";
import { spliceOne } from "nstdlib/lib/internal/util";
import {
URL,
URLSearchParams,
domainToASCII,
domainToUnicode,
fileURLToPath,
pathToFileURL as _pathToFileURL,
urlToHttpOptions,
unsafeProtocol,
hostlessProtocol,
slashedProtocol,
} from "nstdlib/lib/internal/url";
import * as bindingUrl from "nstdlib/stub/binding/url";
import { getOptionValue } from "nstdlib/lib/internal/options";
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 "nstdlib/lib/internal/constants";
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
const { ERR_INVALID_ARG_TYPE, ERR_INVALID_URL } = __codes__;
// This ensures setURLConstructor() is called before the native
// URL::ToObject() method is used.
// WHATWG URL implementation provided by internal/url
// Original url.parse() API
function Url() {
this.protocol = null;
this.slashes = null;
this.auth = null;
this.host = null;
this.port = null;
this.hostname = null;
this.hash = null;
this.search = null;
this.query = null;
this.pathname = null;
this.path = null;
this.href = null;
}
// Reference: RFC 3986, RFC 1808, RFC 2396
// define these here so at least they only have to be
// compiled once on the first module load.
const protocolPattern = /^[a-z0-9.+-]+:/i;
const portPattern = /:[0-9]*$/;
const hostPattern = /^\/\/[^@/]+@[^@/]+/;
// Special case for a simple path URL
const simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/;
const hostnameMaxLen = 255;
let urlParseWarned = false;
function urlParse(url, parseQueryString, slashesDenoteHost) {
if (!urlParseWarned && getOptionValue("--pending-deprecation")) {
urlParseWarned = true;
process.emitWarning(
"`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.",
"DeprecationWarning",
"DEP0169",
);
}
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
);
}
// This prevents some common spoofing bugs due to our use of IDNA toASCII. For
// compatibility, the set of characters we use here is the *intersection* of
// "forbidden host code point" in the WHATWG URL Standard [1] and the
// characters in the host parsing loop in Url.prototype.parse, with the
// following additions:
//
// - ':' since this could cause a "protocol spoofing" bug
// - '@' since this could cause parts of the hostname to be confused with auth
// - '[' and ']' since this could cause a non-IPv6 hostname to be interpreted
// as IPv6 by isIpv6Hostname above
//
// [1]: https://url.spec.whatwg.org/#forbidden-host-code-point
const forbiddenHostChars = /[\0\t\n\r #%/:<>?@[\\\]^|]/;
// For IPv6, permit '[', ']', and ':'.
const forbiddenHostCharsIpv6 = /[\0\t\n\r #%/<>?@\\^|]/;
Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
validateString(url, "url");
// Copy chrome, IE, opera backslash-handling behavior.
// Back slashes before the query string get converted to forward slashes
// See: https://code.google.com/p/chromium/issues/detail?id=25916
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);
// Find first and last non-whitespace characters for trimming
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;
}
// Only convert backslashes while we haven't seen a split character
if (!split) {
switch (code) {
case CHAR_AT:
hasAt = true;
break;
case CHAR_HASH:
hasHash = true;
// Fall through
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;
}
}
// Check if string was non-empty (including strings with only whitespace)
if (start !== -1) {
if (lastPos === start) {
// We didn't convert any backslashes
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) {
// We converted some backslashes and have only part of the entire string
rest += url.slice(lastPos);
} else if (end !== -1 && lastPos < end) {
// We converted some backslashes and have only part of the entire string
rest += url.slice(lastPos, end);
}
}
if (!slashesDenoteHost && !hasHash && !hasAt) {
// Try fast path regexp
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;
}
}
let proto = protocolPattern.exec(rest);
let lowerProto;
if (proto) {
proto = proto[0];
lowerProto = proto.toLowerCase();
this.protocol = lowerProto;
rest = rest.slice(proto.length);
}
// Figure out if it's got a host
// user@server is *always* interpreted as a hostname, and url
// resolution will treat //foo/bar as host=foo,path=bar because that's
// how the browser resolves relative URLs.
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)))
) {
// there's a hostname.
// the first instance of /, ?, ;, or # ends the host.
//
// If there is an @ in the hostname, then non-host chars *are* allowed
// to the left of the last @ sign, unless some host-ending character
// comes *before* the @-sign.
// URLs are obnoxious.
//
// ex:
// http://a@b@c/ => user:a@b host:c
// http://a@b?@c => user:a host:b path:/?@c
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:
// WHATWG URL removes tabs, newlines, and carriage returns. Let's do that too.
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:
// Characters that are never ever allowed in a hostname from RFC 2396
if (nonHost === -1) nonHost = i;
break;
case CHAR_HASH:
case CHAR_FORWARD_SLASH:
case CHAR_QUESTION_MARK:
// Find the first instance of any host-ending characters
if (nonHost === -1) nonHost = i;
hostEnd = i;
break;
case CHAR_AT:
// At this point, either we have an explicit point where the
// auth portion cannot go past, or the last @ char is the decider.
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);
}
// pull out port.
this.parseHost();
// We've indicated that there is a hostname,
// so even if it's empty, it has to be present.
if (typeof this.hostname !== "string") this.hostname = "";
const hostname = this.hostname;
// If hostname begins with [ and ends with ]
// assume that it's an IPv6 address.
const ipv6Hostname = isIpv6Hostname(hostname);
// validate a little.
if (!ipv6Hostname) {
rest = getHostname(this, rest, hostname, url);
}
if (this.hostname.length > hostnameMaxLen) {
this.hostname = "";
} else {
// Hostnames are always lower case.
this.hostname = this.hostname.toLowerCase();
}
if (this.hostname !== "") {
if (ipv6Hostname) {
if (forbiddenHostCharsIpv6.test(this.hostname)) {
throw new ERR_INVALID_URL(url);
}
} else {
// IDNA Support: Returns a punycoded representation of "domain".
// It only converts parts of the domain name that
// have non-ASCII characters, i.e. it doesn't matter if
// you call it with a domain that already is ASCII-only.
this.hostname = toASCII(this.hostname);
// Prevent two potential routes of hostname spoofing.
// 1. If this.hostname is empty, it must have become empty due to toASCII
// since we checked this.hostname above.
// 2. If any of forbiddenHostChars appears in this.hostname, it must have
// also gotten in due to toASCII. This is since getHostname would have
// filtered them out otherwise.
// Rather than trying to correct this by moving the non-host part into
// the pathname as we've done in getHostname, throw an exception to
// convey the severity of this issue.
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;
// strip [ and ] from the hostname
// the host field still retains them, though
if (ipv6Hostname) {
this.hostname = this.hostname.slice(1, -1);
if (rest[0] !== "/") {
rest = "/" + rest;
}
}
}
// Now rest is set to the post-host stuff.
// Chop off any delim chars.
if (!unsafeProtocol.has(lowerProto)) {
// First, make 100% sure that any "autoEscape" chars get
// escaped, even if encodeURIComponent doesn't think they
// need to be.
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) {
// No query string, but parseQueryString still requested
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 = "/";
}
// To support http.request
if (this.pathname || this.search) {
const p = this.pathname || "";
const s = this.search || "";
this.path = p + s;
}
// Finally, reconstruct the href based on what has been validated.
this.href = this.format();
return this;
};
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 leftover starts with :, then it represents an invalid port.
// But url.parse() is lenient about it for now.
// Issue a warning and continue.
if (warnInvalidPort && code === CHAR_COLON) {
const detail = `The URL ${url} is invalid. Future versions of Node.js will throw an error.`;
process.emitWarning(detail, "DeprecationWarning", "DEP0170");
warnInvalidPort = false;
}
self.hostname = hostname.slice(0, i);
return `/${hostname.slice(i)}${rest}`;
}
}
return rest;
}
// Escaped characters. Use empty strings to fill up unused entries.
// Using Array is faster than Object/Map
const escapedCodes = [
/* 0 - 9 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"%09",
/* 10 - 19 */ "%0A",
"",
"",
"%0D",
"",
"",
"",
"",
"",
"",
/* 20 - 29 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 30 - 39 */ "",
"",
"%20",
"",
"%22",
"",
"",
"",
"",
"%27",
/* 40 - 49 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 50 - 59 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 60 - 69 */ "%3C",
"",
"%3E",
"",
"",
"",
"",
"",
"",
"",
/* 70 - 79 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 80 - 89 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 90 - 99 */ "",
"",
"%5C",
"",
"%5E",
"",
"%60",
"",
"",
"",
/* 100 - 109 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 110 - 119 */ "",
"",
"",
"",
"",
"",
"",
"",
"",
"",
/* 120 - 125 */ "",
"",
"",
"%7B",
"%7C",
"%7D",
];
// Automatically escape all delimiters and unwise characters from RFC 2396.
// Also escape single quotes in case of an XSS attack.
// Return the escaped string.
function autoEscapeStr(rest) {
let escaped = "";
let lastEscapedPos = 0;
for (let i = 0; i < rest.length; ++i) {
// `escaped` contains substring up to the last escaped character.
const escapedChar = escapedCodes[rest.charCodeAt(i)];
if (escapedChar) {
// Concat if there are ordinary characters in the middle.
if (i > lastEscapedPos) escaped += rest.slice(lastEscapedPos, i);
escaped += escapedChar;
lastEscapedPos = i + 1;
}
}
if (lastEscapedPos === 0)
// Nothing has been escaped.
return rest;
// There are ordinary characters at the end.
if (lastEscapedPos < rest.length) escaped += rest.slice(lastEscapedPos);
return escaped;
}
// Format a parsed object into a url string
function urlFormat(urlObject, options) {
// Ensure it's an object, and not a string url.
// If it's an object, this is a no-op.
// this way, you can call urlParse() on strings
// to clean up potentially wonky urls.
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) {
validateObject(options, "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);
}
}
return bindingUrl.format(urlObject.href, fragment, unicode, search, auth);
}
return Url.prototype.format.call(urlObject);
}
// These characters do not need escaping:
// ! - . _ ~
// ' ( ) * :
// digits
// alpha (uppercase)
// alpha (lowercase)
const noEscapeAuth = new Int8Array([
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0, // 0x00 - 0x0F
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0, // 0x10 - 0x1F
0,
1,
0,
0,
0,
0,
0,
1,
1,
1,
1,
0,
0,
1,
1,
0, // 0x20 - 0x2F
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
0, // 0x30 - 0x3F
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1, // 0x40 - 0x4F
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1, // 0x50 - 0x5F
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1, // 0x60 - 0x6F
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
1,
0, // 0x70 - 0x7F
]);
Url.prototype.format = function 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 + pathname.slice(lastPos);
else pathname = newPathname;
}
// Only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
// unless they had them to begin with.
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 /* f */ &&
protocol.charCodeAt(1) === 105 /* i */ &&
protocol.charCodeAt(2) === 108 /* l */ &&
protocol.charCodeAt(3) === 101 /* e */
) {
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;
};
function urlResolve(source, relative) {
return urlParse(source, false, true).resolve(relative);
}
Url.prototype.resolve = function resolve(relative) {
return this.resolveObject(urlParse(relative, false, true)).format();
};
function urlResolveObject(source, relative) {
if (!source) return relative;
return urlParse(source, false, true).resolveObject(relative);
}
Url.prototype.resolveObject = function 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);
// Hash is always overridden, no matter what.
// even href="" will remove it.
result.hash = relative.hash;
// If the relative url is empty, then there's nothing left to do here.
if (relative.href === "") {
result.href = result.format();
return result;
}
// Hrefs like //foo/bar always cut to the protocol.
if (relative.slashes && !relative.protocol) {
// Take everything except the protocol from relative
const relativeWithoutProtocol = Object.keys(relative).reduce((acc, key) => {
if (key !== "protocol") {
acc[key] = relative[key];
}
return acc;
}, {});
Object.assign(result, relativeWithoutProtocol);
// urlParse appends trailing / to urls like http://www.example.com
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 it's a known url protocol, then changing
// the protocol does weird things
// first, if it's not file:, then we MUST have a host,
// and if there was a path
// to begin with, then we MUST have a path.
// if it is file:, then the host is dropped,
// because that's known to be hostless.
// anything else is assumed to be absolute.
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 && !(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;
// To support http.request
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 the url is a non-slashed url, then relative
// links like ../.. should be able
// to crawl up to the hostname, as well. This is strange.
// result.protocol has already been set by now.
// Later on, put the first path part into the host field.
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) {
// it's absolute.
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;
// Fall through to the dot-handling below.
} else if (relPath.length) {
// it's relative
// throw away the existing file, and take the new path instead.
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) {
// Just pull out the search.
// like href='?foo'.
// Put this after the other two cases because it simplifies the booleans
if (noLeadingSlashes) {
result.hostname = result.host = srcPath.shift();
// Occasionally the auth can get stuck only in host.
// This especially happens in cases like
// url.resolveObject('mailto:local1@domain1', 'local2@domain2')
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;
// To support http.request
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) {
// No path at all. All other things were already handled above.
result.pathname = null;
// To support http.request
if (result.search) {
result.path = "/" + result.search;
} else {
result.path = null;
}
result.href = result.format();
return result;
}
// If a url ENDs in . or .., then it must get a trailing slash.
// however, if it ends in anything else non-slashy,
// then it must NOT get a trailing slash.
let last = srcPath.slice(-1)[0];
const hasTrailingSlash =
((result.host || relative.host || srcPath.length > 1) &&
(last === "." || last === "..")) ||
last === "";
// Strip single dots, resolve double dots to parent dir
// if the path tries to go above the root, `up` ends up > 0
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 the path is allowed to go above the root, restore leading ..s
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) === "/");
// put the host back
if (noLeadingSlashes) {
result.hostname = result.host = isAbsolute
? ""
: srcPath.length
? srcPath.shift()
: "";
// Occasionally the auth can get stuck only in host.
// This especially happens in cases like
// url.resolveObject('mailto:local1@domain1', 'local2@domain2')
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) {
result.pathname = null;
result.path = null;
} else {
result.pathname = srcPath.join("/");
}
// To support request.http
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;
};
Url.prototype.parseHost = function parseHost() {
let host = this.host;
let port = portPattern.exec(host);
if (port) {
port = port[0];
if (port !== ":") {
this.port = port.slice(1);
}
host = host.slice(0, host.length - port.length);
}
if (host) this.hostname = host;
};
// When used internally, we are not obligated to associate TypeError with
// this function, so non-strings can be rejected by underlying implementation.
// Public API has to validate input and throw appropriate error.
function pathToFileURL(path, options) {
validateString(path, "path");
return _pathToFileURL(path, options);
}
export { Url };
export { urlParse as parse };
export { urlResolve as resolve };
export { urlResolveObject as resolveObject };
export { urlFormat as format };
export { URL };
export { URLSearchParams };
export { domainToASCII };
export { domainToUnicode };
export { pathToFileURL };
export { fileURLToPath };
export { urlToHttpOptions };