url: use private properties for brand check

PR-URL: https://github.com/nodejs/node/pull/46904
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
Yagiz Nizipli 2023-03-03 16:34:56 -05:00 committed by GitHub
parent 5a7491b5ac
commit 027841c964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 117 deletions

View File

@ -79,7 +79,7 @@ const { BuiltinModule } = require('internal/bootstrap/loaders');
const { const {
maybeCacheSourceMap, maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache'); } = require('internal/source_map/source_map_cache');
const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); const { pathToFileURL, fileURLToPath, isURL } = require('internal/url');
const { const {
deprecate, deprecate,
emitExperimentalWarning, emitExperimentalWarning,
@ -1396,7 +1396,7 @@ const createRequireError = 'must be a file URL object, file URL string, or ' +
function createRequire(filename) { function createRequire(filename) {
let filepath; let filepath;
if (isURLInstance(filename) || if (isURL(filename) ||
(typeof filename === 'string' && !path.isAbsolute(filename))) { (typeof filename === 'string' && !path.isAbsolute(filename))) {
try { try {
filepath = fileURLToPath(filename); filepath = fileURLToPath(filename);

View File

@ -21,7 +21,7 @@ const {
ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE, ERR_INVALID_RETURN_VALUE,
} = require('internal/errors').codes; } = require('internal/errors').codes;
const { isURLInstance, URL } = require('internal/url'); const { isURL, URL } = require('internal/url');
const { const {
isAnyArrayBuffer, isAnyArrayBuffer,
isArrayBufferView, isArrayBufferView,
@ -263,7 +263,7 @@ class Hooks {
if ( if (
!isMain && !isMain &&
typeof parentURL !== 'string' && typeof parentURL !== 'string' &&
!isURLInstance(parentURL) !isURL(parentURL)
) { ) {
throw new ERR_INVALID_ARG_TYPE( throw new ERR_INVALID_ARG_TYPE(
'parentURL', 'parentURL',

View File

@ -7,6 +7,7 @@ const {
ArrayPrototypePush, ArrayPrototypePush,
ArrayPrototypeReduce, ArrayPrototypeReduce,
ArrayPrototypeSlice, ArrayPrototypeSlice,
Boolean,
Int8Array, Int8Array,
IteratorPrototype, IteratorPrototype,
Number, Number,
@ -15,7 +16,6 @@ const {
ObjectGetOwnPropertySymbols, ObjectGetOwnPropertySymbols,
ObjectGetPrototypeOf, ObjectGetPrototypeOf,
ObjectKeys, ObjectKeys,
ObjectPrototypeHasOwnProperty,
ReflectGetOwnPropertyDescriptor, ReflectGetOwnPropertyDescriptor,
ReflectOwnKeys, ReflectOwnKeys,
RegExpPrototypeSymbolReplace, RegExpPrototypeSymbolReplace,
@ -534,15 +534,27 @@ ObjectDefineProperties(URLSearchParams.prototype, {
}, },
}); });
/**
* 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.
*
* @param {*} self
* @returns {self is URL}
*/
function isURL(self) { function isURL(self) {
return self != null && ObjectPrototypeHasOwnProperty(self, context); return Boolean(self?.href && self.origin);
} }
class URL { class URL {
#context = new URLContext();
#searchParams;
constructor(input, base = undefined) { constructor(input, base = undefined) {
// toUSVString is not needed. // toUSVString is not needed.
input = `${input}`; input = `${input}`;
this[context] = new URLContext();
if (base !== undefined) { if (base !== undefined) {
base = `${base}`; base = `${base}`;
@ -558,11 +570,6 @@ class URL {
} }
[inspect.custom](depth, opts) { [inspect.custom](depth, opts) {
if (this == null ||
ObjectGetPrototypeOf(this[context]) !== URLContext.prototype) {
throw new ERR_INVALID_THIS('URL');
}
if (typeof depth === 'number' && depth < 0) if (typeof depth === 'number' && depth < 0)
return this; return this;
@ -583,7 +590,7 @@ class URL {
obj.hash = this.hash; obj.hash = this.hash;
if (opts.showHidden) { if (opts.showHidden) {
obj[context] = this[context]; obj[context] = this.#context;
} }
return `${constructor.name} ${inspect(obj, opts)}`; return `${constructor.name} ${inspect(obj, opts)}`;
@ -591,174 +598,125 @@ class URL {
#onParseComplete = (href, origin, protocol, hostname, pathname, #onParseComplete = (href, origin, protocol, hostname, pathname,
search, username, password, port, hash) => { search, username, password, port, hash) => {
const ctx = this[context]; this.#context.href = href;
ctx.href = href; this.#context.origin = origin;
ctx.origin = origin; this.#context.protocol = protocol;
ctx.protocol = protocol; this.#context.hostname = hostname;
ctx.hostname = hostname; this.#context.pathname = pathname;
ctx.pathname = pathname; this.#context.search = search;
ctx.search = search; this.#context.username = username;
ctx.username = username; this.#context.password = password;
ctx.password = password; this.#context.port = port;
ctx.port = port; this.#context.hash = hash;
ctx.hash = hash; if (this.#searchParams) {
if (this[searchParams]) { this.#searchParams[searchParams] = parseParams(search);
this[searchParams][searchParams] = parseParams(search);
} }
}; };
toString() { toString() {
if (!isURL(this)) return this.#context.href;
throw new ERR_INVALID_THIS('URL');
return this[context].href;
} }
get href() { get href() {
if (!isURL(this)) return this.#context.href;
throw new ERR_INVALID_THIS('URL');
return this[context].href;
} }
set href(value) { set href(value) {
if (!isURL(this)) const valid = updateUrl(this.#context.href, updateActions.kHref, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
const valid = updateUrl(this[context].href, updateActions.kHref, `${value}`, this.#onParseComplete);
if (!valid) { throw ERR_INVALID_URL(`${value}`); } if (!valid) { throw ERR_INVALID_URL(`${value}`); }
} }
// readonly // readonly
get origin() { get origin() {
if (!isURL(this)) return this.#context.origin;
throw new ERR_INVALID_THIS('URL');
return this[context].origin;
} }
get protocol() { get protocol() {
if (!isURL(this)) return this.#context.protocol;
throw new ERR_INVALID_THIS('URL');
return this[context].protocol;
} }
set protocol(value) { set protocol(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kProtocol, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kProtocol, `${value}`, this.#onParseComplete);
} }
get username() { get username() {
if (!isURL(this)) return this.#context.username;
throw new ERR_INVALID_THIS('URL');
return this[context].username;
} }
set username(value) { set username(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kUsername, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kUsername, `${value}`, this.#onParseComplete);
} }
get password() { get password() {
if (!isURL(this)) return this.#context.password;
throw new ERR_INVALID_THIS('URL');
return this[context].password;
} }
set password(value) { set password(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kPassword, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPassword, `${value}`, this.#onParseComplete);
} }
get host() { get host() {
if (!isURL(this)) const port = this.#context.port;
throw new ERR_INVALID_THIS('URL');
const port = this[context].port;
const suffix = port.length > 0 ? `:${port}` : ''; const suffix = port.length > 0 ? `:${port}` : '';
return this[context].hostname + suffix; return this.#context.hostname + suffix;
} }
set host(value) { set host(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kHost, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHost, `${value}`, this.#onParseComplete);
} }
get hostname() { get hostname() {
if (!isURL(this)) return this.#context.hostname;
throw new ERR_INVALID_THIS('URL');
return this[context].hostname;
} }
set hostname(value) { set hostname(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kHostname, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHostname, `${value}`, this.#onParseComplete);
} }
get port() { get port() {
if (!isURL(this)) return this.#context.port;
throw new ERR_INVALID_THIS('URL');
return this[context].port;
} }
set port(value) { set port(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kPort, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPort, `${value}`, this.#onParseComplete);
} }
get pathname() { get pathname() {
if (!isURL(this)) return this.#context.pathname;
throw new ERR_INVALID_THIS('URL');
return this[context].pathname;
} }
set pathname(value) { set pathname(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kPathname, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kPathname, `${value}`, this.#onParseComplete);
} }
get search() { get search() {
if (!isURL(this)) return this.#context.search;
throw new ERR_INVALID_THIS('URL');
return this[context].search;
} }
set search(value) { set search(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kSearch, toUSVString(value), this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kSearch, toUSVString(value), this.#onParseComplete);
} }
// readonly // readonly
get searchParams() { get searchParams() {
if (!isURL(this))
throw new ERR_INVALID_THIS('URL');
// Create URLSearchParams on demand to greatly improve the URL performance. // Create URLSearchParams on demand to greatly improve the URL performance.
if (this[searchParams] == null) { if (this.#searchParams == null) {
this[searchParams] = new URLSearchParams(this[context].search); this.#searchParams = new URLSearchParams(this.#context.search);
this[searchParams][context] = this; this.#searchParams[context] = this;
} }
return this[searchParams]; return this.#searchParams;
} }
get hash() { get hash() {
if (!isURL(this)) return this.#context.hash;
throw new ERR_INVALID_THIS('URL');
return this[context].hash;
} }
set hash(value) { set hash(value) {
if (!isURL(this)) updateUrl(this.#context.href, updateActions.kHash, `${value}`, this.#onParseComplete);
throw new ERR_INVALID_THIS('URL');
updateUrl(this[context].href, updateActions.kHash, `${value}`, this.#onParseComplete);
} }
toJSON() { toJSON() {
if (!isURL(this)) return this.#context.href;
throw new ERR_INVALID_THIS('URL');
return this[context].href;
} }
static createObjectURL(obj) { static createObjectURL(obj) {
@ -1206,7 +1164,7 @@ function getPathFromURLPosix(url) {
function fileURLToPath(path) { function fileURLToPath(path) {
if (typeof path === 'string') if (typeof path === 'string')
path = new URL(path); path = new URL(path);
else if (!isURLInstance(path)) else if (!isURL(path))
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path); throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
if (path.protocol !== 'file:') if (path.protocol !== 'file:')
throw new ERR_INVALID_URL_SCHEME('file'); throw new ERR_INVALID_URL_SCHEME('file');
@ -1282,12 +1240,8 @@ function pathToFileURL(filepath) {
return outURL; return outURL;
} }
function isURLInstance(fileURLOrPath) {
return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin;
}
function toPathIfFileURL(fileURLOrPath) { function toPathIfFileURL(fileURLOrPath) {
if (!isURLInstance(fileURLOrPath)) if (!isURL(fileURLOrPath))
return fileURLOrPath; return fileURLOrPath;
return fileURLToPath(fileURLOrPath); return fileURLToPath(fileURLOrPath);
} }
@ -1297,7 +1251,6 @@ module.exports = {
fileURLToPath, fileURLToPath,
pathToFileURL, pathToFileURL,
toPathIfFileURL, toPathIfFileURL,
isURLInstance,
URL, URL,
URLSearchParams, URLSearchParams,
domainToASCII, domainToASCII,

View File

@ -55,7 +55,7 @@ const {
WritableWorkerStdio, WritableWorkerStdio,
} = workerIo; } = workerIo;
const { deserializeError } = require('internal/error_serdes'); const { deserializeError } = require('internal/error_serdes');
const { fileURLToPath, isURLInstance, pathToFileURL } = require('internal/url'); const { fileURLToPath, isURL, pathToFileURL } = require('internal/url');
const { kEmptyObject } = require('internal/util'); const { kEmptyObject } = require('internal/util');
const { validateArray } = require('internal/validators'); const { validateArray } = require('internal/validators');
@ -148,13 +148,13 @@ class Worker extends EventEmitter {
} }
url = null; url = null;
doEval = 'classic'; doEval = 'classic';
} else if (isURLInstance(filename) && filename.protocol === 'data:') { } else if (isURL(filename) && filename.protocol === 'data:') {
url = null; url = null;
doEval = 'module'; doEval = 'module';
filename = `import ${JSONStringify(`${filename}`)}`; filename = `import ${JSONStringify(`${filename}`)}`;
} else { } else {
doEval = false; doEval = false;
if (isURLInstance(filename)) { if (isURL(filename)) {
url = filename; url = filename;
filename = fileURLToPath(filename); filename = fileURLToPath(filename);
} else if (typeof filename !== 'string') { } else if (typeof filename !== 'string') {

View File

@ -61,7 +61,7 @@ assert.strictEqual(
assert.strictEqual( assert.strictEqual(
util.inspect({ a: url }, { depth: 0 }), util.inspect({ a: url }, { depth: 0 }),
'{ a: [URL] }'); '{ a: URL {} }');
class MyURL extends URL {} class MyURL extends URL {}
assert(util.inspect(new MyURL(url.href)).startsWith('MyURL {')); assert(util.inspect(new MyURL(url.href)).startsWith('MyURL {'));

View File

@ -10,7 +10,8 @@ const assert = require('assert');
'toJSON', 'toJSON',
].forEach((i) => { ].forEach((i) => {
assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), { assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS', name: 'TypeError',
message: /Cannot read private member/,
}); });
}); });
@ -27,11 +28,13 @@ const assert = require('assert');
'hash', 'hash',
].forEach((i) => { ].forEach((i) => {
assert.throws(() => Reflect.get(URL.prototype, i, {}), { assert.throws(() => Reflect.get(URL.prototype, i, {}), {
code: 'ERR_INVALID_THIS', name: 'TypeError',
message: /Cannot read private member/,
}); });
assert.throws(() => Reflect.set(URL.prototype, i, null, {}), { assert.throws(() => Reflect.set(URL.prototype, i, null, {}), {
code: 'ERR_INVALID_THIS', name: 'TypeError',
message: /Cannot read private member/,
}); });
}); });
@ -40,6 +43,7 @@ const assert = require('assert');
'searchParams', 'searchParams',
].forEach((i) => { ].forEach((i) => {
assert.throws(() => Reflect.get(URL.prototype, i, {}), { assert.throws(() => Reflect.get(URL.prototype, i, {}), {
code: 'ERR_INVALID_THIS', name: 'TypeError',
message: /Cannot read private member/,
}); });
}); });