Browse Source

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/12507
Reviewed-By: James M Snell <jasnell@gmail.com>
v7.x
Timothy Gu 8 years ago
committed by Evan Lucas
parent
commit
7c9ca0f3ce
  1. 98
      lib/internal/url.js
  2. 53
      src/node_url.cc
  3. 237
      test/fixtures/url-setter-tests-additional.js
  4. 30
      test/fixtures/url-tests-additional.js
  5. 9
      test/parallel/test-whatwg-url-searchparams-append.js
  6. 16
      test/parallel/test-whatwg-url-searchparams-constructor.js
  7. 6
      test/parallel/test-whatwg-url-searchparams-delete.js
  8. 6
      test/parallel/test-whatwg-url-searchparams-get.js
  9. 6
      test/parallel/test-whatwg-url-searchparams-getall.js
  10. 6
      test/parallel/test-whatwg-url-searchparams-has.js
  11. 9
      test/parallel/test-whatwg-url-searchparams-set.js
  12. 32
      test/parallel/test-whatwg-url-searchparams.js
  13. 45
      test/parallel/test-whatwg-url-setters.js

98
lib/internal/url.js

@ -23,6 +23,18 @@ const IteratorPrototype = Object.getPrototypeOf(
Object.getPrototypeOf([][Symbol.iterator]()) 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 { class OpaqueOrigin {
toString() { toString() {
return 'null'; return 'null';
@ -108,7 +120,6 @@ function onParseError(flags, input) {
// Reused by URL constructor and URL#href setter. // Reused by URL constructor and URL#href setter.
function parse(url, input, base) { function parse(url, input, base) {
input = String(input);
const base_context = base ? base[context] : undefined; const base_context = base ? base[context] : undefined;
url[context] = new StorageObject(); url[context] = new StorageObject();
binding.parse(input.trim(), -1, binding.parse(input.trim(), -1,
@ -203,8 +214,10 @@ function onParseHashComplete(flags, protocol, username, password,
class URL { class URL {
constructor(input, base) { constructor(input, base) {
// toUSVString is not needed.
input = '' + input;
if (base !== undefined && !(base instanceof URL)) if (base !== undefined && !(base instanceof URL))
base = new URL(String(base)); base = new URL(base);
parse(this, input, base); parse(this, input, base);
} }
@ -312,6 +325,8 @@ Object.defineProperties(URL.prototype, {
return this[kFormat]({}); return this[kFormat]({});
}, },
set(input) { set(input) {
// toUSVString is not needed.
input = '' + input;
parse(this, input); parse(this, input);
} }
}, },
@ -329,7 +344,8 @@ Object.defineProperties(URL.prototype, {
return this[context].scheme; return this[context].scheme;
}, },
set(scheme) { set(scheme) {
scheme = String(scheme); // toUSVString is not needed.
scheme = '' + scheme;
if (scheme.length === 0) if (scheme.length === 0)
return; return;
binding.parse(scheme, binding.kSchemeStart, null, this[context], binding.parse(scheme, binding.kSchemeStart, null, this[context],
@ -343,7 +359,8 @@ Object.defineProperties(URL.prototype, {
return this[context].username || ''; return this[context].username || '';
}, },
set(username) { set(username) {
username = String(username); // toUSVString is not needed.
username = '' + username;
if (!this.hostname) if (!this.hostname)
return; return;
const ctx = this[context]; const ctx = this[context];
@ -363,7 +380,8 @@ Object.defineProperties(URL.prototype, {
return this[context].password || ''; return this[context].password || '';
}, },
set(password) { set(password) {
password = String(password); // toUSVString is not needed.
password = '' + password;
if (!this.hostname) if (!this.hostname)
return; return;
const ctx = this[context]; const ctx = this[context];
@ -388,7 +406,8 @@ Object.defineProperties(URL.prototype, {
}, },
set(host) { set(host) {
const ctx = this[context]; const ctx = this[context];
host = String(host); // toUSVString is not needed.
host = '' + host;
if (this[cannotBeBase] || if (this[cannotBeBase] ||
(this[special] && host.length === 0)) { (this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or // Cannot set the host if cannot-be-base is set or
@ -412,7 +431,8 @@ Object.defineProperties(URL.prototype, {
}, },
set(host) { set(host) {
const ctx = this[context]; const ctx = this[context];
host = String(host); // toUSVString is not needed.
host = '' + host;
if (this[cannotBeBase] || if (this[cannotBeBase] ||
(this[special] && host.length === 0)) { (this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or // Cannot set the host if cannot-be-base is set or
@ -436,11 +456,12 @@ Object.defineProperties(URL.prototype, {
return port === undefined ? '' : String(port); return port === undefined ? '' : String(port);
}, },
set(port) { set(port) {
// toUSVString is not needed.
port = '' + port;
const ctx = this[context]; const ctx = this[context];
if (!ctx.host || this[cannotBeBase] || if (!ctx.host || this[cannotBeBase] ||
this.protocol === 'file:') this.protocol === 'file:')
return; return;
port = String(port);
if (port === '') { if (port === '') {
ctx.port = undefined; ctx.port = undefined;
return; return;
@ -459,9 +480,11 @@ Object.defineProperties(URL.prototype, {
return ctx.path !== undefined ? `/${ctx.path.join('/')}` : ''; return ctx.path !== undefined ? `/${ctx.path.join('/')}` : '';
}, },
set(path) { set(path) {
// toUSVString is not needed.
path = '' + path;
if (this[cannotBeBase]) if (this[cannotBeBase])
return; return;
binding.parse(String(path), binding.kPathStart, null, this[context], binding.parse(path, binding.kPathStart, null, this[context],
onParsePathComplete.bind(this)); onParsePathComplete.bind(this));
} }
}, },
@ -474,7 +497,7 @@ Object.defineProperties(URL.prototype, {
}, },
set(search) { set(search) {
const ctx = this[context]; const ctx = this[context];
search = String(search); search = toUSVString(search);
if (!search) { if (!search) {
ctx.query = null; ctx.query = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY; ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
@ -506,7 +529,8 @@ Object.defineProperties(URL.prototype, {
}, },
set(hash) { set(hash) {
const ctx = this[context]; const ctx = this[context];
hash = String(hash); // toUSVString is not needed.
hash = '' + hash;
if (this.protocol === 'javascript:') if (this.protocol === 'javascript:')
return; return;
if (!hash) { if (!hash) {
@ -649,19 +673,22 @@ class URLSearchParams {
if (pair.length !== 2) { if (pair.length !== 2) {
throw new TypeError('Each query pair must be a name/value tuple'); 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 { } else {
// record<USVString, USVString> // record<USVString, USVString>
this[searchParams] = []; this[searchParams] = [];
for (const key of Object.keys(init)) { for (var key of Object.keys(init)) {
const value = String(init[key]); key = toUSVString(key);
const value = toUSVString(init[key]);
this[searchParams].push(key, value); this[searchParams].push(key, value);
} }
} }
} else { } else {
// USVString // USVString
init = String(init); init = toUSVString(init);
if (init[0] === '?') init = init.slice(1); if (init[0] === '?') init = init.slice(1);
initSearchParams(this, init); initSearchParams(this, init);
} }
@ -740,8 +767,8 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
throw new TypeError('"name" and "value" arguments must be specified'); throw new TypeError('"name" and "value" arguments must be specified');
} }
name = String(name); name = toUSVString(name);
value = String(value); value = toUSVString(value);
this[searchParams].push(name, value); this[searchParams].push(name, value);
update(this[context], this); update(this[context], this);
}, },
@ -755,7 +782,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
} }
const list = this[searchParams]; const list = this[searchParams];
name = String(name); name = toUSVString(name);
for (var i = 0; i < list.length;) { for (var i = 0; i < list.length;) {
const cur = list[i]; const cur = list[i];
if (cur === name) { if (cur === name) {
@ -776,7 +803,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
} }
const list = this[searchParams]; const list = this[searchParams];
name = String(name); name = toUSVString(name);
for (var i = 0; i < list.length; i += 2) { for (var i = 0; i < list.length; i += 2) {
if (list[i] === name) { if (list[i] === name) {
return list[i + 1]; return list[i + 1];
@ -795,7 +822,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
const list = this[searchParams]; const list = this[searchParams];
const values = []; const values = [];
name = String(name); name = toUSVString(name);
for (var i = 0; i < list.length; i += 2) { for (var i = 0; i < list.length; i += 2) {
if (list[i] === name) { if (list[i] === name) {
values.push(list[i + 1]); values.push(list[i + 1]);
@ -813,7 +840,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
} }
const list = this[searchParams]; const list = this[searchParams];
name = String(name); name = toUSVString(name);
for (var i = 0; i < list.length; i += 2) { for (var i = 0; i < list.length; i += 2) {
if (list[i] === name) { if (list[i] === name) {
return true; return true;
@ -831,8 +858,8 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
} }
const list = this[searchParams]; const list = this[searchParams];
name = String(name); name = toUSVString(name);
value = String(value); value = toUSVString(value);
// If there are any name-value pairs whose name is `name`, in `list`, set // 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 // the value of the first such name-value pair to `value` and remove the
@ -1094,11 +1121,13 @@ function originFor(url, base) {
} }
function domainToASCII(domain) { function domainToASCII(domain) {
return binding.domainToASCII(String(domain)); // toUSVString is not needed.
return binding.domainToASCII('' + domain);
} }
function domainToUnicode(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 // Utility function that converts a URL object into an ordinary
@ -1184,11 +1213,14 @@ function getPathFromURL(path) {
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
} }
exports.getPathFromURL = getPathFromURL; module.exports = {
exports.URL = URL; toUSVString,
exports.URLSearchParams = URLSearchParams; getPathFromURL,
exports.domainToASCII = domainToASCII; URL,
exports.domainToUnicode = domainToUnicode; URLSearchParams,
exports.urlToOptions = urlToOptions; domainToASCII,
exports.formatSymbol = kFormat; domainToUnicode,
exports.searchParamsSymbol = searchParams; urlToOptions,
formatSymbol: kFormat,
searchParamsSymbol: searchParams
};

53
src/node_url.cc

@ -20,6 +20,8 @@
#include <unicode/utf.h> #include <unicode/utf.h>
#endif #endif
#define UNICODE_REPLACEMENT_CHARACTER 0xFFFD
namespace node { namespace node {
using v8::Array; using v8::Array;
@ -104,6 +106,21 @@ namespace url {
} }
#endif #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, static url_host_type ParseIPv6Host(url_host* host,
const char* input, const char* input,
size_t length) { size_t length) {
@ -1356,6 +1373,41 @@ namespace url {
v8::NewStringType::kNormal).ToLocalChecked()); v8::NewStringType::kNormal).ToLocalChecked());
} }
static void ToUSVString(const FunctionCallbackInfo<Value>& 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<Value>& args) { static void DomainToASCII(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1); CHECK_GE(args.Length(), 1);
@ -1403,6 +1455,7 @@ namespace url {
Environment* env = Environment::GetCurrent(context); Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "parse", Parse); env->SetMethod(target, "parse", Parse);
env->SetMethod(target, "encodeAuth", EncodeAuthSet); env->SetMethod(target, "encodeAuth", EncodeAuthSet);
env->SetMethod(target, "toUSVString", ToUSVString);
env->SetMethod(target, "domainToASCII", DomainToASCII); env->SetMethod(target, "domainToASCII", DomainToASCII);
env->SetMethod(target, "domainToUnicode", DomainToUnicode); env->SetMethod(target, "domainToUnicode", DomainToUnicode);

237
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'
}
}
]
};

30
test/fixtures/url-tests-additional.js

@ -3,4 +3,34 @@
// This file contains test cases not part of the WPT // This file contains test cases not part of the WPT
module.exports = [ 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'
}
]; ];

9
test/parallel/test-whatwg-url-searchparams-append.js

@ -57,4 +57,13 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.set('a'); params.set('a');
}, /^TypeError: "name" and "value" arguments must be specified$/); }, /^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$/);
} }

16
test/parallel/test-whatwg-url-searchparams-constructor.js

@ -207,3 +207,19 @@ test(() => {
assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]), assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]),
/^TypeError: Each query pair must be iterable$/); /^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$/);
}

6
test/parallel/test-whatwg-url-searchparams-delete.js

@ -51,6 +51,12 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.delete(); params.delete();
}, /^TypeError: "name" argument must be specified$/); }, /^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 // https://github.com/nodejs/node/issues/10480

6
test/parallel/test-whatwg-url-searchparams-get.js

@ -42,4 +42,10 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.get(); params.get();
}, /^TypeError: "name" argument must be specified$/); }, /^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$/);
} }

6
test/parallel/test-whatwg-url-searchparams-getall.js

@ -46,4 +46,10 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.getAll(); params.getAll();
}, /^TypeError: "name" argument must be specified$/); }, /^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$/);
} }

6
test/parallel/test-whatwg-url-searchparams-has.js

@ -45,4 +45,10 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.has(); params.has();
}, /^TypeError: "name" argument must be specified$/); }, /^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$/);
} }

9
test/parallel/test-whatwg-url-searchparams-set.js

@ -43,4 +43,13 @@ test(function() {
assert.throws(() => { assert.throws(() => {
params.set('a'); params.set('a');
}, /^TypeError: "name" and "value" arguments must be specified$/); }, /^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$/);
} }

32
test/parallel/test-whatwg-url-searchparams.js

@ -5,8 +5,14 @@ const assert = require('assert');
const URL = require('url').URL; const URL = require('url').URL;
// Tests below are not from WPT. // Tests below are not from WPT.
const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%5Bobject%20Object%5D'; const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' +
const values = ['a', 1, true, undefined, null, {}]; '&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 m = new URL('http://example.org');
const sp = m.searchParams; const sp = m.searchParams;
@ -27,7 +33,7 @@ assert.strictEqual(sp.toString(), '');
values.forEach((i) => sp.append('a', i)); values.forEach((i) => sp.append('a', i));
assert(sp.has('a')); 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.get('a'), 'a');
assert.strictEqual(sp.toString(), serialized); assert.strictEqual(sp.toString(), serialized);
@ -39,23 +45,27 @@ assert.strictEqual(sp[Symbol.iterator], sp.entries);
let key, val; let key, val;
let n = 0; let n = 0;
for ([key, val] of sp) { for ([key, val] of sp) {
assert.strictEqual(key, 'a'); assert.strictEqual(key, 'a', n);
assert.strictEqual(val, String(values[n++])); assert.strictEqual(val, normalizedValues[n], n);
n++;
} }
n = 0; n = 0;
for (key of sp.keys()) { for (key of sp.keys()) {
assert.strictEqual(key, 'a'); assert.strictEqual(key, 'a', n);
n++;
} }
n = 0; n = 0;
for (val of sp.values()) { for (val of sp.values()) {
assert.strictEqual(val, String(values[n++])); assert.strictEqual(val, normalizedValues[n], n);
n++;
} }
n = 0; n = 0;
sp.forEach(function(val, key, obj) { sp.forEach(function(val, key, obj) {
assert.strictEqual(this, undefined); assert.strictEqual(this, undefined, n);
assert.strictEqual(key, 'a'); assert.strictEqual(key, 'a', n);
assert.strictEqual(val, String(values[n++])); assert.strictEqual(val, normalizedValues[n], n);
assert.strictEqual(obj, sp); assert.strictEqual(obj, sp, n);
n++;
}); });
sp.forEach(function() { sp.forEach(function() {
assert.strictEqual(this, m); assert.strictEqual(this, m);

45
test/parallel/test-whatwg-url-setters.js

@ -1,9 +1,12 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
const assert = require('assert');
const path = require('path'); const path = require('path');
const URL = require('url').URL; const URL = require('url').URL;
const { test, assert_equals } = common.WPT; const { test, assert_equals } = common.WPT;
const additionalTestCases = require(
path.join(common.fixturesDir, 'url-setter-tests-additional.js'));
if (!common.hasIntl) { if (!common.hasIntl) {
// A handful of the tests fail when ICU is not included. // A handful of the tests fail when ICU is not included.
@ -76,3 +79,45 @@ function runURLSettersTests(all_test_cases) {
startURLSettersTests() startURLSettersTests()
/* eslint-enable */ /* 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)}`);
}
}
}

Loading…
Cancel
Save