mirror of
https://github.com/nodejs/node.git
synced 2025-05-22 08:35:19 +00:00

The change aims to add value argument to two methods of URLSearchParams class i.e the has method and the delete method. For has method, if value argument is provided, then use it to check for presence. For delete method, if value argument provided, use it to delete. Fixes: https://github.com/nodejs/node/issues/47883 PR-URL: https://github.com/nodejs/node/pull/47885 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Debadree Chatterjee <debadree333@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1455 lines
40 KiB
JavaScript
1455 lines
40 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
Array,
|
|
ArrayIsArray,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeReduce,
|
|
ArrayPrototypeSlice,
|
|
Boolean,
|
|
Int8Array,
|
|
IteratorPrototype,
|
|
Number,
|
|
ObjectDefineProperties,
|
|
ObjectSetPrototypeOf,
|
|
ReflectGetOwnPropertyDescriptor,
|
|
ReflectOwnKeys,
|
|
RegExpPrototypeSymbolReplace,
|
|
SafeMap,
|
|
StringPrototypeCharAt,
|
|
StringPrototypeCharCodeAt,
|
|
StringPrototypeCodePointAt,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeIndexOf,
|
|
StringPrototypeSlice,
|
|
StringPrototypeStartsWith,
|
|
Symbol,
|
|
SymbolIterator,
|
|
SymbolToStringTag,
|
|
decodeURIComponent,
|
|
} = primordials;
|
|
|
|
const { inspect } = require('internal/util/inspect');
|
|
const {
|
|
encodeStr,
|
|
hexTable,
|
|
isHexTable,
|
|
} = require('internal/querystring');
|
|
|
|
const {
|
|
getConstructorOf,
|
|
removeColors,
|
|
toUSVString,
|
|
kEnumerableProperty,
|
|
SideEffectFreeRegExpPrototypeSymbolReplace,
|
|
} = require('internal/util');
|
|
|
|
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_AMPERSAND,
|
|
CHAR_BACKWARD_SLASH,
|
|
CHAR_EQUAL,
|
|
CHAR_FORWARD_SLASH,
|
|
CHAR_LOWERCASE_A,
|
|
CHAR_LOWERCASE_Z,
|
|
CHAR_PERCENT,
|
|
CHAR_PLUS,
|
|
} = require('internal/constants');
|
|
const path = require('path');
|
|
|
|
const {
|
|
validateFunction,
|
|
} = require('internal/validators');
|
|
|
|
const querystring = require('querystring');
|
|
|
|
const { platform } = process;
|
|
const isWindows = platform === 'win32';
|
|
|
|
const bindingUrl = internalBinding('url');
|
|
|
|
const FORWARD_SLASH = /\//g;
|
|
|
|
const contextForInspect = Symbol('context');
|
|
|
|
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;
|
|
}
|
|
|
|
// 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
|
|
class URLContext {
|
|
// This is the maximum value uint32_t can get.
|
|
// Ada uses uint32_t(-1) for declaring omitted values.
|
|
static #omitted = 4294967295;
|
|
|
|
href = '';
|
|
protocol_end = 0;
|
|
username_end = 0;
|
|
host_start = 0;
|
|
host_end = 0;
|
|
pathname_start = 0;
|
|
search_start = 0;
|
|
hash_start = 0;
|
|
port = 0;
|
|
/**
|
|
* Refers to `ada::scheme::type`
|
|
*
|
|
* enum type : uint8_t {
|
|
* HTTP = 0,
|
|
* NOT_SPECIAL = 1,
|
|
* HTTPS = 2,
|
|
* WS = 3,
|
|
* FTP = 4,
|
|
* WSS = 5,
|
|
* FILE = 6
|
|
* };
|
|
* @type {number}
|
|
*/
|
|
scheme_type = 1;
|
|
|
|
get hasPort() {
|
|
return this.port !== URLContext.#omitted;
|
|
}
|
|
|
|
get hasSearch() {
|
|
return this.search_start !== URLContext.#omitted;
|
|
}
|
|
|
|
get hasHash() {
|
|
return this.hash_start !== URLContext.#omitted;
|
|
}
|
|
}
|
|
|
|
let setURLSearchParamsContext;
|
|
let getURLSearchParamsList;
|
|
let setURLSearchParams;
|
|
|
|
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 {
|
|
value: undefined,
|
|
done: true,
|
|
};
|
|
}
|
|
|
|
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 {
|
|
value: result,
|
|
done: false,
|
|
};
|
|
}
|
|
|
|
[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 = ArrayPrototypeReduce(
|
|
ArrayPrototypeSlice(values, index),
|
|
(prev, cur, i) => {
|
|
const key = i % 2 === 0;
|
|
if (this.#kind === 'key' && key) {
|
|
ArrayPrototypePush(prev, cur);
|
|
} else if (this.#kind === 'value' && !key) {
|
|
ArrayPrototypePush(prev, cur);
|
|
} else if (this.#kind === 'key+value' && !key) {
|
|
ArrayPrototypePush(prev, [values[index + i - 1], cur]);
|
|
}
|
|
return prev;
|
|
},
|
|
[],
|
|
);
|
|
const breakLn = StringPrototypeIncludes(inspect(output, innerOpts), '\n');
|
|
const outputStrs = ArrayPrototypeMap(output, (p) => inspect(p, innerOpts));
|
|
let outputStr;
|
|
if (breakLn) {
|
|
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;
|
|
|
|
static {
|
|
setURLSearchParamsContext = (obj, ctx) => {
|
|
obj.#context = ctx;
|
|
};
|
|
getURLSearchParamsList = (obj) => obj.#searchParams;
|
|
setURLSearchParams = (obj, query) => {
|
|
if (query === undefined) {
|
|
obj.#searchParams = [];
|
|
} else {
|
|
obj.#searchParams = parseParams(query);
|
|
}
|
|
};
|
|
}
|
|
|
|
// 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) {
|
|
if (init == null) {
|
|
// Do nothing
|
|
} else if (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();
|
|
} else if (method != null) {
|
|
// Sequence<sequence<USVString>>
|
|
if (typeof method !== 'function') {
|
|
throw new ERR_ARG_NOT_ITERABLE('Query pairs');
|
|
}
|
|
|
|
// The following implementationd 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 querys 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.
|
|
const visited = new SafeMap();
|
|
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]);
|
|
|
|
// Two different keys may become the same USVString after normalization.
|
|
// In that case, we retain the later one. Refer to WPT.
|
|
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])}`);
|
|
|
|
const length = ArrayPrototypeReduce(
|
|
output,
|
|
(prev, cur) => prev + removeColors(cur).length + separator.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);
|
|
if (this.#context) {
|
|
this.#context.search = this.toString();
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if (value !== undefined) {
|
|
value = toUSVString(value);
|
|
for (let i = 0; i < list.length;) {
|
|
if (list[i] === name && list[i + 1] === value) {
|
|
list.splice(i, 2);
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
} else {
|
|
for (let i = 0; i < list.length;) {
|
|
if (list[i] === name) {
|
|
list.splice(i, 2);
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
}
|
|
if (this.#context) {
|
|
this.#context.search = this.toString();
|
|
}
|
|
}
|
|
|
|
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);
|
|
for (let i = 0; i < list.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);
|
|
for (let i = 0; i < list.length; i += 2) {
|
|
if (list[i] === name) {
|
|
values.push(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);
|
|
}
|
|
|
|
for (let i = 0; i < list.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);
|
|
|
|
// 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;
|
|
for (let i = 0; i < list.length;) {
|
|
const cur = list[i];
|
|
if (cur === name) {
|
|
if (!found) {
|
|
list[i + 1] = value;
|
|
found = true;
|
|
i += 2;
|
|
} else {
|
|
list.splice(i, 2);
|
|
}
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
|
|
// Otherwise, append a new name-value pair whose name is `name` and value
|
|
// is `value`, to `list`.
|
|
if (!found) {
|
|
ArrayPrototypePush(list, name, value);
|
|
}
|
|
|
|
if (this.#context) {
|
|
this.#context.search = this.toString();
|
|
}
|
|
}
|
|
|
|
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);
|
|
const rBuffer = 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, rBuffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.#context) {
|
|
this.#context.search = this.toString();
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
|
|
return serializeParams(this.#searchParams);
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
|
|
class URL {
|
|
#context = new URLContext();
|
|
#searchParams;
|
|
|
|
constructor(input, base = undefined) {
|
|
if (arguments.length === 0) {
|
|
throw new ERR_MISSING_ARGS('url');
|
|
}
|
|
|
|
// toUSVString is not needed.
|
|
input = `${input}`;
|
|
|
|
if (base !== undefined) {
|
|
base = `${base}`;
|
|
}
|
|
|
|
const href = bindingUrl.parse(input, base);
|
|
|
|
if (!href) {
|
|
throw new ERR_INVALID_URL(input);
|
|
}
|
|
|
|
this.#updateContext(href);
|
|
}
|
|
|
|
[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)}`;
|
|
}
|
|
|
|
#updateContext(href) {
|
|
this.#context.href = href;
|
|
|
|
const {
|
|
0: protocol_end,
|
|
1: username_end,
|
|
2: host_start,
|
|
3: host_end,
|
|
4: port,
|
|
5: pathname_start,
|
|
6: search_start,
|
|
7: hash_start,
|
|
8: scheme_type,
|
|
} = bindingUrl.urlComponents;
|
|
|
|
this.#context.protocol_end = protocol_end;
|
|
this.#context.username_end = username_end;
|
|
this.#context.host_start = host_start;
|
|
this.#context.host_end = host_end;
|
|
this.#context.port = port;
|
|
this.#context.pathname_start = pathname_start;
|
|
this.#context.search_start = search_start;
|
|
this.#context.hash_start = hash_start;
|
|
this.#context.scheme_type = scheme_type;
|
|
|
|
if (this.#searchParams) {
|
|
if (this.#context.hasSearch) {
|
|
setURLSearchParams(this.#searchParams, this.search);
|
|
} else {
|
|
setURLSearchParams(this.#searchParams, undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
return this.#context.href;
|
|
}
|
|
|
|
get href() {
|
|
return this.#context.href;
|
|
}
|
|
|
|
set href(value) {
|
|
value = `${value}`;
|
|
const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
|
|
if (!href) { throw ERR_INVALID_URL(value); }
|
|
this.#updateContext(href);
|
|
}
|
|
|
|
// 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);
|
|
if (out.#context.scheme_type !== 1) {
|
|
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() {
|
|
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);
|
|
}
|
|
|
|
set search(value) {
|
|
const href = bindingUrl.update(this.#context.href, updateActions.kSearch, toUSVString(value));
|
|
if (href) {
|
|
this.#updateContext(href);
|
|
}
|
|
}
|
|
|
|
// readonly
|
|
get searchParams() {
|
|
// Create URLSearchParams on demand to greatly improve the URL performance.
|
|
if (this.#searchParams == null) {
|
|
this.#searchParams = new URLSearchParams(this.search);
|
|
setURLSearchParamsContext(this.#searchParams, this);
|
|
}
|
|
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() {
|
|
return this.#context.href;
|
|
}
|
|
|
|
static canParse(url, base = undefined) {
|
|
if (arguments.length === 0) {
|
|
throw new ERR_MISSING_ARGS('url');
|
|
}
|
|
|
|
url = `${url}`;
|
|
|
|
if (base !== undefined) {
|
|
base = `${base}`;
|
|
}
|
|
|
|
return bindingUrl.canParse(url, base);
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|
|
|
|
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) {
|
|
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,
|
|
},
|
|
});
|
|
}
|
|
|
|
// application/x-www-form-urlencoded parser
|
|
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
|
|
function parseParams(qs) {
|
|
const out = [];
|
|
let seenSep = false;
|
|
let buf = '';
|
|
let encoded = false;
|
|
let encodeCheck = 0;
|
|
let i = qs[0] === '?' ? 1 : 0;
|
|
let pairStart = i;
|
|
let lastPos = i;
|
|
for (; i < qs.length; ++i) {
|
|
const code = StringPrototypeCharCodeAt(qs, i);
|
|
|
|
// Try matching key/value pair separator
|
|
if (code === CHAR_AMPERSAND) {
|
|
if (pairStart === i) {
|
|
// We saw an empty substring between pair separators
|
|
lastPos = pairStart = i + 1;
|
|
continue;
|
|
}
|
|
|
|
if (lastPos < i)
|
|
buf += qs.slice(lastPos, i);
|
|
if (encoded)
|
|
buf = querystring.unescape(buf);
|
|
out.push(buf);
|
|
|
|
// If `buf` is the key, add an empty value.
|
|
if (!seenSep)
|
|
out.push('');
|
|
|
|
seenSep = false;
|
|
buf = '';
|
|
encoded = false;
|
|
encodeCheck = 0;
|
|
lastPos = pairStart = i + 1;
|
|
continue;
|
|
}
|
|
|
|
// Try matching key/value separator (e.g. '=') if we haven't already
|
|
if (!seenSep && code === CHAR_EQUAL) {
|
|
// Key/value separator match!
|
|
if (lastPos < i)
|
|
buf += qs.slice(lastPos, i);
|
|
if (encoded)
|
|
buf = querystring.unescape(buf);
|
|
out.push(buf);
|
|
|
|
seenSep = true;
|
|
buf = '';
|
|
encoded = false;
|
|
encodeCheck = 0;
|
|
lastPos = i + 1;
|
|
continue;
|
|
}
|
|
|
|
// Handle + and percent decoding.
|
|
if (code === CHAR_PLUS) {
|
|
if (lastPos < i)
|
|
buf += StringPrototypeSlice(qs, lastPos, i);
|
|
buf += ' ';
|
|
lastPos = i + 1;
|
|
} else if (!encoded) {
|
|
// Try to match an (valid) encoded byte (once) to minimize unnecessary
|
|
// calls to string decoding functions
|
|
if (code === CHAR_PERCENT) {
|
|
encodeCheck = 1;
|
|
} else if (encodeCheck > 0) {
|
|
if (isHexTable[code] === 1) {
|
|
if (++encodeCheck === 3) {
|
|
encoded = true;
|
|
}
|
|
} else {
|
|
encodeCheck = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deal with any leftover key or value data
|
|
|
|
// There is a trailing &. No more processing is needed.
|
|
if (pairStart === i)
|
|
return out;
|
|
|
|
if (lastPos < i)
|
|
buf += StringPrototypeSlice(qs, lastPos, i);
|
|
if (encoded)
|
|
buf = querystring.unescape(buf);
|
|
ArrayPrototypePush(out, buf);
|
|
|
|
// If `buf` is the key, add an empty value.
|
|
if (!seenSep)
|
|
ArrayPrototypePush(out, '');
|
|
|
|
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 '';
|
|
|
|
const firstEncodedParam = encodeStr(array[0], noEscape, paramHexTable);
|
|
const firstEncodedValue = encodeStr(array[1], noEscape, paramHexTable);
|
|
let output = `${firstEncodedParam}=${firstEncodedValue}`;
|
|
|
|
for (let i = 2; i < len; i += 2) {
|
|
const encodedParam = encodeStr(array[i], noEscape, paramHexTable);
|
|
const encodedValue = encodeStr(array[i + 1], noEscape, paramHexTable);
|
|
output += `&${encodedParam}=${encodedValue}`;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
// for merge sort
|
|
function merge(out, start, mid, end, lBuffer, rBuffer) {
|
|
const sizeLeft = mid - start;
|
|
const sizeRight = end - mid;
|
|
let l, r, o;
|
|
|
|
for (l = 0; l < sizeLeft; l++)
|
|
lBuffer[l] = out[start + l];
|
|
for (r = 0; r < sizeRight; r++)
|
|
rBuffer[r] = out[mid + r];
|
|
|
|
l = 0;
|
|
r = 0;
|
|
o = start;
|
|
while (l < sizeLeft && r < sizeRight) {
|
|
if (lBuffer[l] <= rBuffer[r]) {
|
|
out[o++] = lBuffer[l++];
|
|
out[o++] = lBuffer[l++];
|
|
} else {
|
|
out[o++] = rBuffer[r++];
|
|
out[o++] = rBuffer[r++];
|
|
}
|
|
}
|
|
while (l < sizeLeft)
|
|
out[o++] = lBuffer[l++];
|
|
while (r < sizeRight)
|
|
out[o++] = rBuffer[r++];
|
|
}
|
|
|
|
function domainToASCII(domain) {
|
|
if (arguments.length < 1)
|
|
throw new ERR_MISSING_ARGS('domain');
|
|
|
|
// toUSVString is not needed.
|
|
return bindingUrl.domainToASCII(`${domain}`);
|
|
}
|
|
|
|
function domainToUnicode(domain) {
|
|
if (arguments.length < 1)
|
|
throw new ERR_MISSING_ARGS('domain');
|
|
|
|
// toUSVString 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) {
|
|
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 && StringPrototypeStartsWith(hostname, '[') ?
|
|
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',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
pathname = SideEffectFreeRegExpPrototypeSymbolReplace(FORWARD_SLASH, 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');
|
|
}
|
|
return StringPrototypeSlice(pathname, 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',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return decodeURIComponent(pathname);
|
|
}
|
|
|
|
function fileURLToPath(path) {
|
|
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 isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
|
|
}
|
|
|
|
// The following characters are percent-encoded when converting from file path
|
|
// to URL:
|
|
// - %: The percent character is the only character not encoded by the
|
|
// `pathname` setter.
|
|
// - \: Backslash is encoded on non-windows platforms since it's a valid
|
|
// character but the `pathname` setters replaces it by a forward slash.
|
|
// - LF: The newline character is stripped out by the `pathname` setter.
|
|
// (See whatwg/url#419)
|
|
// - CR: The carriage return character is also stripped out by the `pathname`
|
|
// setter.
|
|
// - TAB: The tab character is also stripped out by the `pathname` setter.
|
|
const percentRegEx = /%/g;
|
|
const backslashRegEx = /\\/g;
|
|
const newlineRegEx = /\n/g;
|
|
const carriageReturnRegEx = /\r/g;
|
|
const tabRegEx = /\t/g;
|
|
|
|
function encodePathChars(filepath) {
|
|
if (StringPrototypeIncludes(filepath, '%'))
|
|
filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25');
|
|
// In posix, backslash is a valid character in paths:
|
|
if (!isWindows && StringPrototypeIncludes(filepath, '\\'))
|
|
filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C');
|
|
if (StringPrototypeIncludes(filepath, '\n'))
|
|
filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A');
|
|
if (StringPrototypeIncludes(filepath, '\r'))
|
|
filepath = RegExpPrototypeSymbolReplace(carriageReturnRegEx, filepath, '%0D');
|
|
if (StringPrototypeIncludes(filepath, '\t'))
|
|
filepath = RegExpPrototypeSymbolReplace(tabRegEx, filepath, '%09');
|
|
return filepath;
|
|
}
|
|
|
|
function pathToFileURL(filepath) {
|
|
const outURL = new URL('file://');
|
|
if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) {
|
|
// UNC path format: \\server\share\resource
|
|
const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2);
|
|
if (hostnameEndIndex === -1) {
|
|
throw new ERR_INVALID_ARG_VALUE(
|
|
'filepath',
|
|
filepath,
|
|
'Missing UNC resource path',
|
|
);
|
|
}
|
|
if (hostnameEndIndex === 2) {
|
|
throw new ERR_INVALID_ARG_VALUE(
|
|
'filepath',
|
|
filepath,
|
|
'Empty UNC servername',
|
|
);
|
|
}
|
|
const hostname = StringPrototypeSlice(filepath, 2, hostnameEndIndex);
|
|
outURL.hostname = domainToASCII(hostname);
|
|
outURL.pathname = encodePathChars(
|
|
RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/'));
|
|
} else {
|
|
let resolved = path.resolve(filepath);
|
|
// path.resolve strips trailing slashes so we must add them back
|
|
const filePathLast = StringPrototypeCharCodeAt(filepath,
|
|
filepath.length - 1);
|
|
if ((filePathLast === CHAR_FORWARD_SLASH ||
|
|
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
|
|
resolved[resolved.length - 1] !== path.sep)
|
|
resolved += '/';
|
|
outURL.pathname = encodePathChars(resolved);
|
|
}
|
|
return outURL;
|
|
}
|
|
|
|
function toPathIfFileURL(fileURLOrPath) {
|
|
if (!isURL(fileURLOrPath))
|
|
return fileURLOrPath;
|
|
return fileURLToPath(fileURLOrPath);
|
|
}
|
|
|
|
module.exports = {
|
|
toUSVString,
|
|
fileURLToPath,
|
|
pathToFileURL,
|
|
toPathIfFileURL,
|
|
installObjectURLMethods,
|
|
URL,
|
|
URLSearchParams,
|
|
domainToASCII,
|
|
domainToUnicode,
|
|
urlToHttpOptions,
|
|
encodeStr,
|
|
isURL,
|
|
|
|
urlUpdateActions: updateActions,
|
|
};
|