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 <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Timothy Gu 2017-02-03 21:53:27 -08:00
parent a7f7724167
commit b610a4db1c
13 changed files with 509 additions and 44 deletions

View File

@ -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<USVString, USVString>
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
};

View File

@ -20,6 +20,8 @@
#include <unicode/utf.h>
#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<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) {
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);

View File

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

View File

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

View File

@ -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$/);
}

View File

@ -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$/);
}

View File

@ -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

View File

@ -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$/);
}

View File

@ -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$/);
}

View File

@ -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$/);
}

View File

@ -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$/);
}

View File

@ -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);

View File

@ -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)}`);
}
}
}