Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

url: spec-compliant URLSearchParams serializer #11626

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const inputs = require('../fixtures/url-inputs.js').searchParams;
const bench = common.createBenchmark(main, {
type: Object.keys(inputs),
method: ['legacy', 'whatwg'],
n: [1e5]
n: [1e6]
});

function useLegacy(n, input, prop) {
Expand Down
105 changes: 93 additions & 12 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const util = require('util');
const { StorageObject } = require('internal/querystring');
const { hexTable, StorageObject } = require('internal/querystring');
const binding = process.binding('url');
const context = Symbol('context');
const cannotBeBase = Symbol('cannot-be-base');
Expand Down Expand Up @@ -597,18 +597,99 @@ function getParamsFromObject(obj) {
return values;
}

function getObjectFromParams(array) {
const obj = new StorageObject();
for (var i = 0; i < array.length; i += 2) {
const name = array[i];
const value = array[i + 1];
if (obj[name]) {
obj[name].push(value);
} else {
obj[name] = [value];
// Adapted from querystring's implementation.
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
const noEscape = [
//0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 // 0x70 - 0x7F
];

// Special version of hexTable that uses `+` for U+0020 SPACE.
const paramHexTable = hexTable.slice();
paramHexTable[0x20] = '+';

function escapeParam(str) {
const len = str.length;
if (len === 0)
return '';

var out = '';
var lastPos = 0;

for (var i = 0; i < len; i++) {
var c = str.charCodeAt(i);

// ASCII
if (c < 0x80) {
if (noEscape[c] === 1)
continue;
if (lastPos < i)
out += str.slice(lastPos, i);
lastPos = i + 1;
out += paramHexTable[c];
continue;
}

if (lastPos < i)
out += str.slice(lastPos, i);

// Multi-byte characters ...
if (c < 0x800) {
lastPos = i + 1;
out += paramHexTable[0xC0 | (c >> 6)] +
paramHexTable[0x80 | (c & 0x3F)];
continue;
}
if (c < 0xD800 || c >= 0xE000) {
lastPos = i + 1;
out += paramHexTable[0xE0 | (c >> 12)] +
paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
paramHexTable[0x80 | (c & 0x3F)];
continue;
}
// Surrogate pair
++i;
var c2;
if (i < len)
c2 = str.charCodeAt(i) & 0x3FF;
else {
// This branch should never happen because all URLSearchParams entries
// should already be converted to USVString. But, included for
// completion's sake anyway.
c2 = 0;
}
lastPos = i + 1;
c = 0x10000 + (((c & 0x3FF) << 10) | c2);
out += paramHexTable[0xF0 | (c >> 18)] +
paramHexTable[0x80 | ((c >> 12) & 0x3F)] +
paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
paramHexTable[0x80 | (c & 0x3F)];
}
return obj;
if (lastPos === 0)
return str;
if (lastPos < len)
return out + str.slice(lastPos);
return out;
}

// application/x-www-form-urlencoded serializer
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
function serializeParams(array) {
const len = array.length;
if (len === 0)
return '';

var output = `${escapeParam(array[0])}=${escapeParam(array[1])}`;
for (var i = 2; i < len; i += 2)
output += `&${escapeParam(array[i])}=${escapeParam(array[i + 1])}`;
return output;
}

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

return querystring.stringify(getObjectFromParams(this[searchParams]));
return serializeParams(this[searchParams]);
}
});

Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/url-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4639,7 +4639,7 @@ module.exports =
"port": "",
"pathname": "/foo/bar",
"search": "??a=b&c=d",
// "searchParams": "%3Fa=b&c=d",
"searchParams": "%3Fa=b&c=d",
"hash": ""
},
"# Scheme only",
Expand Down
6 changes: 3 additions & 3 deletions test/parallel/test-whatwg-url-constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ function runURLSearchParamTests() {
// And in the other direction, altering searchParams propagates
// back to 'search'.
searchParams.append('i', ' j ')
// assert_equals(url.search, '?e=f&g=h&i=+j+')
// assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+')
assert_equals(url.search, '?e=f&g=h&i=+j+')
assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+')
assert_equals(searchParams.get('i'), ' j ')

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

var url2 = bURL('http://example.org/file??a=b&c=d')
Expand Down
4 changes: 2 additions & 2 deletions test/parallel/test-whatwg-url-searchparams-constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const {
/* eslint-disable */
var params; // Strict mode fix for WPT.
/* WPT Refs:
https://github.com/w3c/web-platform-tests/blob/405394a/url/urlsearchparams-constructor.html
https://github.com/w3c/web-platform-tests/blob/e94c604916/url/urlsearchparams-constructor.html
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
*/
test(function() {
Expand Down Expand Up @@ -154,7 +154,7 @@ test(function() {
}, "Constructor with sequence of sequences of strings");

[
// { "input": {"+": "%C2"}, "output": [[" ", "\uFFFD"]], "name": "object with +" },
{ "input": {"+": "%C2"}, "output": [["+", "%C2"]], "name": "object with +" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update the Refs SHA? This fix was in e94c604

{ "input": {c: "x", a: "?"}, "output": [["c", "x"], ["a", "?"]], "name": "object with two keys" },
{ "input": [["c", "x"], ["a", "?"]], "output": [["c", "x"], ["a", "?"]], "name": "array with two keys" }
].forEach((val) => {
Expand Down
20 changes: 10 additions & 10 deletions test/parallel/test-whatwg-url-searchparams-stringifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const { test, assert_equals } = common.WPT;
https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-stringifier.html
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
*/
// test(function() {
// var params = new URLSearchParams();
// params.append('a', 'b c');
// assert_equals(params + '', 'a=b+c');
// params.delete('a');
// params.append('a b', 'c');
// assert_equals(params + '', 'a+b=c');
// }, 'Serialize space');
test(function() {
var params = new URLSearchParams();
params.append('a', 'b c');
assert_equals(params + '', 'a=b+c');
params.delete('a');
params.append('a b', 'c');
assert_equals(params + '', 'a+b=c');
}, 'Serialize space');

test(function() {
var params = new URLSearchParams();
Expand Down Expand Up @@ -114,8 +114,8 @@ test(function() {
var params;
params = new URLSearchParams('a=b&c=d&&e&&');
assert_equals(params.toString(), 'a=b&c=d&e=');
// params = new URLSearchParams('a = b &a=b&c=d%20');
// assert_equals(params.toString(), 'a+=+b+&a=b&c=d+');
params = new URLSearchParams('a = b &a=b&c=d%20');
assert_equals(params.toString(), 'a+=+b+&a=b&c=d+');
// The lone '=' _does_ survive the roundtrip.
params = new URLSearchParams('a=&a=b');
assert_equals(params.toString(), 'a=&a=b');
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-whatwg-url-searchparams.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const URL = require('url').URL;
// Tests below are not from WPT.
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';
'&a=%5Bobject+Object%5D';
const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00',
'\uD83D\uDE00', '\uDE00\uD83D', {}];
const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD',
Expand Down