Skip to content

Commit 01b8839

Browse files
TimothyGuevanlucas
authored andcommitted
url: spec-compliant URLSearchParams parser
The entire `URLSearchParams` class is now fully spec-compliant. PR-URL: #12507 Fixes: #10821 Reviewed-By: James M Snell <[email protected]>
1 parent 0cc37c7 commit 01b8839

File tree

4 files changed

+197
-17
lines changed

4 files changed

+197
-17
lines changed

benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const inputs = require('../fixtures/url-inputs.js').searchParams;
77
const bench = common.createBenchmark(main, {
88
type: Object.keys(inputs),
99
method: ['legacy', 'whatwg'],
10-
n: [1e5]
10+
n: [1e6]
1111
});
1212

1313
function useLegacy(n, input) {

lib/internal/url.js

+101-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
'use strict';
22

33
const util = require('util');
4-
const { hexTable, StorageObject } = require('internal/querystring');
4+
const {
5+
hexTable,
6+
isHexTable,
7+
StorageObject
8+
} = require('internal/querystring');
59
const binding = process.binding('url');
610
const context = Symbol('context');
711
const cannotBeBase = Symbol('cannot-be-base');
@@ -575,23 +579,106 @@ function initSearchParams(url, init) {
575579
url[searchParams] = [];
576580
return;
577581
}
578-
url[searchParams] = getParamsFromObject(querystring.parse(init));
582+
url[searchParams] = parseParams(init);
579583
}
580584

581-
function getParamsFromObject(obj) {
582-
const keys = Object.keys(obj);
583-
const values = [];
584-
for (var i = 0; i < keys.length; i++) {
585-
const name = keys[i];
586-
const value = obj[name];
587-
if (Array.isArray(value)) {
588-
for (const item of value)
589-
values.push(name, item);
590-
} else {
591-
values.push(name, value);
585+
// application/x-www-form-urlencoded parser
586+
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
587+
function parseParams(qs) {
588+
const out = [];
589+
var pairStart = 0;
590+
var lastPos = 0;
591+
var seenSep = false;
592+
var buf = '';
593+
var encoded = false;
594+
var encodeCheck = 0;
595+
var i;
596+
for (i = 0; i < qs.length; ++i) {
597+
const code = qs.charCodeAt(i);
598+
599+
// Try matching key/value pair separator
600+
if (code === 38/*&*/) {
601+
if (pairStart === i) {
602+
// We saw an empty substring between pair separators
603+
lastPos = pairStart = i + 1;
604+
continue;
605+
}
606+
607+
if (lastPos < i)
608+
buf += qs.slice(lastPos, i);
609+
if (encoded)
610+
buf = querystring.unescape(buf);
611+
out.push(buf);
612+
613+
// If `buf` is the key, add an empty value.
614+
if (!seenSep)
615+
out.push('');
616+
617+
seenSep = false;
618+
buf = '';
619+
encoded = false;
620+
encodeCheck = 0;
621+
lastPos = pairStart = i + 1;
622+
continue;
623+
}
624+
625+
// Try matching key/value separator (e.g. '=') if we haven't already
626+
if (!seenSep && code === 61/*=*/) {
627+
// Key/value separator match!
628+
if (lastPos < i)
629+
buf += qs.slice(lastPos, i);
630+
if (encoded)
631+
buf = querystring.unescape(buf);
632+
out.push(buf);
633+
634+
seenSep = true;
635+
buf = '';
636+
encoded = false;
637+
encodeCheck = 0;
638+
lastPos = i + 1;
639+
continue;
640+
}
641+
642+
// Handle + and percent decoding.
643+
if (code === 43/*+*/) {
644+
if (lastPos < i)
645+
buf += qs.slice(lastPos, i);
646+
buf += ' ';
647+
lastPos = i + 1;
648+
} else if (!encoded) {
649+
// Try to match an (valid) encoded byte (once) to minimize unnecessary
650+
// calls to string decoding functions
651+
if (code === 37/*%*/) {
652+
encodeCheck = 1;
653+
} else if (encodeCheck > 0) {
654+
// eslint-disable-next-line no-extra-boolean-cast
655+
if (!!isHexTable[code]) {
656+
if (++encodeCheck === 3)
657+
encoded = true;
658+
} else {
659+
encodeCheck = 0;
660+
}
661+
}
592662
}
593663
}
594-
return values;
664+
665+
// Deal with any leftover key or value data
666+
667+
// There is a trailing &. No more processing is needed.
668+
if (pairStart === i)
669+
return out;
670+
671+
if (lastPos < i)
672+
buf += qs.slice(lastPos, i);
673+
if (encoded)
674+
buf = querystring.unescape(buf);
675+
out.push(buf);
676+
677+
// If `buf` is the key, add an empty value.
678+
if (!seenSep)
679+
out.push('');
680+
681+
return out;
595682
}
596683

597684
// Adapted from querystring's implementation.

test/fixtures/url-searchparams.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module.exports = [
2+
['', '', []],
3+
[
4+
'foo=918854443121279438895193',
5+
'foo=918854443121279438895193',
6+
[['foo', '918854443121279438895193']]
7+
],
8+
['foo=bar', 'foo=bar', [['foo', 'bar']]],
9+
['foo=bar&foo=quux', 'foo=bar&foo=quux', [['foo', 'bar'], ['foo', 'quux']]],
10+
['foo=1&bar=2', 'foo=1&bar=2', [['foo', '1'], ['bar', '2']]],
11+
[
12+
"my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F",
13+
'my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F',
14+
[['my weird field', 'q1!2"\'w$5&7/z8)?']]
15+
],
16+
['foo%3Dbaz=bar', 'foo%3Dbaz=bar', [['foo=baz', 'bar']]],
17+
['foo=baz=bar', 'foo=baz%3Dbar', [['foo', 'baz=bar']]],
18+
[
19+
'str=foo&arr=1&somenull&arr=2&undef=&arr=3',
20+
'str=foo&arr=1&somenull=&arr=2&undef=&arr=3',
21+
[
22+
['str', 'foo'],
23+
['arr', '1'],
24+
['somenull', ''],
25+
['arr', '2'],
26+
['undef', ''],
27+
['arr', '3']
28+
]
29+
],
30+
[' foo = bar ', '+foo+=+bar+', [[' foo ', ' bar ']]],
31+
['foo=%zx', 'foo=%25zx', [['foo', '%zx']]],
32+
['foo=%EF%BF%BD', 'foo=%EF%BF%BD', [['foo', '\ufffd']]],
33+
// See: https://github.com/joyent/node/issues/3058
34+
['foo&bar=baz', 'foo=&bar=baz', [['foo', ''], ['bar', 'baz']]],
35+
['a=b&c&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]],
36+
['a=b&c=&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]],
37+
['a=b&=c&d=e', 'a=b&=c&d=e', [['a', 'b'], ['', 'c'], ['d', 'e']]],
38+
['a=b&=&d=e', 'a=b&=&d=e', [['a', 'b'], ['', ''], ['d', 'e']]],
39+
['&&foo=bar&&', 'foo=bar', [['foo', 'bar']]],
40+
['&', '', []],
41+
['&&&&', '', []],
42+
['&=&', '=', [['', '']]],
43+
['&=&=', '=&=', [['', ''], ['', '']]],
44+
['=', '=', [['', '']]],
45+
['+', '+=', [[' ', '']]],
46+
['+=', '+=', [[' ', '']]],
47+
['=+', '=+', [['', ' ']]],
48+
['+=&', '+=', [[' ', '']]],
49+
['a&&b', 'a=&b=', [['a', ''], ['b', '']]],
50+
['a=a&&b=b', 'a=a&b=b', [['a', 'a'], ['b', 'b']]],
51+
['&a', 'a=', [['a', '']]],
52+
['&=', '=', [['', '']]],
53+
['a&a&', 'a=&a=', [['a', ''], ['a', '']]],
54+
['a&a&a&', 'a=&a=&a=', [['a', ''], ['a', ''], ['a', '']]],
55+
['a&a&a&a&', 'a=&a=&a=&a=', [['a', ''], ['a', ''], ['a', ''], ['a', '']]],
56+
['a=&a=value&a=', 'a=&a=value&a=', [['a', ''], ['a', 'value'], ['a', '']]],
57+
['foo%20bar=baz%20quux', 'foo+bar=baz+quux', [['foo bar', 'baz quux']]],
58+
['+foo=+bar', '+foo=+bar', [[' foo', ' bar']]],
59+
[
60+
// fake percent encoding
61+
'foo=%©ar&baz=%A©uux&xyzzy=%©ud',
62+
'foo=%25%C2%A9ar&baz=%25A%C2%A9uux&xyzzy=%25%C2%A9ud',
63+
[['foo', '%©ar'], ['baz', '%A©uux'], ['xyzzy', '%©ud']]
64+
],
65+
// always preserve order of key-value pairs
66+
['a=1&b=2&a=3', 'a=1&b=2&a=3', [['a', '1'], ['b', '2'], ['a', '3']]],
67+
['?a', '%3Fa=', [['?a', '']]]
68+
];

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

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict';
22

3-
require('../common');
3+
const common = require('../common');
44
const assert = require('assert');
5-
const URL = require('url').URL;
5+
const path = require('path');
6+
const { URL, URLSearchParams } = require('url');
67

78
// Tests below are not from WPT.
89
const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' +
@@ -77,3 +78,27 @@ assert.throws(() => sp.forEach(1),
7778

7879
m.search = '?a=a&b=b';
7980
assert.strictEqual(sp.toString(), 'a=a&b=b');
81+
82+
const tests = require(path.join(common.fixturesDir, 'url-searchparams.js'));
83+
84+
for (const [input, expected, parsed] of tests) {
85+
if (input[0] !== '?') {
86+
const sp = new URLSearchParams(input);
87+
assert.strictEqual(String(sp), expected);
88+
assert.deepStrictEqual(Array.from(sp), parsed);
89+
90+
m.search = input;
91+
assert.strictEqual(String(m.searchParams), expected);
92+
assert.deepStrictEqual(Array.from(m.searchParams), parsed);
93+
}
94+
95+
{
96+
const sp = new URLSearchParams(`?${input}`);
97+
assert.strictEqual(String(sp), expected);
98+
assert.deepStrictEqual(Array.from(sp), parsed);
99+
100+
m.search = `?${input}`;
101+
assert.strictEqual(String(m.searchParams), expected);
102+
assert.deepStrictEqual(Array.from(m.searchParams), parsed);
103+
}
104+
}

0 commit comments

Comments
 (0)