From 4b448c8aefe83cccbbb61acdfe34b183f8f7057f Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 6 Apr 2023 21:17:14 -0700 Subject: [PATCH] url: more sophisticated brand check for URLSearchParams Use private properties and static {} blocks. PR-URL: https://github.com/nodejs/node/pull/47414 Reviewed-By: Yagiz Nizipli Reviewed-By: Benjamin Gruenbaum Reviewed-By: Rich Trott --- lib/internal/url.js | 352 +++++++++++++++++++++----------------------- 1 file changed, 166 insertions(+), 186 deletions(-) diff --git a/lib/internal/url.js b/lib/internal/url.js index 0c60757b1efd89..cb662b90ac052c 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -13,10 +13,7 @@ const { IteratorPrototype, Number, ObjectDefineProperties, - ObjectDefineProperty, - ObjectGetOwnPropertySymbols, - ObjectGetPrototypeOf, - ObjectKeys, + ObjectSetPrototypeOf, ReflectGetOwnPropertyDescriptor, ReflectOwnKeys, RegExpPrototypeSymbolReplace, @@ -90,8 +87,7 @@ const bindingUrl = internalBinding('url'); const FORWARD_SLASH = /\//g; -const context = Symbol('context'); -const searchParams = Symbol('query'); +const contextForInspect = Symbol('context'); const updateActions = { kProtocol: 0, @@ -172,15 +168,124 @@ class URLContext { } } -function isURLSearchParams(self) { - return self && self[searchParams] && !self[searchParams][searchParams]; +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] = []; + #searchParams = []; // "associated url object" - [context] = null; + #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. @@ -191,11 +296,11 @@ class URLSearchParams { // Do nothing } else if (typeof init === 'object' || typeof init === 'function') { const method = init[SymbolIterator]; - if (method === this[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(); + const childParams = init.#searchParams; + this.#searchParams = childParams.slice(); } else if (method != null) { // Sequence> if (typeof method !== 'function') { @@ -221,7 +326,7 @@ class URLSearchParams { 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])); + ArrayPrototypePush(this.#searchParams, toUSVString(pair[0]), toUSVString(pair[1])); } else { if (((typeof pair !== 'object' && typeof pair !== 'function') || typeof pair[SymbolIterator] !== 'function')) { @@ -232,7 +337,7 @@ class URLSearchParams { for (const element of pair) { length++; - ArrayPrototypePush(this[searchParams], toUSVString(element)); + ArrayPrototypePush(this.#searchParams, toUSVString(element)); } // If innerSequence's size is not 2, then throw a TypeError. @@ -257,9 +362,9 @@ class URLSearchParams { // In that case, we retain the later one. Refer to WPT. const keyIdx = visited.get(typedKey); if (keyIdx !== undefined) { - this[searchParams][keyIdx] = typedValue; + this.#searchParams[keyIdx] = typedValue; } else { - visited.set(typedKey, ArrayPrototypePush(this[searchParams], + visited.set(typedKey, ArrayPrototypePush(this.#searchParams, typedKey, typedValue) - 1); } @@ -269,12 +374,12 @@ class URLSearchParams { } else { // https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams init = toUSVString(init); - this[searchParams] = init ? parseParams(init) : []; + this.#searchParams = init ? parseParams(init) : []; } } [inspect.custom](recurseTimes, ctx) { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); if (typeof recurseTimes === 'number' && recurseTimes < 0) @@ -287,7 +392,7 @@ class URLSearchParams { } const innerInspect = (v) => inspect(v, innerOpts); - const list = this[searchParams]; + const list = this.#searchParams; const output = []; for (let i = 0; i < list.length; i += 2) ArrayPrototypePush( @@ -310,13 +415,13 @@ class URLSearchParams { } get size() { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return this[searchParams].length / 2; + return this.#searchParams.length / 2; } append(name, value) { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); if (arguments.length < 2) { @@ -325,21 +430,21 @@ class URLSearchParams { name = toUSVString(name); value = toUSVString(value); - ArrayPrototypePush(this[searchParams], name, value); - if (this[context]) { - this[context].search = this.toString(); + ArrayPrototypePush(this.#searchParams, name, value); + if (this.#context) { + this.#context.search = this.toString(); } } delete(name) { - if (!isURLSearchParams(this)) + 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 list = this.#searchParams; name = toUSVString(name); for (let i = 0; i < list.length;) { const cur = list[i]; @@ -349,20 +454,20 @@ class URLSearchParams { i += 2; } } - if (this[context]) { - this[context].search = this.toString(); + if (this.#context) { + this.#context.search = this.toString(); } } get(name) { - if (!isURLSearchParams(this)) + 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 list = this.#searchParams; name = toUSVString(name); for (let i = 0; i < list.length; i += 2) { if (list[i] === name) { @@ -373,14 +478,14 @@ class URLSearchParams { } getAll(name) { - if (!isURLSearchParams(this)) + 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 list = this.#searchParams; const values = []; name = toUSVString(name); for (let i = 0; i < list.length; i += 2) { @@ -392,14 +497,14 @@ class URLSearchParams { } has(name) { - if (!isURLSearchParams(this)) + 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 list = this.#searchParams; name = toUSVString(name); for (let i = 0; i < list.length; i += 2) { if (list[i] === name) { @@ -410,14 +515,14 @@ class URLSearchParams { } set(name, value) { - if (!isURLSearchParams(this)) + 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]; + const list = this.#searchParams; name = toUSVString(name); value = toUSVString(value); @@ -446,13 +551,16 @@ class URLSearchParams { ArrayPrototypePush(list, name, value); } - if (this[context]) { - this[context].search = this.toString(); + if (this.#context) { + this.#context.search = this.toString(); } } sort() { - const a = this[searchParams]; + 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) { @@ -492,8 +600,8 @@ class URLSearchParams { } } - if (this[context]) { - this[context].search = this.toString(); + if (this.#context) { + this.#context.search = this.toString(); } } @@ -501,19 +609,19 @@ class URLSearchParams { // Define entries here rather than [Symbol.iterator] as the function name // must be set to `entries`. entries() { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return createSearchParamsIterator(this, 'key+value'); + return new URLSearchParamsIterator(this, 'key+value'); } forEach(callback, thisArg = undefined) { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); validateFunction(callback, 'callback'); - let list = this[searchParams]; + let list = this.#searchParams; let i = 0; while (i < list.length) { @@ -521,33 +629,33 @@ class URLSearchParams { const value = list[i + 1]; callback.call(thisArg, value, key, this); // In case the URL object's `search` is updated - list = this[searchParams]; + list = this.#searchParams; i += 2; } } // https://heycam.github.io/webidl/#es-iterable keys() { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return createSearchParamsIterator(this, 'key'); + return new URLSearchParamsIterator(this, 'key'); } values() { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return createSearchParamsIterator(this, 'value'); + return new URLSearchParamsIterator(this, 'value'); } // https://heycam.github.io/webidl/#es-stringifier // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior toString() { - if (!isURLSearchParams(this)) + if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return serializeParams(this[searchParams]); + return serializeParams(this.#searchParams); } } @@ -635,7 +743,7 @@ class URL { obj.hash = this.hash; if (opts.showHidden) { - obj[context] = this.#context; + obj[contextForInspect] = this.#context; } return `${constructor.name} ${inspect(obj, opts)}`; @@ -668,9 +776,9 @@ class URL { if (this.#searchParams) { if (this.#context.hasSearch) { - this.#searchParams[searchParams] = parseParams(this.search); + setURLSearchParams(this.#searchParams, this.search); } else { - this.#searchParams[searchParams] = []; + setURLSearchParams(this.#searchParams, undefined); } } } @@ -846,7 +954,7 @@ class URL { // Create URLSearchParams on demand to greatly improve the URL performance. if (this.#searchParams == null) { this.#searchParams = new URLSearchParams(this.search); - this.#searchParams[context] = this; + setURLSearchParamsContext(this.#searchParams, this); } return this.#searchParams; } @@ -1099,38 +1207,6 @@ function serializeParams(array) { return output; } -// Mainly to mitigate func-name-matching ESLint rule -function defineIDLClass(proto, classStr, obj) { - // https://heycam.github.io/webidl/#dfn-class-string - ObjectDefineProperty(proto, SymbolToStringTag, { - __proto__: null, - writable: false, - enumerable: false, - configurable: true, - value: classStr, - }); - - // https://heycam.github.io/webidl/#es-operations - for (const key of ObjectKeys(obj)) { - ObjectDefineProperty(proto, key, { - __proto__: null, - writable: true, - enumerable: true, - configurable: true, - value: obj[key], - }); - } - for (const key of ObjectGetOwnPropertySymbols(obj)) { - ObjectDefineProperty(proto, key, { - __proto__: null, - writable: true, - enumerable: false, - configurable: true, - value: obj[key], - }); - } -} - // for merge sort function merge(out, start, mid, end, lBuffer, rBuffer) { const sizeLeft = mid - start; @@ -1160,102 +1236,6 @@ function merge(out, start, mid, end, lBuffer, rBuffer) { out[o++] = rBuffer[r++]; } -// https://heycam.github.io/webidl/#dfn-default-iterator-object -function createSearchParamsIterator(target, kind) { - const iterator = { __proto__: URLSearchParamsIteratorPrototype }; - iterator[context] = { - target, - kind, - index: 0, - }; - return iterator; -} - -// https://heycam.github.io/webidl/#dfn-iterator-prototype-object -const URLSearchParamsIteratorPrototype = { __proto__: IteratorPrototype }; - -defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParams Iterator', { - next() { - if (!this || - ObjectGetPrototypeOf(this) !== URLSearchParamsIteratorPrototype) { - throw new ERR_INVALID_THIS('URLSearchParamsIterator'); - } - - const { - target, - kind, - index, - } = this[context]; - const values = target[searchParams]; - const len = values.length; - if (index >= len) { - return { - value: undefined, - done: true, - }; - } - - const name = values[index]; - const value = values[index + 1]; - this[context].index = index + 2; - - let result; - if (kind === 'key') { - result = name; - } else if (kind === 'value') { - result = value; - } else { - result = [name, value]; - } - - return { - value: result, - done: false, - }; - }, - [inspect.custom](recurseTimes, ctx) { - if (this == null || this[context] == null || this[context].target == null) - 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 { - target, - kind, - index, - } = this[context]; - const output = ArrayPrototypeReduce( - ArrayPrototypeSlice(target[searchParams], index), - (prev, cur, i) => { - const key = i % 2 === 0; - if (kind === 'key' && key) { - ArrayPrototypePush(prev, cur); - } else if (kind === 'value' && !key) { - ArrayPrototypePush(prev, cur); - } else if (kind === 'key+value' && !key) { - ArrayPrototypePush(prev, [target[searchParams][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} }`; - }, -}); - function domainToASCII(domain) { if (arguments.length < 1) throw new ERR_MISSING_ARGS('domain');