Files
Roberto Bianchi f59539b514 url: skip plain prefixes in percent-escape checks
decodeFormComponent() already finds the first percent character, but
hasPercentHex() scans the plain prefix again. Start the check at the
known position to avoid repeating that work.

Replacing plus signs with spaces preserves character positions, so
escape validation and decoding behavior remain unchanged.

Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com>
PR-URL: https://github.com/nodejs/node/pull/66696
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-10-16 16:59:40 +00:00

1759 lines
54 KiB
JavaScript

'use strict';
const {
Array,
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
Boolean,
Int8Array,
IteratorPrototype,
Number,
ObjectDefineProperties,
ObjectSetPrototypeOf,
ReflectGetOwnPropertyDescriptor,
ReflectOwnKeys,
SafeMap,
SafeSet,
StringPrototypeCharAt,
StringPrototypeCharCodeAt,
StringPrototypeCodePointAt,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeToWellFormed,
Symbol,
SymbolIterator,
SymbolToStringTag,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetByteLength,
TypedArrayPrototypeGetByteOffset,
decodeURIComponent,
} = primordials;
const { URLPattern } = internalBinding('url_pattern');
const { inspect } = require('internal/util/inspect');
const {
encodeStr,
hexTable,
isHexTable,
} = require('internal/querystring');
const {
getConstructorOf,
removeColors,
kEnumerableProperty,
kEmptyObject,
SideEffectFreeRegExpPrototypeSymbolReplace,
isWindows,
} = require('internal/util');
const {
platform,
} = require('internal/process/per_thread');
const {
markTransferMode,
} = require('internal/worker/js_transferable');
const {
codes: {
ERR_ARG_NOT_ITERABLE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_FILE_URL_HOST,
ERR_INVALID_FILE_URL_PATH,
ERR_INVALID_THIS,
ERR_INVALID_TUPLE,
ERR_INVALID_URL,
ERR_INVALID_URL_SCHEME,
ERR_MISSING_ARGS,
ERR_NO_CRYPTO,
},
} = require('internal/errors');
const {
CHAR_BACKWARD_SLASH,
CHAR_FORWARD_SLASH,
CHAR_LOWERCASE_A,
CHAR_LOWERCASE_Z,
CHAR_COLON,
} = require('internal/constants');
const path = require('path');
const { Buffer } = require('buffer');
const {
validateFunction,
validateObject,
kValidateObjectAllowObjects,
} = require('internal/validators');
let percentDecode;
function lazyPercentDecode(input) {
percentDecode ??= require('internal/data_url').percentDecode;
return percentDecode(input);
}
const querystring = require('querystring');
const bindingUrl = internalBinding('url');
const FORWARD_SLASH = /\//g;
const contextForInspect = Symbol('context');
// `unsafeProtocol`, `hostlessProtocol` and `slashedProtocol` is
// deliberately moved to `internal/url` rather than `url`.
// Workers does not bootstrap URL module. Therefore, `SafeSet`
// is not initialized on bootstrap. This case breaks the
// test-require-delete-array-iterator test.
// Protocols that can allow "unsafe" and "unwise" chars.
const unsafeProtocol = new SafeSet([
'javascript',
'javascript:',
]);
// Protocols that never have a hostname.
const hostlessProtocol = new SafeSet([
'javascript',
'javascript:',
]);
// Protocols that always contain a // bit.
const slashedProtocol = new SafeSet([
'http',
'http:',
'https',
'https:',
'ftp',
'ftp:',
'gopher',
'gopher:',
'file',
'file:',
'ws',
'ws:',
'wss',
'wss:',
]);
const updateActions = {
kProtocol: 0,
kHost: 1,
kHostname: 2,
kPort: 3,
kUsername: 4,
kPassword: 5,
kPathname: 6,
kSearch: 7,
kHash: 8,
kHref: 9,
};
let blob;
let cryptoRandom;
function lazyBlob() {
blob ??= require('internal/blob');
return blob;
}
function lazyCryptoRandom() {
try {
cryptoRandom ??= require('internal/crypto/random');
} catch {
// If Node.js built without crypto support, we'll fall
// through here and handle it later.
}
return cryptoRandom;
}
/**
* Copy href and the latest `urlComponents` snapshot into a URLContext.
* Property assignment order matches the historical URLContext fields so
* `util.inspect(..., { showHidden: true })` stays stable.
* @param {object} ctx
* @param {string} href
*/
function setURLContextFromBinding(ctx, href) {
const c = bindingUrl.urlComponents;
ctx.href = href;
ctx.protocol_end = c[0];
ctx.username_end = c[1];
ctx.host_start = c[2];
ctx.host_end = c[3];
ctx.pathname_start = c[5];
ctx.search_start = c[6];
ctx.hash_start = c[7];
ctx.port = c[4];
ctx.scheme_type = c[8];
}
// This class provides the internal state of a URL object. An instance of this
// class is stored in every URL object and is accessed internally by setters
// and getters. It roughly corresponds to the concept of a URL record in the
// URL Standard, with a few differences. It is also the object transported to
// the C++ binding.
// Refs: https://url.spec.whatwg.org/#concept-url
//
// scheme_type refers to ada::scheme::type:
// HTTP = 0, NOT_SPECIAL = 1, HTTPS = 2, WS = 3, FTP = 4, WSS = 5, FILE = 6
class URLContext {
// This is the maximum value uint32_t can get.
// Ada uses uint32_t(-1) for declaring omitted values.
static #omitted = 4294967295;
/**
* @param {string} [href] Parsed href. When omitted, create an empty context
* (used by `URL.parse` on invalid input). When provided, `bindingUrl.parse`
* / `update` has just written `urlComponents`.
*/
constructor(href) {
if (href === undefined) {
this.href = '';
this.protocol_end = 0;
this.username_end = 0;
this.host_start = 0;
this.host_end = 0;
this.pathname_start = 0;
this.search_start = 0;
this.hash_start = 0;
this.port = 0;
this.scheme_type = 1;
return;
}
setURLContextFromBinding(this, href);
}
get hasPort() {
return this.port !== URLContext.#omitted;
}
get hasSearch() {
return this.search_start !== URLContext.#omitted;
}
get hasHash() {
return this.hash_start !== URLContext.#omitted;
}
}
let setURLSearchParamsModified;
let setURLSearchParamsContext;
let getURLSearchParamsList;
let setURLSearchParams;
/**
* Brand check for URL instances created by this realm's URL constructor.
*
* Unlike `isURL()`, which duck-types so that URL objects coming from other
* implementations are recognized, this cannot be satisfied by an ordinary
* object that merely exposes `href` and `protocol`. Use it where treating an
* attacker-supplied plain object as a URL would be a security concern.
* @type {(value: unknown) => value is URL}
*/
let isURLInstance;
/**
* Returns the canonical serialization of a URL from its private state.
* @type {(value: URL) => string}
*/
let getURLHref;
class URLSearchParamsIterator {
#target;
#kind;
#index;
// https://heycam.github.io/webidl/#dfn-default-iterator-object
constructor(target, kind) {
this.#target = target;
this.#kind = kind;
this.#index = 0;
}
next() {
if (typeof this !== 'object' || this === null || !(#target in this))
throw new ERR_INVALID_THIS('URLSearchParamsIterator');
const index = this.#index;
const values = getURLSearchParamsList(this.#target);
const len = values.length;
if (index >= len) {
return {
done: true,
value: undefined,
};
}
const name = values[index];
const value = values[index + 1];
this.#index = index + 2;
let result;
if (this.#kind === 'key') {
result = name;
} else if (this.#kind === 'value') {
result = value;
} else {
result = [name, value];
}
return {
done: false,
value: result,
};
}
[inspect.custom](recurseTimes, ctx) {
if (!this || typeof this !== 'object' || !(#target in this))
throw new ERR_INVALID_THIS('URLSearchParamsIterator');
if (typeof recurseTimes === 'number' && recurseTimes < 0)
return ctx.stylize('[Object]', 'special');
const innerOpts = { ...ctx };
if (recurseTimes !== null) {
innerOpts.depth = recurseTimes - 1;
}
const index = this.#index;
const values = getURLSearchParamsList(this.#target);
const output = [];
for (let i = index; i < values.length; i++) {
const isKey = ((i - index) % 2) === 0;
if (this.#kind === 'key') {
if (isKey) ArrayPrototypePush(output, values[i]);
} else if (!isKey) {
ArrayPrototypePush(output, this.#kind === 'value' ? values[i] : [values[i - 1], values[i]]);
}
}
const hasBreak = StringPrototypeIncludes(inspect(output, innerOpts), '\n');
const outputStrs = ArrayPrototypeMap(output, (p) => inspect(p, innerOpts));
let outputStr;
if (hasBreak) {
outputStr = `\n ${ArrayPrototypeJoin(outputStrs, ',\n ')}`;
} else {
outputStr = ` ${ArrayPrototypeJoin(outputStrs, ', ')}`;
}
return `${this[SymbolToStringTag]} {${outputStr} }`;
}
}
// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
delete URLSearchParamsIterator.prototype.constructor;
ObjectSetPrototypeOf(URLSearchParamsIterator.prototype, IteratorPrototype);
ObjectDefineProperties(URLSearchParamsIterator.prototype, {
[SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URLSearchParams Iterator' },
next: kEnumerableProperty,
});
class URLSearchParams {
#searchParams = [];
// "associated url object"
#context;
// Cached application/x-www-form-urlencoded serialization. Cleared on
// mutation so repeated toString()/URL.href reads stay cheap.
#serialized;
static {
setURLSearchParamsContext = (obj, ctx) => {
obj.#context = ctx;
};
getURLSearchParamsList = (obj) => obj.#searchParams;
setURLSearchParams = (obj, query) => {
obj.#serialized = undefined;
if (query === undefined) {
obj.#searchParams = [];
} else {
obj.#searchParams = parseParams(query);
}
};
}
#markUpdated() {
this.#serialized = undefined;
if (this.#context) {
setURLSearchParamsModified(this.#context);
}
}
// URL Standard says the default value is '', but as undefined and '' have
// the same result, undefined is used to prevent unnecessary parsing.
// Default parameter is necessary to keep URLSearchParams.length === 0 in
// accordance with Web IDL spec.
constructor(init = undefined) {
markTransferMode(this, false, false);
if (init === undefined) {
// Do nothing
} else if (init !== null && (typeof init === 'object' || typeof init === 'function')) {
const method = init[SymbolIterator];
if (method === this[SymbolIterator] && #searchParams in init) {
// While the spec does not have this branch, we can use it as a
// shortcut to avoid having to go through the costly generic iterator.
const childParams = init.#searchParams;
this.#searchParams = childParams.slice();
this.#serialized = init.#serialized;
} else if (method != null) {
// Sequence<sequence<USVString>>
if (typeof method !== 'function') {
throw new ERR_ARG_NOT_ITERABLE('Query pairs');
}
// The following implementation differs from the URL specification:
// Sequences must first be converted from ECMAScript objects before
// and operations are done on them, and the operation of converting
// the sequences would first exhaust the iterators. If the iterator
// returns something invalid in the middle, whether it would be called
// after that would be an observable change to the users.
// Exhausting the iterator and later converting them to USVString comes
// with a significant cost (~40-80%). In order optimize URLSearchParams
// creation duration, Node.js merges the iteration and converting
// iterations into a single iteration.
for (const pair of init) {
if (pair == null) {
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
} else if (ArrayIsArray(pair)) {
// If innerSequence's size is not 2, then throw a TypeError.
if (pair.length !== 2) {
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
}
// Append (innerSequence[0], innerSequence[1]) to query's list.
ArrayPrototypePush(
this.#searchParams,
toUSVString(pair[0]),
toUSVString(pair[1]),
);
} else {
if (((typeof pair !== 'object' && typeof pair !== 'function') ||
typeof pair[SymbolIterator] !== 'function')) {
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
}
let length = 0;
for (const element of pair) {
length++;
ArrayPrototypePush(this.#searchParams, toUSVString(element));
}
// If innerSequence's size is not 2, then throw a TypeError.
if (length !== 2) {
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
}
}
}
} else {
// Record<USVString, USVString>
// Need to use reflection APIs for full spec compliance.
// Object keys are unique, but USVString normalization may merge
// distinct keys. Lazily track normalized keys if a key changes.
let visited;
const keys = ReflectOwnKeys(init);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const desc = ReflectGetOwnPropertyDescriptor(init, key);
if (desc !== undefined && desc.enumerable) {
const typedKey = toUSVString(key);
const typedValue = toUSVString(init[key]);
if (visited === undefined && typedKey === key) {
ArrayPrototypePush(this.#searchParams, typedKey, typedValue);
continue;
}
if (visited === undefined) {
visited = new SafeMap();
for (let j = 0; j < this.#searchParams.length; j += 2) {
visited.set(this.#searchParams[j], j + 1);
}
}
const keyIdx = visited.get(typedKey);
if (keyIdx !== undefined) {
this.#searchParams[keyIdx] = typedValue;
} else {
visited.set(typedKey, ArrayPrototypePush(this.#searchParams,
typedKey,
typedValue) - 1);
}
}
}
}
} else {
// https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
init = toUSVString(init);
this.#searchParams = init ? parseParams(init) : [];
}
}
[inspect.custom](recurseTimes, ctx) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (typeof recurseTimes === 'number' && recurseTimes < 0)
return ctx.stylize('[Object]', 'special');
const separator = ', ';
const innerOpts = { ...ctx };
if (recurseTimes !== null) {
innerOpts.depth = recurseTimes - 1;
}
const innerInspect = (v) => inspect(v, innerOpts);
const list = this.#searchParams;
const output = [];
for (let i = 0; i < list.length; i += 2)
ArrayPrototypePush(
output,
`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);
let length = -separator.length;
for (let i = 0; i < output.length; i++) {
length += removeColors(output[i]).length + separator.length;
}
if (length > ctx.breakLength) {
return `${this.constructor.name} {\n` +
` ${ArrayPrototypeJoin(output, ',\n ')} }`;
} else if (output.length) {
return `${this.constructor.name} { ` +
`${ArrayPrototypeJoin(output, separator)} }`;
}
return `${this.constructor.name} {}`;
}
get size() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
return this.#searchParams.length / 2;
}
append(name, value) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('name', 'value');
}
name = toUSVString(name);
value = toUSVString(value);
ArrayPrototypePush(this.#searchParams, name, value);
this.#markUpdated();
}
delete(name, value = undefined) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 1) {
throw new ERR_MISSING_ARGS('name');
}
const list = this.#searchParams;
name = toUSVString(name);
const { length } = list;
let write = 0;
if (value !== undefined) {
value = toUSVString(value);
for (let i = 0; i < length; i += 2) {
if (list[i] === name && list[i + 1] === value) {
continue;
}
if (write !== i) {
list[write] = list[i];
list[write + 1] = list[i + 1];
}
write += 2;
}
} else {
for (let i = 0; i < length; i += 2) {
if (list[i] === name) {
continue;
}
if (write !== i) {
list[write] = list[i];
list[write + 1] = list[i + 1];
}
write += 2;
}
}
if (write !== length) {
list.length = write;
this.#serialized = undefined;
}
if (this.#context) {
setURLSearchParamsModified(this.#context);
}
}
get(name) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 1) {
throw new ERR_MISSING_ARGS('name');
}
const list = this.#searchParams;
name = toUSVString(name);
const { length } = list;
for (let i = 0; i < length; i += 2) {
if (list[i] === name) {
return list[i + 1];
}
}
return null;
}
getAll(name) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 1) {
throw new ERR_MISSING_ARGS('name');
}
const list = this.#searchParams;
const values = [];
name = toUSVString(name);
const { length } = list;
for (let i = 0; i < length; i += 2) {
if (list[i] === name) {
ArrayPrototypePush(values, list[i + 1]);
}
}
return values;
}
has(name, value = undefined) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 1) {
throw new ERR_MISSING_ARGS('name');
}
const list = this.#searchParams;
name = toUSVString(name);
if (value !== undefined) {
value = toUSVString(value);
}
const { length } = list;
for (let i = 0; i < length; i += 2) {
if (list[i] === name) {
if (value === undefined || list[i + 1] === value) {
return true;
}
}
}
return false;
}
set(name, value) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('name', 'value');
}
const list = this.#searchParams;
name = toUSVString(name);
value = toUSVString(value);
const { length } = list;
// If there are any name-value pairs whose name is `name`, in `list`, set
// the value of the first such name-value pair to `value` and remove the
// others.
let found = false;
let write = 0;
for (let i = 0; i < length; i += 2) {
const cur = list[i];
let keep = true;
if (cur === name) {
if (!found) {
list[write] = cur;
list[write + 1] = value;
found = true;
} else {
keep = false;
}
} else if (write !== i) {
list[write] = cur;
list[write + 1] = list[i + 1];
}
if (keep)
write += 2;
}
if (found && write !== length) {
list.length = write;
}
// Otherwise, append a new name-value pair whose name is `name` and value
// is `value`, to `list`.
if (!found) {
ArrayPrototypePush(list, name, value);
}
this.#markUpdated();
}
sort() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
const a = this.#searchParams;
const len = a.length;
if (len <= 2) {
// Nothing needs to be done.
} else if (len < 100) {
// 100 is found through testing.
// Simple stable in-place insertion sort
// Derived from v8/src/js/array.js
for (let i = 2; i < len; i += 2) {
const curKey = a[i];
const curVal = a[i + 1];
let j;
for (j = i - 2; j >= 0; j -= 2) {
if (a[j] > curKey) {
a[j + 2] = a[j];
a[j + 3] = a[j + 1];
} else {
break;
}
}
a[j + 2] = curKey;
a[j + 3] = curVal;
}
} else {
// Bottom-up iterative stable merge sort
const lBuffer = new Array(len);
for (let step = 2; step < len; step *= 2) {
for (let start = 0; start < len - 2; start += 2 * step) {
const mid = start + step;
let end = mid + step;
end = end < len ? end : len;
if (mid > end)
continue;
merge(a, start, mid, end, lBuffer);
}
}
}
this.#markUpdated();
}
// https://heycam.github.io/webidl/#es-iterators
// Define entries here rather than [Symbol.iterator] as the function name
// must be set to `entries`.
entries() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
return new URLSearchParamsIterator(this, 'key+value');
}
forEach(callback, thisArg = undefined) {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
validateFunction(callback, 'callback');
let list = this.#searchParams;
let i = 0;
while (i < list.length) {
const key = list[i];
const value = list[i + 1];
callback.call(thisArg, value, key, this);
// In case the URL object's `search` is updated
list = this.#searchParams;
i += 2;
}
}
// https://heycam.github.io/webidl/#es-iterable
keys() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
return new URLSearchParamsIterator(this, 'key');
}
values() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
return new URLSearchParamsIterator(this, 'value');
}
// https://heycam.github.io/webidl/#es-stringifier
// https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
toString() {
if (typeof this !== 'object' || this === null || !(#searchParams in this))
throw new ERR_INVALID_THIS('URLSearchParams');
if (this.#serialized !== undefined) {
return this.#serialized;
}
const serialized = serializeParams(this.#searchParams);
this.#serialized = serialized;
return serialized;
}
}
ObjectDefineProperties(URLSearchParams.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
getAll: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
size: kEnumerableProperty,
sort: kEnumerableProperty,
entries: kEnumerableProperty,
forEach: kEnumerableProperty,
keys: kEnumerableProperty,
values: kEnumerableProperty,
toString: kEnumerableProperty,
[SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URLSearchParams' },
// https://heycam.github.io/webidl/#es-iterable-entries
[SymbolIterator]: {
__proto__: null,
configurable: true,
writable: true,
value: URLSearchParams.prototype.entries,
},
});
/**
* Checks if a value has the shape of a WHATWG URL object.
*
* Using a symbol or instanceof would not be able to recognize URL objects
* coming from other implementations (e.g. in Electron), so instead we are
* checking some well known properties for a lack of a better test.
*
* We use `href` and `protocol` as they are the only properties that are
* easy to retrieve and calculate due to the lazy nature of the getters.
*
* We check for `auth` and `path` attribute to distinguish legacy url instance with
* WHATWG URL instance.
* @param {*} self
* @returns {self is URL}
*/
function isURL(self) {
return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
}
/**
* A unique symbol used as a private identifier to safely invoke the URL constructor
* with a special parsing behavior. When passed as the third argument to the URL
* constructor, it signals that the constructor should not throw an exception
* for invalid URL inputs.
*/
const kParseURLSymbol = Symbol('kParseURL');
const kCreateURLFromPosixPathSymbol = Symbol('kCreateURLFromPosixPath');
const kCreateURLFromWindowsPathSymbol = Symbol('kCreateURLFromWindowsPath');
class URL {
#context;
#searchParams;
#searchParamsModified;
static {
isURLInstance = (value) => typeof value === 'object' && value !== null && #context in value;
getURLHref = (value) => {
value.#ensureSearchParamsUpdated();
return value.#context.href;
};
setURLSearchParamsModified = (obj) => {
// When URLSearchParams changes, we lazily update URL on the next read/write for performance.
obj.#searchParamsModified = true;
// If URL has an existing search, remove it without cascading back to URLSearchParams.
// Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
if (obj.#context.hasSearch) {
obj.#updateContext(bindingUrl.update(obj.#context.href, updateActions.kSearch, ''));
}
};
}
constructor(input, base = undefined, parseSymbol = undefined) {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('url');
}
// StringPrototypeToWellFormed is not needed.
if (typeof input !== 'string') {
input = `${input}`;
}
if (base !== undefined && typeof base !== 'string') {
base = `${base}`;
}
let href;
if (arguments.length < 3) {
href = bindingUrl.parse(input, base, true);
} else {
const raiseException = parseSymbol !== kParseURLSymbol;
const interpretAsWindowsPath = parseSymbol === kCreateURLFromWindowsPathSymbol;
const pathToFileURL = interpretAsWindowsPath || (parseSymbol === kCreateURLFromPosixPathSymbol);
href = pathToFileURL ?
bindingUrl.pathToFileURL(input, interpretAsWindowsPath, base) :
bindingUrl.parse(input, base, raiseException);
}
// Delay context allocation until parse finishes so invalid URLs that
// throw do not pay for an unused URLContext. Initialize in one shot
// from the binding snapshot instead of writing an empty context first.
this.#context = href ? new URLContext(href) : new URLContext();
markTransferMode(this, false, false);
}
static parse(input, base = undefined) {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('url');
}
const parsedURLObject = new URL(input, base, kParseURLSymbol);
return parsedURLObject.href ? parsedURLObject : null;
}
[inspect.custom](depth, opts) {
if (typeof depth === 'number' && depth < 0)
return this;
const constructor = getConstructorOf(this) || URL;
const obj = { __proto__: { constructor } };
obj.href = this.href;
obj.origin = this.origin;
obj.protocol = this.protocol;
obj.username = this.username;
obj.password = this.password;
obj.host = this.host;
obj.hostname = this.hostname;
obj.port = this.port;
obj.pathname = this.pathname;
obj.search = this.search;
obj.searchParams = this.searchParams;
obj.hash = this.hash;
if (opts.showHidden) {
obj[contextForInspect] = this.#context;
}
return `${constructor.name} ${inspect(obj, opts)}`;
}
#getSearchFromContext() {
if (!this.#context.hasSearch) return '';
let endsAt = this.#context.href.length;
if (this.#context.hasHash) endsAt = this.#context.hash_start;
if (endsAt - this.#context.search_start <= 1) return '';
return StringPrototypeSlice(this.#context.href, this.#context.search_start, endsAt);
}
#getSearchFromParams() {
if (!this.#searchParams?.size) return '';
return `?${this.#searchParams}`;
}
#ensureSearchParamsUpdated() {
// URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
// If URLSearchParams has been modified, reflect that back into URL, without cascading back.
if (this.#searchParamsModified) {
this.#searchParamsModified = false;
this.#updateContext(bindingUrl.update(this.#context.href, updateActions.kSearch, this.#getSearchFromParams()));
}
}
/**
* Update the internal context state for URL.
* @param {string} href New href string from `bindingUrl.update`.
* @param {boolean} [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
*/
#updateContext(href, shouldUpdateSearchParams = false) {
const previousSearch = shouldUpdateSearchParams && this.#searchParams &&
(this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext());
setURLContextFromBinding(this.#context, href);
if (this.#searchParams) {
// If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
// Only do this when we're expecting it to have changed, otherwise a change to hash etc.
// would incorrectly compare the URLSearchParams state to the empty URL search state.
if (shouldUpdateSearchParams) {
const currentSearch = this.#getSearchFromContext();
if (previousSearch !== currentSearch) {
setURLSearchParams(this.#searchParams, currentSearch);
this.#searchParamsModified = false;
}
}
// If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
this.#ensureSearchParamsUpdated();
}
}
toString() {
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
this.#ensureSearchParamsUpdated();
return this.#context.href;
}
get href() {
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
this.#ensureSearchParamsUpdated();
return this.#context.href;
}
set href(value) {
value = `${value}`;
const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
if (!href) { throw new ERR_INVALID_URL(value); }
this.#updateContext(href, true);
}
// readonly
get origin() {
const protocol = StringPrototypeSlice(this.#context.href, 0, this.#context.protocol_end);
// Check if scheme_type is not `NOT_SPECIAL`
if (this.#context.scheme_type !== 1) {
// Check if scheme_type is `FILE`
if (this.#context.scheme_type === 6) {
return 'null';
}
return `${protocol}//${this.host}`;
}
if (protocol === 'blob:') {
const path = this.pathname;
if (path.length > 0) {
try {
const out = new URL(path);
// Only return origin of scheme is `http` or `https`
// Otherwise return a new opaque origin (null).
if (out.#context.scheme_type === 0 || out.#context.scheme_type === 2) {
return `${out.protocol}//${out.host}`;
}
} catch {
// Do nothing.
}
}
}
return 'null';
}
get protocol() {
return StringPrototypeSlice(this.#context.href, 0, this.#context.protocol_end);
}
set protocol(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kProtocol, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get username() {
if (this.#context.protocol_end + 2 < this.#context.username_end) {
return StringPrototypeSlice(this.#context.href, this.#context.protocol_end + 2, this.#context.username_end);
}
return '';
}
set username(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kUsername, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get password() {
if (this.#context.host_start - this.#context.username_end > 0) {
return StringPrototypeSlice(this.#context.href, this.#context.username_end + 1, this.#context.host_start);
}
return '';
}
set password(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kPassword, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get host() {
let startsAt = this.#context.host_start;
if (this.#context.href[startsAt] === '@') {
startsAt++;
}
// If we have an empty host, then the space between components.host_end and
// components.pathname_start may be occupied by /.
if (startsAt === this.#context.host_end) {
return '';
}
return StringPrototypeSlice(this.#context.href, startsAt, this.#context.pathname_start);
}
set host(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kHost, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get hostname() {
let startsAt = this.#context.host_start;
// host_start might be "@" if the URL has credentials
if (this.#context.href[startsAt] === '@') {
startsAt++;
}
return StringPrototypeSlice(this.#context.href, startsAt, this.#context.host_end);
}
set hostname(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kHostname, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get port() {
if (this.#context.hasPort) {
return `${this.#context.port}`;
}
return '';
}
set port(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kPort, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get pathname() {
let endsAt;
if (this.#context.hasSearch) {
endsAt = this.#context.search_start;
} else if (this.#context.hasHash) {
endsAt = this.#context.hash_start;
}
return StringPrototypeSlice(this.#context.href, this.#context.pathname_start, endsAt);
}
set pathname(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kPathname, `${value}`);
if (href) {
this.#updateContext(href);
}
}
get search() {
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
this.#ensureSearchParamsUpdated();
return this.#getSearchFromContext();
}
set search(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`));
if (href) {
this.#updateContext(href, true);
}
}
// readonly
get searchParams() {
// Create URLSearchParams on demand to greatly improve the URL performance.
if (this.#searchParams == null) {
this.#searchParams = new URLSearchParams(this.#getSearchFromContext());
setURLSearchParamsContext(this.#searchParams, this);
this.#searchParamsModified = false;
}
return this.#searchParams;
}
get hash() {
if (!this.#context.hasHash || (this.#context.href.length - this.#context.hash_start <= 1)) {
return '';
}
return StringPrototypeSlice(this.#context.href, this.#context.hash_start);
}
set hash(value) {
const href = bindingUrl.update(this.#context.href, updateActions.kHash, `${value}`);
if (href) {
this.#updateContext(href);
}
}
toJSON() {
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
this.#ensureSearchParamsUpdated();
return this.#context.href;
}
static canParse(url, base = undefined) {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('url');
}
if (typeof url !== 'string') {
url = `${url}`;
}
if (base !== undefined) {
return bindingUrl.canParse(url, typeof base === 'string' ? base : `${base}`);
}
// It is important to differentiate the canParse call statements
// since they resolve into different v8 fast api overloads.
return bindingUrl.canParse(url);
}
}
ObjectDefineProperties(URL.prototype, {
[SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URL' },
toString: kEnumerableProperty,
href: kEnumerableProperty,
origin: kEnumerableProperty,
protocol: kEnumerableProperty,
username: kEnumerableProperty,
password: kEnumerableProperty,
host: kEnumerableProperty,
hostname: kEnumerableProperty,
port: kEnumerableProperty,
pathname: kEnumerableProperty,
search: kEnumerableProperty,
searchParams: kEnumerableProperty,
hash: kEnumerableProperty,
toJSON: kEnumerableProperty,
});
ObjectDefineProperties(URL, {
canParse: {
__proto__: null,
configurable: true,
writable: true,
enumerable: true,
},
parse: {
__proto__: null,
configurable: true,
writable: true,
enumerable: true,
},
});
function installObjectURLMethods() {
const bindingBlob = internalBinding('blob');
function createObjectURL(obj) {
const cryptoRandom = lazyCryptoRandom();
if (cryptoRandom === undefined)
throw new ERR_NO_CRYPTO();
const blob = lazyBlob();
if (!blob.isBlob(obj))
throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj);
const id = cryptoRandom.randomUUID();
bindingBlob.storeDataObject(id, obj[blob.kHandle], obj.size, obj.type);
return `blob:nodedata:${id}`;
}
function revokeObjectURL(url) {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('url');
}
bindingBlob.revokeObjectURL(`${url}`);
}
ObjectDefineProperties(URL, {
createObjectURL: {
__proto__: null,
configurable: true,
writable: true,
enumerable: true,
value: createObjectURL,
},
revokeObjectURL: {
__proto__: null,
configurable: true,
writable: true,
enumerable: true,
value: revokeObjectURL,
},
});
}
function toUSVString(value) {
return typeof value === 'string' ?
StringPrototypeToWellFormed(value) :
StringPrototypeToWellFormed(`${value}`);
}
function unescapeFormComponent(s) {
try {
return decodeURIComponent(s);
} catch {
return querystring.unescapeBuffer(s).toString();
}
}
function hasPercentHex(s, start) {
const end = s.length - 2;
for (let i = start; i !== -1 && i < end;
i = StringPrototypeIndexOf(s, '%', i + 1)) {
if (isHexTable[StringPrototypeCharCodeAt(s, i + 1)] === 1 &&
isHexTable[StringPrototypeCharCodeAt(s, i + 2)] === 1) {
return true;
}
}
return false;
}
function decodeFormComponent(qs, start, end) {
if (start >= end) {
return '';
}
const s = qs.slice(start, end);
const plus = s.indexOf('+');
const pct = s.indexOf('%');
if (plus === -1 && pct === -1) {
return s;
}
const replaced = plus === -1 ? s : s.replaceAll('+', ' ');
// Only percent-decode when a complete %HH sequence exists. A lone '%' or
// a '%' followed by a non-hex character must be left intact so later
// serialization can encode the raw bytes.
if (pct === -1 || !hasPercentHex(replaced, pct)) {
return replaced;
}
return unescapeFormComponent(replaced);
}
// application/x-www-form-urlencoded parser
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
function parseParams(qs) {
const len = qs.length;
let i = qs[0] === '?' ? 1 : 0;
if (i >= len) {
return [];
}
const out = [];
// Native indexOf/slice/push outperform primordials on this tight loop.
const encoded = qs.indexOf('+', i) !== -1 || qs.indexOf('%', i) !== -1;
while (i < len) {
let amp = qs.indexOf('&', i);
if (amp === -1) {
amp = len;
}
if (amp !== i) {
const eq = qs.indexOf('=', i);
if (eq === -1 || eq > amp) {
out.push(encoded ? decodeFormComponent(qs, i, amp) : qs.slice(i, amp), '');
} else {
out.push(
encoded ? decodeFormComponent(qs, i, eq) : qs.slice(i, eq),
encoded ? decodeFormComponent(qs, eq + 1, amp) : qs.slice(eq + 1, amp),
);
}
}
i = amp + 1;
}
return out;
}
// Adapted from querystring's implementation.
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
const noEscape = new Int8Array([
/*
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
*/
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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 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, 0, 0, // 0x70 - 0x7F
]);
// Special version of hexTable that uses `+` for U+0020 SPACE.
const paramHexTable = hexTable.slice();
paramHexTable[0x20] = '+';
// application/x-www-form-urlencoded serializer
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
function serializeParams(array) {
const len = array.length;
if (len === 0)
return '';
if (len === 2) {
return encodeStr(array[0], noEscape, paramHexTable) + '=' +
encodeStr(array[1], noEscape, paramHexTable);
}
const pairs = new Array(len / 2);
for (let i = 0, j = 0; i < len; i += 2, ++j) {
pairs[j] = encodeStr(array[i], noEscape, paramHexTable) + '=' +
encodeStr(array[i + 1], noEscape, paramHexTable);
}
return ArrayPrototypeJoin(pairs, '&');
}
// for merge sort
function merge(out, start, mid, end, lBuffer) {
const sizeLeft = mid - start;
for (let index = 0; index < sizeLeft; index++)
lBuffer[index] = out[start + index];
let left = 0;
let right = mid;
let output = start;
while (left < sizeLeft && right < end) {
if (lBuffer[left] <= out[right]) {
out[output++] = lBuffer[left++];
out[output++] = lBuffer[left++];
} else {
out[output++] = out[right++];
out[output++] = out[right++];
}
}
while (left < sizeLeft)
out[output++] = lBuffer[left++];
}
function domainToASCII(domain) {
if (arguments.length < 1)
throw new ERR_MISSING_ARGS('domain');
// StringPrototypeToWellFormed is not needed.
return bindingUrl.domainToASCII(`${domain}`);
}
function domainToUnicode(domain) {
if (arguments.length < 1)
throw new ERR_MISSING_ARGS('domain');
// StringPrototypeToWellFormed is not needed.
return bindingUrl.domainToUnicode(`${domain}`);
}
/**
* Utility function that converts a URL object into an ordinary options object
* as expected by the `http.request` and `https.request` APIs.
* @param {URL} url
* @returns {Record<string, unknown>}
*/
function urlToHttpOptions(url) {
validateObject(url, 'url', kValidateObjectAllowObjects);
const { hostname, pathname, port, username, password, search } = url;
const options = {
__proto__: null,
...url, // In case the url object was extended by the user.
protocol: url.protocol,
hostname: hostname && hostname[0] === '[' ?
StringPrototypeSlice(hostname, 1, -1) :
hostname,
hash: url.hash,
search: search,
pathname: pathname,
path: `${pathname || ''}${search || ''}`,
href: url.href,
};
if (port !== '') {
options.port = Number(port);
}
if (username || password) {
options.auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`;
}
return options;
}
function getPathFromURLWin32(url) {
const hostname = url.hostname;
let pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === '%') {
const third = StringPrototypeCodePointAt(pathname, n + 2) | 0x20;
if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F /
(pathname[n + 1] === '5' && third === 99)) { // 5c 5C \
throw new ERR_INVALID_FILE_URL_PATH(
'must not include encoded \\ or / characters', url,
);
}
}
}
pathname = SideEffectFreeRegExpPrototypeSymbolReplace(FORWARD_SLASH, pathname, '\\');
// Fast-path: if there is no percent-encoding, avoid decodeURIComponent.
if (StringPrototypeIncludes(pathname, '%')) {
pathname = decodeURIComponent(pathname);
}
if (hostname !== '') {
// If hostname is set, then we have a UNC path
// Pass the hostname through domainToUnicode just in case
// it is an IDN using punycode encoding. We do not need to worry
// about percent encoding because the URL parser will have
// already taken care of that for us. Note that this only
// causes IDNs with an appropriate `xn--` prefix to be decoded.
return `\\\\${domainToUnicode(hostname)}${pathname}`;
}
// Otherwise, it's a local path that requires a drive letter
const letter = StringPrototypeCodePointAt(pathname, 1) | 0x20;
const sep = StringPrototypeCharAt(pathname, 2);
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z
(sep !== ':')) {
throw new ERR_INVALID_FILE_URL_PATH('must be absolute', url);
}
return StringPrototypeSlice(pathname, 1);
}
function getPathBufferFromURLWin32(url) {
const hostname = url.hostname;
let pathname = url.pathname;
// In the getPathFromURLWin32 variant, we scan the input for backslash (\)
// and forward slash (/) characters, specifically looking for the ASCII/UTF8
// encoding these and forbidding their use. This is a bit tricky
// because these may conflict with non-UTF8 encodings. For instance,
// in shift-jis, %5C identifies the symbol for the Japanese Yen and not the
// backslash. If we have a url like file:///foo/%5c/bar, then we really have
// no way of knowing if that %5c is meant to be a backslash \ or a yen sign.
// Passing in an encoding option does not help since our Buffer encoding only
// knows about certain specific text encodings and a single file path might
// actually contain segments that use multiple encodings. It's tricky! So,
// for this variation where we are producing a buffer, we won't scan for the
// slashes at all, and instead will decode the bytes literally into the
// returned Buffer. That said, that can also be tricky because, on windows,
// the file path separator *is* the ASCII backslash. This is a known issue
// on windows specific to the Shift-JIS encoding that we're not really going
// to solve here. Instead, we're going to do the best we can and just
// interpret the input url as a sequence of bytes.
// Because we are converting to a Windows file path here, we need to replace
// the explicit forward slash separators with backslashes. Note that this
// intentionally disregards any percent-encoded forward slashes in the path.
pathname = SideEffectFreeRegExpPrototypeSymbolReplace(FORWARD_SLASH, pathname, '\\');
// Now, let's start to build our Buffer. We will initially start with a
// Buffer allocated to fit in the entire string. Worst case there are no
// percent encoded characters and we take the string as is. Any invalid
// percent encodings, e.g. `%ZZ` are ignored and are passed through
// literally.
const decodedu8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
const decodedPathname = Buffer.from(TypedArrayPrototypeGetBuffer(decodedu8),
TypedArrayPrototypeGetByteOffset(decodedu8),
TypedArrayPrototypeGetByteLength(decodedu8));
if (hostname !== '') {
// If hostname is set, then we have a UNC path
// Pass the hostname through domainToUnicode just in case
// it is an IDN using punycode encoding. We do not need to worry
// about percent encoding because the URL parser will have
// already taken care of that for us. Note that this only
// causes IDNs with an appropriate `xn--` prefix to be decoded.
// This is a bit tricky because of the need to convert to a Buffer
// followed by concatenation of the results.
const prefix = Buffer.from('\\\\', 'ascii');
const domain = Buffer.from(domainToUnicode(hostname), 'utf8');
return Buffer.concat([prefix, domain, decodedPathname]);
}
// Otherwise, it's a local path that requires a drive letter
// In this case we're only going to pay attention to the second and
// third bytes in the decodedPathname. If first byte is either an ASCII
// uppercase letter between 'A' and 'Z' or lowercase letter between
// 'a' and 'z', and the second byte must be an ASCII `:` or the
// operation will fail.
const letter = decodedPathname[1] | 0x20;
const sep = decodedPathname[2];
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z
(sep !== CHAR_COLON)) {
throw new ERR_INVALID_FILE_URL_PATH('must be absolute', url);
}
// Now, we'll just return everything except the first byte of
// decodedPathname
return decodedPathname.subarray(1);
}
function getPathFromURLPosix(url) {
if (url.hostname !== '') {
throw new ERR_INVALID_FILE_URL_HOST(platform);
}
const pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === '%') {
const third = StringPrototypeCodePointAt(pathname, n + 2) | 0x20;
if (pathname[n + 1] === '2' && third === 102) {
throw new ERR_INVALID_FILE_URL_PATH(
'must not include encoded / characters',
url,
);
}
}
}
// Fast-path: if there is no percent-encoding, avoid decodeURIComponent.
return StringPrototypeIncludes(pathname, '%') ? decodeURIComponent(pathname) : pathname;
}
function getPathBufferFromURLPosix(url) {
if (url.hostname !== '') {
throw new ERR_INVALID_FILE_URL_HOST(platform);
}
const pathname = url.pathname;
// In the getPathFromURLPosix variant, we scan the input for forward slash
// (/) characters, specifically looking for the ASCII/UTF8 and forbidding
// its use. This is a bit tricky because these may conflict with non-UTF8
// encodings. Passing in an encoding option does not help since our Buffer
// encoding only knows about certain specific text encodings and a single
// file path might actually contain segments that use multiple encodings.
// It's tricky! So, for this variation where we are producing a buffer, we
// won't scan for the slashes at all, and instead will decode the bytes
// literally into the returned Buffer. We're going to do the best we can and
// just interpret the input url as a sequence of bytes.
const u8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
return Buffer.from(TypedArrayPrototypeGetBuffer(u8),
TypedArrayPrototypeGetByteOffset(u8),
TypedArrayPrototypeGetByteLength(u8));
}
function fileURLToPath(path, options = kEmptyObject) {
const windows = options?.windows;
if (typeof path === 'string')
path = new URL(path);
else if (!isURL(path))
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
if (path.protocol !== 'file:')
throw new ERR_INVALID_URL_SCHEME('file');
return (windows ?? isWindows) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
// An alternative to fileURLToPath that outputs a Buffer
// instead of a string. The other fileURLToPath does not
// handle non-UTF8 encoded percent encodings at all, so
// converting to a Buffer is necessary in cases where the
// to string conversion would fail.
function fileURLToPathBuffer(path, options = kEmptyObject) {
const windows = options?.windows;
if (typeof path === 'string') {
path = new URL(path);
} else if (!isURL(path)) {
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
}
if (path.protocol !== 'file:') {
throw new ERR_INVALID_URL_SCHEME('file');
}
return (windows ?? isWindows) ? getPathBufferFromURLWin32(path) : getPathBufferFromURLPosix(path);
}
function pathToFileURL(filepath, options = kEmptyObject) {
const windows = options?.windows ?? isWindows;
const isUNC = windows && StringPrototypeStartsWith(filepath, '\\\\');
let resolved = isUNC ?
filepath :
(windows ? path.win32.resolve(filepath) : path.posix.resolve(filepath));
if (isUNC || (windows && StringPrototypeStartsWith(resolved, '\\\\'))) {
// UNC path format: \\server\share\resource
// Handle extended UNC path and standard UNC path
// "\\?\UNC\" path prefix should be ignored.
// Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
const isExtendedUNC = StringPrototypeStartsWith(resolved, '\\\\?\\UNC\\');
const prefixLength = isExtendedUNC ? 8 : 2;
const hostnameEndIndex = StringPrototypeIndexOf(resolved, '\\', prefixLength);
if (hostnameEndIndex === -1) {
throw new ERR_INVALID_ARG_VALUE(
'path',
resolved,
'Missing UNC resource path',
);
}
if (hostnameEndIndex === 2) {
throw new ERR_INVALID_ARG_VALUE(
'path',
resolved,
'Empty UNC servername',
);
}
const hostname = StringPrototypeSlice(resolved, prefixLength, hostnameEndIndex);
return new URL(StringPrototypeSlice(resolved, hostnameEndIndex), hostname, kCreateURLFromWindowsPathSymbol);
}
// path.resolve strips trailing slashes so we must add them back
const filePathLast = StringPrototypeCharCodeAt(filepath,
filepath.length - 1);
if ((filePathLast === CHAR_FORWARD_SLASH ||
((windows ?? isWindows) && filePathLast === CHAR_BACKWARD_SLASH)) &&
resolved[resolved.length - 1] !== path.sep)
resolved += '/';
return new URL(resolved, undefined, windows ? kCreateURLFromWindowsPathSymbol : kCreateURLFromPosixPathSymbol);
}
function toPathIfFileURL(fileURLOrPath) {
if (!isURL(fileURLOrPath))
return fileURLOrPath;
return fileURLToPath(fileURLOrPath);
}
/**
* This util takes a string containing a URL and return the URL origin,
* its meant to avoid calls to `new URL` constructor.
* @param {string} url
* @returns {URL['origin']}
*/
function getURLOrigin(url) {
return bindingUrl.getOrigin(url);
}
module.exports = {
fileURLToPath,
fileURLToPathBuffer,
pathToFileURL,
toPathIfFileURL,
installObjectURLMethods,
URL,
URLPattern,
URLSearchParams,
URLParse: URL.parse,
domainToASCII,
domainToUnicode,
urlToHttpOptions,
encodeStr,
isURL,
isURLInstance,
getURLHref,
urlUpdateActions: updateActions,
getURLOrigin,
unsafeProtocol,
hostlessProtocol,
slashedProtocol,
};