From b610a4db1c2919f88711962f5797f25ecb1cd36b Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Fri, 3 Feb 2017 21:53:27 -0800 Subject: [PATCH] url: enforce valid UTF-8 in WHATWG parser This commit implements the Web IDL USVString conversion, which mandates all unpaired Unicode surrogates be turned into U+FFFD REPLACEMENT CHARACTER. It also disallows Symbols to be used as USVString per spec. Certain functions call into C++ methods in the binding that use the Utf8Value class to access string arguments. Utf8Value already does the normalization using V8's String::Write, so in those cases, instead of doing the full USVString normalization, only a symbol check is done (`'' + val`, which uses ES's ToString, versus `String()` which has special provisions for symbols). PR-URL: https://github.com/nodejs/node/pull/11436 Reviewed-By: Ben Noordhuis Reviewed-By: James M Snell --- lib/internal/url.js | 98 +++++--- src/node_url.cc | 53 ++++ test/fixtures/url-setter-tests-additional.js | 237 ++++++++++++++++++ test/fixtures/url-tests-additional.js | 30 +++ .../test-whatwg-url-searchparams-append.js | 9 + ...est-whatwg-url-searchparams-constructor.js | 16 ++ .../test-whatwg-url-searchparams-delete.js | 6 + .../test-whatwg-url-searchparams-get.js | 6 + .../test-whatwg-url-searchparams-getall.js | 6 + .../test-whatwg-url-searchparams-has.js | 6 + .../test-whatwg-url-searchparams-set.js | 9 + test/parallel/test-whatwg-url-searchparams.js | 32 ++- test/parallel/test-whatwg-url-setters.js | 45 ++++ 13 files changed, 509 insertions(+), 44 deletions(-) create mode 100644 test/fixtures/url-setter-tests-additional.js diff --git a/lib/internal/url.js b/lib/internal/url.js index 76530f2edc1998..6e6cc62226766b 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -23,6 +23,18 @@ const IteratorPrototype = Object.getPrototypeOf( Object.getPrototypeOf([][Symbol.iterator]()) ); +const unpairedSurrogateRe = + /([^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/; +function toUSVString(val) { + const str = '' + val; + // As of V8 5.5, `str.search()` (and `unpairedSurrogateRe[@@search]()`) are + // slower than `unpairedSurrogateRe.exec()`. + const match = unpairedSurrogateRe.exec(str); + if (!match) + return str; + return binding.toUSVString(str, match.index); +} + class OpaqueOrigin { toString() { return 'null'; @@ -104,7 +116,6 @@ function onParseComplete(flags, protocol, username, password, // Reused by URL constructor and URL#href setter. function parse(url, input, base) { - input = String(input); const base_context = base ? base[context] : undefined; url[context] = new StorageObject(); binding.parse(input.trim(), -1, @@ -206,8 +217,10 @@ function onParseHashComplete(flags, protocol, username, password, class URL { constructor(input, base) { + // toUSVString is not needed. + input = '' + input; if (base !== undefined && !(base instanceof URL)) - base = new URL(String(base)); + base = new URL(base); parse(this, input, base); } @@ -315,6 +328,8 @@ Object.defineProperties(URL.prototype, { return this[kFormat]({}); }, set(input) { + // toUSVString is not needed. + input = '' + input; parse(this, input); } }, @@ -332,7 +347,8 @@ Object.defineProperties(URL.prototype, { return this[context].scheme; }, set(scheme) { - scheme = String(scheme); + // toUSVString is not needed. + scheme = '' + scheme; if (scheme.length === 0) return; binding.parse(scheme, binding.kSchemeStart, null, this[context], @@ -346,7 +362,8 @@ Object.defineProperties(URL.prototype, { return this[context].username || ''; }, set(username) { - username = String(username); + // toUSVString is not needed. + username = '' + username; if (!this.hostname) return; const ctx = this[context]; @@ -366,7 +383,8 @@ Object.defineProperties(URL.prototype, { return this[context].password || ''; }, set(password) { - password = String(password); + // toUSVString is not needed. + password = '' + password; if (!this.hostname) return; const ctx = this[context]; @@ -391,7 +409,8 @@ Object.defineProperties(URL.prototype, { }, set(host) { const ctx = this[context]; - host = String(host); + // toUSVString is not needed. + host = '' + host; if (this[cannotBeBase] || (this[special] && host.length === 0)) { // Cannot set the host if cannot-be-base is set or @@ -415,7 +434,8 @@ Object.defineProperties(URL.prototype, { }, set(host) { const ctx = this[context]; - host = String(host); + // toUSVString is not needed. + host = '' + host; if (this[cannotBeBase] || (this[special] && host.length === 0)) { // Cannot set the host if cannot-be-base is set or @@ -439,11 +459,12 @@ Object.defineProperties(URL.prototype, { return port === undefined ? '' : String(port); }, set(port) { + // toUSVString is not needed. + port = '' + port; const ctx = this[context]; if (!ctx.host || this[cannotBeBase] || this.protocol === 'file:') return; - port = String(port); if (port === '') { ctx.port = undefined; return; @@ -462,9 +483,11 @@ Object.defineProperties(URL.prototype, { return ctx.path !== undefined ? `/${ctx.path.join('/')}` : ''; }, set(path) { + // toUSVString is not needed. + path = '' + path; if (this[cannotBeBase]) return; - binding.parse(String(path), binding.kPathStart, null, this[context], + binding.parse(path, binding.kPathStart, null, this[context], onParsePathComplete.bind(this)); } }, @@ -477,7 +500,7 @@ Object.defineProperties(URL.prototype, { }, set(search) { const ctx = this[context]; - search = String(search); + search = toUSVString(search); if (!search) { ctx.query = null; ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY; @@ -509,7 +532,8 @@ Object.defineProperties(URL.prototype, { }, set(hash) { const ctx = this[context]; - hash = String(hash); + // toUSVString is not needed. + hash = '' + hash; if (this.protocol === 'javascript:') return; if (!hash) { @@ -652,19 +676,22 @@ class URLSearchParams { if (pair.length !== 2) { throw new TypeError('Each query pair must be a name/value tuple'); } - this[searchParams].push(String(pair[0]), String(pair[1])); + const key = toUSVString(pair[0]); + const value = toUSVString(pair[1]); + this[searchParams].push(key, value); } } else { // record this[searchParams] = []; - for (const key of Object.keys(init)) { - const value = String(init[key]); + for (var key of Object.keys(init)) { + key = toUSVString(key); + const value = toUSVString(init[key]); this[searchParams].push(key, value); } } } else { // USVString - init = String(init); + init = toUSVString(init); if (init[0] === '?') init = init.slice(1); initSearchParams(this, init); } @@ -743,8 +770,8 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { throw new TypeError('"name" and "value" arguments must be specified'); } - name = String(name); - value = String(value); + name = toUSVString(name); + value = toUSVString(value); this[searchParams].push(name, value); update(this[context], this); }, @@ -758,7 +785,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { } const list = this[searchParams]; - name = String(name); + name = toUSVString(name); for (var i = 0; i < list.length;) { const cur = list[i]; if (cur === name) { @@ -779,7 +806,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { } const list = this[searchParams]; - name = String(name); + name = toUSVString(name); for (var i = 0; i < list.length; i += 2) { if (list[i] === name) { return list[i + 1]; @@ -798,7 +825,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { const list = this[searchParams]; const values = []; - name = String(name); + name = toUSVString(name); for (var i = 0; i < list.length; i += 2) { if (list[i] === name) { values.push(list[i + 1]); @@ -816,7 +843,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { } const list = this[searchParams]; - name = String(name); + name = toUSVString(name); for (var i = 0; i < list.length; i += 2) { if (list[i] === name) { return true; @@ -834,8 +861,8 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { } const list = this[searchParams]; - name = String(name); - value = String(value); + 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 @@ -1098,11 +1125,13 @@ function originFor(url, base) { } function domainToASCII(domain) { - return binding.domainToASCII(String(domain)); + // toUSVString is not needed. + return binding.domainToASCII('' + domain); } function domainToUnicode(domain) { - return binding.domainToUnicode(String(domain)); + // toUSVString is not needed. + return binding.domainToUnicode('' + domain); } // Utility function that converts a URL object into an ordinary @@ -1188,11 +1217,14 @@ function getPathFromURL(path) { return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); } -exports.getPathFromURL = getPathFromURL; -exports.URL = URL; -exports.URLSearchParams = URLSearchParams; -exports.domainToASCII = domainToASCII; -exports.domainToUnicode = domainToUnicode; -exports.urlToOptions = urlToOptions; -exports.formatSymbol = kFormat; -exports.searchParamsSymbol = searchParams; +module.exports = { + toUSVString, + getPathFromURL, + URL, + URLSearchParams, + domainToASCII, + domainToUnicode, + urlToOptions, + formatSymbol: kFormat, + searchParamsSymbol: searchParams +}; diff --git a/src/node_url.cc b/src/node_url.cc index 5027399e89dd71..b2f1322ade3cc0 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -20,6 +20,8 @@ #include #endif +#define UNICODE_REPLACEMENT_CHARACTER 0xFFFD + namespace node { using v8::Array; @@ -143,6 +145,21 @@ namespace url { } #endif + // If a UTF-16 character is a low/trailing surrogate. + static inline bool IsUnicodeTrail(uint16_t c) { + return (c & 0xFC00) == 0xDC00; + } + + // If a UTF-16 character is a surrogate. + static inline bool IsUnicodeSurrogate(uint16_t c) { + return (c & 0xF800) == 0xD800; + } + + // If a UTF-16 surrogate is a low/trailing one. + static inline bool IsUnicodeSurrogateTrail(uint16_t c) { + return (c & 0x400) != 0; + } + static url_host_type ParseIPv6Host(url_host* host, const char* input, size_t length) { @@ -1351,6 +1368,41 @@ namespace url { v8::NewStringType::kNormal).ToLocalChecked()); } + static void ToUSVString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 2); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsNumber()); + + TwoByteValue value(env->isolate(), args[0]); + const size_t n = value.length(); + + const int64_t start = args[1]->IntegerValue(env->context()).FromJust(); + CHECK_GE(start, 0); + + for (size_t i = start; i < n; i++) { + uint16_t c = value[i]; + if (!IsUnicodeSurrogate(c)) { + continue; + } else if (IsUnicodeSurrogateTrail(c) || i == n - 1) { + value[i] = UNICODE_REPLACEMENT_CHARACTER; + } else { + uint16_t d = value[i + 1]; + if (IsUnicodeTrail(d)) { + i++; + } else { + value[i] = UNICODE_REPLACEMENT_CHARACTER; + } + } + } + + args.GetReturnValue().Set( + String::NewFromTwoByte(env->isolate(), + *value, + v8::NewStringType::kNormal, + n).ToLocalChecked()); + } + static void DomainToASCII(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_GE(args.Length(), 1); @@ -1398,6 +1450,7 @@ namespace url { Environment* env = Environment::GetCurrent(context); env->SetMethod(target, "parse", Parse); env->SetMethod(target, "encodeAuth", EncodeAuthSet); + env->SetMethod(target, "toUSVString", ToUSVString); env->SetMethod(target, "domainToASCII", DomainToASCII); env->SetMethod(target, "domainToUnicode", DomainToUnicode); diff --git a/test/fixtures/url-setter-tests-additional.js b/test/fixtures/url-setter-tests-additional.js new file mode 100644 index 00000000000000..b27ae336a28776 --- /dev/null +++ b/test/fixtures/url-setter-tests-additional.js @@ -0,0 +1,237 @@ +module.exports = { + 'username': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://%F0%9F%98%80@github.com/', + 'username': '%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://%EF%BF%BD@github.com/', + 'username': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://%EF%BF%BDnode@github.com/', + 'username': '%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://%EF%BF%BD@github.com/', + 'username': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://%EF%BF%BDnode@github.com/', + 'username': '%EF%BF%BDnode' + } + } + ], + 'password': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://:%F0%9F%98%80@github.com/', + 'password': '%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://:%EF%BF%BD@github.com/', + 'password': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://:%EF%BF%BDnode@github.com/', + 'password': '%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://:%EF%BF%BD@github.com/', + 'password': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://:%EF%BF%BDnode@github.com/', + 'password': '%EF%BF%BDnode' + } + } + ], + 'pathname': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '/\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/%F0%9F%98%80', + 'pathname': '/%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '/\uD83D', + 'expected': { + 'href': 'https://github.com/%EF%BF%BD', + 'pathname': '/%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '/\uD83Dnode', + 'expected': { + 'href': 'https://github.com/%EF%BF%BDnode', + 'pathname': '/%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '/\uDE00', + 'expected': { + 'href': 'https://github.com/%EF%BF%BD', + 'pathname': '/%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '/\uDE00node', + 'expected': { + 'href': 'https://github.com/%EF%BF%BDnode', + 'pathname': '/%EF%BF%BDnode' + } + } + ], + 'search': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/?%F0%9F%98%80', + 'search': '?%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BD', + 'search': '?%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BDnode', + 'search': '?%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BD', + 'search': '?%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BDnode', + 'search': '?%EF%BF%BDnode' + } + } + ], + 'hash': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/#%F0%9F%98%80', + 'hash': '#%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BD', + 'hash': '#%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BDnode', + 'hash': '#%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BD', + 'hash': '#%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BDnode', + 'hash': '#%EF%BF%BDnode' + } + } + ] +}; diff --git a/test/fixtures/url-tests-additional.js b/test/fixtures/url-tests-additional.js index ffe47fb639dcba..c1c640f4bb4b7d 100644 --- a/test/fixtures/url-tests-additional.js +++ b/test/fixtures/url-tests-additional.js @@ -3,4 +3,34 @@ // This file contains test cases not part of the WPT module.exports = [ + { + // surrogate pair + 'url': 'https://github.com/nodejs/\uD83D\uDE00node', + 'protocol': 'https:', + 'pathname': '/nodejs/%F0%9F%98%80node' + }, + { + // unpaired low surrogate + 'url': 'https://github.com/nodejs/\uD83D', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BD' + }, + { + // unpaired low surrogate + 'url': 'https://github.com/nodejs/\uD83Dnode', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BDnode' + }, + { + // unmatched high surrogate + 'url': 'https://github.com/nodejs/\uDE00', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BD' + }, + { + // unmatched high surrogate + 'url': 'https://github.com/nodejs/\uDE00node', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BDnode' + } ]; diff --git a/test/parallel/test-whatwg-url-searchparams-append.js b/test/parallel/test-whatwg-url-searchparams-append.js index 2e3a33b26307c3..67eddbcc503e1e 100644 --- a/test/parallel/test-whatwg-url-searchparams-append.js +++ b/test/parallel/test-whatwg-url-searchparams-append.js @@ -57,4 +57,13 @@ test(function() { assert.throws(() => { params.set('a'); }, /^TypeError: "name" and "value" arguments must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.set(obj, 'b'), /^Error: toString$/); + assert.throws(() => params.set('a', obj), /^Error: toString$/); + assert.throws(() => params.set(sym, 'b'), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => params.set('a', sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js index d57373e727ac51..8ccd8f9427f160 100644 --- a/test/parallel/test-whatwg-url-searchparams-constructor.js +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -207,3 +207,19 @@ test(() => { assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]), /^TypeError: Each query pair must be iterable$/); } + +{ + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + + assert.throws(() => new URLSearchParams({ a: obj }), /^Error: toString$/); + assert.throws(() => new URLSearchParams([['a', obj]]), /^Error: toString$/); + assert.throws(() => new URLSearchParams(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => new URLSearchParams({ a: sym }), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => new URLSearchParams([[sym, 'a']]), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => new URLSearchParams([['a', sym]]), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-delete.js b/test/parallel/test-whatwg-url-searchparams-delete.js index c6235263f22bad..d0bae75b4718a8 100644 --- a/test/parallel/test-whatwg-url-searchparams-delete.js +++ b/test/parallel/test-whatwg-url-searchparams-delete.js @@ -51,6 +51,12 @@ test(function() { assert.throws(() => { params.delete(); }, /^TypeError: "name" argument must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.delete(obj), /^Error: toString$/); + assert.throws(() => params.delete(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } // https://github.com/nodejs/node/issues/10480 diff --git a/test/parallel/test-whatwg-url-searchparams-get.js b/test/parallel/test-whatwg-url-searchparams-get.js index 3a46993214a997..2244fc28612755 100644 --- a/test/parallel/test-whatwg-url-searchparams-get.js +++ b/test/parallel/test-whatwg-url-searchparams-get.js @@ -42,4 +42,10 @@ test(function() { assert.throws(() => { params.get(); }, /^TypeError: "name" argument must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.get(obj), /^Error: toString$/); + assert.throws(() => params.get(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } diff --git a/test/parallel/test-whatwg-url-searchparams-getall.js b/test/parallel/test-whatwg-url-searchparams-getall.js index df055e009e7e4d..921a6c9bc66da2 100644 --- a/test/parallel/test-whatwg-url-searchparams-getall.js +++ b/test/parallel/test-whatwg-url-searchparams-getall.js @@ -46,4 +46,10 @@ test(function() { assert.throws(() => { params.getAll(); }, /^TypeError: "name" argument must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.getAll(obj), /^Error: toString$/); + assert.throws(() => params.getAll(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } diff --git a/test/parallel/test-whatwg-url-searchparams-has.js b/test/parallel/test-whatwg-url-searchparams-has.js index 1be9cf6121593e..9d7272f999c653 100644 --- a/test/parallel/test-whatwg-url-searchparams-has.js +++ b/test/parallel/test-whatwg-url-searchparams-has.js @@ -45,4 +45,10 @@ test(function() { assert.throws(() => { params.has(); }, /^TypeError: "name" argument must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.has(obj), /^Error: toString$/); + assert.throws(() => params.has(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } diff --git a/test/parallel/test-whatwg-url-searchparams-set.js b/test/parallel/test-whatwg-url-searchparams-set.js index e78ce4763158b5..0eee7b5c9a0130 100644 --- a/test/parallel/test-whatwg-url-searchparams-set.js +++ b/test/parallel/test-whatwg-url-searchparams-set.js @@ -43,4 +43,13 @@ test(function() { assert.throws(() => { params.set('a'); }, /^TypeError: "name" and "value" arguments must be specified$/); + + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + assert.throws(() => params.append(obj, 'b'), /^Error: toString$/); + assert.throws(() => params.append('a', obj), /^Error: toString$/); + assert.throws(() => params.append(sym, 'b'), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => params.append('a', sym), + /^TypeError: Cannot convert a Symbol value to a string$/); } diff --git a/test/parallel/test-whatwg-url-searchparams.js b/test/parallel/test-whatwg-url-searchparams.js index 36fac3a2307ecc..e0d1826596704c 100644 --- a/test/parallel/test-whatwg-url-searchparams.js +++ b/test/parallel/test-whatwg-url-searchparams.js @@ -5,8 +5,14 @@ const assert = require('assert'); const URL = require('url').URL; // Tests below are not from WPT. -const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%5Bobject%20Object%5D'; -const values = ['a', 1, true, undefined, null, {}]; +const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' + + '&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD' + + '&a=%5Bobject%20Object%5D'; +const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00', + '\uD83D\uDE00', '\uDE00\uD83D', {}]; +const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD', + '\uFFFD', '\uD83D\uDE00', '\uFFFD\uFFFD', + '[object Object]']; const m = new URL('http://example.org'); const sp = m.searchParams; @@ -27,7 +33,7 @@ assert.strictEqual(sp.toString(), ''); values.forEach((i) => sp.append('a', i)); assert(sp.has('a')); -assert.strictEqual(sp.getAll('a').length, 6); +assert.strictEqual(sp.getAll('a').length, values.length); assert.strictEqual(sp.get('a'), 'a'); assert.strictEqual(sp.toString(), serialized); @@ -39,23 +45,27 @@ assert.strictEqual(sp[Symbol.iterator], sp.entries); let key, val; let n = 0; for ([key, val] of sp) { - assert.strictEqual(key, 'a'); - assert.strictEqual(val, String(values[n++])); + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + n++; } n = 0; for (key of sp.keys()) { - assert.strictEqual(key, 'a'); + assert.strictEqual(key, 'a', n); + n++; } n = 0; for (val of sp.values()) { - assert.strictEqual(val, String(values[n++])); + assert.strictEqual(val, normalizedValues[n], n); + n++; } n = 0; sp.forEach(function(val, key, obj) { - assert.strictEqual(this, undefined); - assert.strictEqual(key, 'a'); - assert.strictEqual(val, String(values[n++])); - assert.strictEqual(obj, sp); + assert.strictEqual(this, undefined, n); + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + assert.strictEqual(obj, sp, n); + n++; }); sp.forEach(function() { assert.strictEqual(this, m); diff --git a/test/parallel/test-whatwg-url-setters.js b/test/parallel/test-whatwg-url-setters.js index 409ed1bd00d36c..6342243c78fd17 100644 --- a/test/parallel/test-whatwg-url-setters.js +++ b/test/parallel/test-whatwg-url-setters.js @@ -1,9 +1,12 @@ 'use strict'; const common = require('../common'); +const assert = require('assert'); const path = require('path'); const URL = require('url').URL; const { test, assert_equals } = common.WPT; +const additionalTestCases = require( + path.join(common.fixturesDir, 'url-setter-tests-additional.js')); if (!common.hasIntl) { // A handful of the tests fail when ICU is not included. @@ -76,3 +79,45 @@ function runURLSettersTests(all_test_cases) { startURLSettersTests() /* eslint-enable */ + +// Tests below are not from WPT. + +{ + for (const attributeToBeSet in additionalTestCases) { + if (attributeToBeSet === 'comment') { + continue; + } + const testCases = additionalTestCases[attributeToBeSet]; + for (const testCase of testCases) { + let name = `Setting <${testCase.href}>.${attributeToBeSet}` + + ` = "${testCase.new_value}"`; + if ('comment' in testCase) { + name += ' ' + testCase.comment; + } + test(function() { + const url = new URL(testCase.href); + url[attributeToBeSet] = testCase.new_value; + for (const attribute in testCase.expected) { + assert_equals(url[attribute], testCase.expected[attribute]); + } + }, 'URL: ' + name); + } + } +} + +{ + const url = new URL('http://example.com/'); + const obj = { toString() { throw new Error('toString'); } }; + const sym = Symbol(); + const props = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(url)); + for (const [name, { set }] of Object.entries(props)) { + if (set) { + assert.throws(() => url[name] = obj, + /^Error: toString$/, + `url.${name} = { toString() { throw ... } }`); + assert.throws(() => url[name] = sym, + /^TypeError: Cannot convert a Symbol value to a string$/, + `url.${name} = ${String(sym)}`); + } + } +}