Skip to content

Commit d77a758

Browse files
committed
url: spec-compliant URLSearchParams serializer
PR-URL: #11626 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
1 parent 92bcc13 commit d77a758

7 files changed

+111
-30
lines changed

benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.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, prop) {

lib/internal/url.js

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

33
const util = require('util');
4-
const { StorageObject } = require('internal/querystring');
4+
const { hexTable, StorageObject } = require('internal/querystring');
55
const binding = process.binding('url');
66
const context = Symbol('context');
77
const cannotBeBase = Symbol('cannot-be-base');
@@ -597,18 +597,99 @@ function getParamsFromObject(obj) {
597597
return values;
598598
}
599599

600-
function getObjectFromParams(array) {
601-
const obj = new StorageObject();
602-
for (var i = 0; i < array.length; i += 2) {
603-
const name = array[i];
604-
const value = array[i + 1];
605-
if (obj[name]) {
606-
obj[name].push(value);
607-
} else {
608-
obj[name] = [value];
600+
// Adapted from querystring's implementation.
601+
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
602+
const noEscape = [
603+
//0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
604+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
605+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
606+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
607+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F
608+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
609+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
610+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
611+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 // 0x70 - 0x7F
612+
];
613+
614+
// Special version of hexTable that uses `+` for U+0020 SPACE.
615+
const paramHexTable = hexTable.slice();
616+
paramHexTable[0x20] = '+';
617+
618+
function escapeParam(str) {
619+
const len = str.length;
620+
if (len === 0)
621+
return '';
622+
623+
var out = '';
624+
var lastPos = 0;
625+
626+
for (var i = 0; i < len; i++) {
627+
var c = str.charCodeAt(i);
628+
629+
// ASCII
630+
if (c < 0x80) {
631+
if (noEscape[c] === 1)
632+
continue;
633+
if (lastPos < i)
634+
out += str.slice(lastPos, i);
635+
lastPos = i + 1;
636+
out += paramHexTable[c];
637+
continue;
638+
}
639+
640+
if (lastPos < i)
641+
out += str.slice(lastPos, i);
642+
643+
// Multi-byte characters ...
644+
if (c < 0x800) {
645+
lastPos = i + 1;
646+
out += paramHexTable[0xC0 | (c >> 6)] +
647+
paramHexTable[0x80 | (c & 0x3F)];
648+
continue;
649+
}
650+
if (c < 0xD800 || c >= 0xE000) {
651+
lastPos = i + 1;
652+
out += paramHexTable[0xE0 | (c >> 12)] +
653+
paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
654+
paramHexTable[0x80 | (c & 0x3F)];
655+
continue;
609656
}
657+
// Surrogate pair
658+
++i;
659+
var c2;
660+
if (i < len)
661+
c2 = str.charCodeAt(i) & 0x3FF;
662+
else {
663+
// This branch should never happen because all URLSearchParams entries
664+
// should already be converted to USVString. But, included for
665+
// completion's sake anyway.
666+
c2 = 0;
667+
}
668+
lastPos = i + 1;
669+
c = 0x10000 + (((c & 0x3FF) << 10) | c2);
670+
out += paramHexTable[0xF0 | (c >> 18)] +
671+
paramHexTable[0x80 | ((c >> 12) & 0x3F)] +
672+
paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
673+
paramHexTable[0x80 | (c & 0x3F)];
610674
}
611-
return obj;
675+
if (lastPos === 0)
676+
return str;
677+
if (lastPos < len)
678+
return out + str.slice(lastPos);
679+
return out;
680+
}
681+
682+
// application/x-www-form-urlencoded serializer
683+
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
684+
function serializeParams(array) {
685+
const len = array.length;
686+
if (len === 0)
687+
return '';
688+
689+
var output = `${escapeParam(array[0])}=${escapeParam(array[1])}`;
690+
for (var i = 2; i < len; i += 2)
691+
output += `&${escapeParam(array[i])}=${escapeParam(array[i + 1])}`;
692+
return output;
612693
}
613694

614695
// Mainly to mitigate func-name-matching ESLint rule
@@ -993,7 +1074,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
9931074
throw new TypeError('Value of `this` is not a URLSearchParams');
9941075
}
9951076

996-
return querystring.stringify(getObjectFromParams(this[searchParams]));
1077+
return serializeParams(this[searchParams]);
9971078
}
9981079
});
9991080

test/fixtures/url-tests.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4639,7 +4639,7 @@ module.exports =
46394639
"port": "",
46404640
"pathname": "/foo/bar",
46414641
"search": "??a=b&c=d",
4642-
// "searchParams": "%3Fa=b&c=d",
4642+
"searchParams": "%3Fa=b&c=d",
46434643
"hash": ""
46444644
},
46454645
"# Scheme only",

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ function runURLSearchParamTests() {
120120
// And in the other direction, altering searchParams propagates
121121
// back to 'search'.
122122
searchParams.append('i', ' j ')
123-
// assert_equals(url.search, '?e=f&g=h&i=+j+')
124-
// assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+')
123+
assert_equals(url.search, '?e=f&g=h&i=+j+')
124+
assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+')
125125
assert_equals(searchParams.get('i'), ' j ')
126126

127127
searchParams.set('e', 'updated')
128-
// assert_equals(url.search, '?e=updated&g=h&i=+j+')
128+
assert_equals(url.search, '?e=updated&g=h&i=+j+')
129129
assert_equals(searchParams.get('e'), 'updated')
130130

131131
var url2 = bURL('http://example.org/file??a=b&c=d')

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const {
1111
/* eslint-disable */
1212
var params; // Strict mode fix for WPT.
1313
/* WPT Refs:
14-
https://github.com/w3c/web-platform-tests/blob/405394a/url/urlsearchparams-constructor.html
14+
https://github.com/w3c/web-platform-tests/blob/e94c604916/url/urlsearchparams-constructor.html
1515
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
1616
*/
1717
test(function() {
@@ -154,7 +154,7 @@ test(function() {
154154
}, "Constructor with sequence of sequences of strings");
155155

156156
[
157-
// { "input": {"+": "%C2"}, "output": [[" ", "\uFFFD"]], "name": "object with +" },
157+
{ "input": {"+": "%C2"}, "output": [["+", "%C2"]], "name": "object with +" },
158158
{ "input": {c: "x", a: "?"}, "output": [["c", "x"], ["a", "?"]], "name": "object with two keys" },
159159
{ "input": [["c", "x"], ["a", "?"]], "output": [["c", "x"], ["a", "?"]], "name": "array with two keys" }
160160
].forEach((val) => {

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

+10-10
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ const { test, assert_equals } = common.WPT;
1010
https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-stringifier.html
1111
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
1212
*/
13-
// test(function() {
14-
// var params = new URLSearchParams();
15-
// params.append('a', 'b c');
16-
// assert_equals(params + '', 'a=b+c');
17-
// params.delete('a');
18-
// params.append('a b', 'c');
19-
// assert_equals(params + '', 'a+b=c');
20-
// }, 'Serialize space');
13+
test(function() {
14+
var params = new URLSearchParams();
15+
params.append('a', 'b c');
16+
assert_equals(params + '', 'a=b+c');
17+
params.delete('a');
18+
params.append('a b', 'c');
19+
assert_equals(params + '', 'a+b=c');
20+
}, 'Serialize space');
2121

2222
test(function() {
2323
var params = new URLSearchParams();
@@ -114,8 +114,8 @@ test(function() {
114114
var params;
115115
params = new URLSearchParams('a=b&c=d&&e&&');
116116
assert_equals(params.toString(), 'a=b&c=d&e=');
117-
// params = new URLSearchParams('a = b &a=b&c=d%20');
118-
// assert_equals(params.toString(), 'a+=+b+&a=b&c=d+');
117+
params = new URLSearchParams('a = b &a=b&c=d%20');
118+
assert_equals(params.toString(), 'a+=+b+&a=b&c=d+');
119119
// The lone '=' _does_ survive the roundtrip.
120120
params = new URLSearchParams('a=&a=b');
121121
assert_equals(params.toString(), 'a=&a=b');

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const URL = require('url').URL;
77
// Tests below are not from WPT.
88
const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' +
99
'&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD' +
10-
'&a=%5Bobject%20Object%5D';
10+
'&a=%5Bobject+Object%5D';
1111
const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00',
1212
'\uD83D\uDE00', '\uDE00\uD83D', {}];
1313
const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD',

0 commit comments

Comments
 (0)