Skip to content

Commit c515a98

Browse files
committed
url: spec-compliant URLSearchParams parser
The entire `URLSearchParams` class is now fully spec-compliant. PR-URL: #11858Fixes: #10821 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
1 parent c98a802 commit c515a98

4 files changed

Lines changed: 197 additions & 17 deletions

File tree

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

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

1313
functionuseLegacy(n,input){

‎lib/internal/url.js‎

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
'use strict';
22

33
constutil=require('util');
4-
const{ hexTable, StorageObject }=require('internal/querystring');
4+
const{
5+
hexTable,
6+
isHexTable,
7+
StorageObject
8+
}=require('internal/querystring');
59
constbinding=process.binding('url');
610
constcontext=Symbol('context');
711
constcannotBeBase=Symbol('cannot-be-base');
@@ -585,23 +589,106 @@ function initSearchParams(url, init) {
585589
url[searchParams]=[];
586590
return;
587591
}
588-
url[searchParams]=getParamsFromObject(querystring.parse(init));
592+
url[searchParams]=parseParams(init);
589593
}
590594

591-
functiongetParamsFromObject(obj){
592-
constkeys=Object.keys(obj);
593-
constvalues=[];
594-
for(vari=0;i<keys.length;i++){
595-
constname=keys[i];
596-
constvalue=obj[name];
597-
if(Array.isArray(value)){
598-
for(constitemofvalue)
599-
values.push(name,item);
600-
}else{
601-
values.push(name,value);
595+
// application/x-www-form-urlencoded parser
596+
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
597+
functionparseParams(qs){
598+
constout=[];
599+
varpairStart=0;
600+
varlastPos=0;
601+
varseenSep=false;
602+
varbuf='';
603+
varencoded=false;
604+
varencodeCheck=0;
605+
vari;
606+
for(i=0;i<qs.length;++i){
607+
constcode=qs.charCodeAt(i);
608+
609+
// Try matching key/value pair separator
610+
if(code===38/*&*/){
611+
if(pairStart===i){
612+
// We saw an empty substring between pair separators
613+
lastPos=pairStart=i+1;
614+
continue;
615+
}
616+
617+
if(lastPos<i)
618+
buf+=qs.slice(lastPos,i);
619+
if(encoded)
620+
buf=querystring.unescape(buf);
621+
out.push(buf);
622+
623+
// If `buf` is the key, add an empty value.
624+
if(!seenSep)
625+
out.push('');
626+
627+
seenSep=false;
628+
buf='';
629+
encoded=false;
630+
encodeCheck=0;
631+
lastPos=pairStart=i+1;
632+
continue;
633+
}
634+
635+
// Try matching key/value separator (e.g. '=') if we haven't already
636+
if(!seenSep&&code===61/*=*/){
637+
// Key/value separator match!
638+
if(lastPos<i)
639+
buf+=qs.slice(lastPos,i);
640+
if(encoded)
641+
buf=querystring.unescape(buf);
642+
out.push(buf);
643+
644+
seenSep=true;
645+
buf='';
646+
encoded=false;
647+
encodeCheck=0;
648+
lastPos=i+1;
649+
continue;
650+
}
651+
652+
// Handle + and percent decoding.
653+
if(code===43/*+*/){
654+
if(lastPos<i)
655+
buf+=qs.slice(lastPos,i);
656+
buf+=' ';
657+
lastPos=i+1;
658+
}elseif(!encoded){
659+
// Try to match an (valid) encoded byte (once) to minimize unnecessary
660+
// calls to string decoding functions
661+
if(code===37/*%*/){
662+
encodeCheck=1;
663+
}elseif(encodeCheck>0){
664+
// eslint-disable-next-line no-extra-boolean-cast
665+
if(!!isHexTable[code]){
666+
if(++encodeCheck===3)
667+
encoded=true;
668+
}else{
669+
encodeCheck=0;
670+
}
671+
}
602672
}
603673
}
604-
returnvalues;
674+
675+
// Deal with any leftover key or value data
676+
677+
// There is a trailing &. No more processing is needed.
678+
if(pairStart===i)
679+
returnout;
680+
681+
if(lastPos<i)
682+
buf+=qs.slice(lastPos,i);
683+
if(encoded)
684+
buf=querystring.unescape(buf);
685+
out.push(buf);
686+
687+
// If `buf` is the key, add an empty value.
688+
if(!seenSep)
689+
out.push('');
690+
691+
returnout;
605692
}
606693

607694
// Adapted from querystring's implementation.

‎test/fixtures/url-searchparams.js‎

Lines changed: 68 additions & 0 deletions
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‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict';
22

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

78
// Tests below are not from WPT.
89
constserialized='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+
consttests=require(path.join(common.fixturesDir,'url-searchparams.js'));
83+
84+
for(const[input,expected,parsed]oftests){
85+
if(input[0]!=='?'){
86+
constsp=newURLSearchParams(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+
constsp=newURLSearchParams(`?${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)