Skip to content

Commit 223caf5

Browse files
committed
fix: simplify implementation and use node:qs tests
1 parent dcfa8b1 commit 223caf5

6 files changed

Lines changed: 121 additions & 454 deletions

File tree

lib/encoding.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use strict";
2+
/**
3+
* @function is_c0_control_percent_encoded
4+
* @description
5+
* The C0 control percent-encode set are the C0 controls and all code points greater than U+007E (~).
6+
* A C0 control is a code point in the range U+0000 NULL to U+001F INFORMATION SEPARATOR ONE, inclusive.
7+
*
8+
* [Specification]{@link https://url.spec.whatwg.org/#c0-control-percent-encode-set}
9+
*
10+
* @param {number} code_point
11+
* @returns {boolean}
12+
*/
13+
function is_c0_control_percent_encoded(code_point) {
14+
return code_point <= 0x1F || code_point > 0x7E;
15+
}
16+
17+
const query_percent_encodes = {
18+
0x20: true,
19+
0x22: true,
20+
0x23: true,
21+
0x3C: true,
22+
0x3E: true,
23+
};
24+
25+
/**
26+
* @function is_query_percent_encoded
27+
* @description The query percent-encode set is the C0 control percent-encode set and U+0020 SPACE, U+0022 ("),
28+
* U+0023 (#), U+003C (<), and U+003E (>).
29+
* [Specification]{@link https://url.spec.whatwg.org/#query-percent-encode-set}
30+
* @param {number} code
31+
* @returns {boolean}
32+
*/
33+
function is_query_percent_encoded(code) {
34+
return (
35+
typeof query_percent_encodes[
36+
code
37+
] !== "undefined" || is_c0_control_percent_encoded(code)
38+
);
39+
}
40+
41+
/**
42+
* @function percent_encode
43+
* @description To percent-encode a byte, return a string consisting of U+0025 (%), followed by two ASCII upper hex digits representing byte.
44+
* [Specification]{@link https://url.spec.whatwg.org/#percent-encode}
45+
* @param {number} code
46+
* @returns {string}
47+
*/
48+
function percent_encode(code) {
49+
const hex = code.toString(16).toUpperCase();
50+
51+
if (hex.length === 1) {
52+
return `%0${hex}`;
53+
}
54+
55+
return `%${hex}`;
56+
}
57+
58+
/**
59+
* @function is_special_query_percent_encoded
60+
* [Specification]{@link https://url.spec.whatwg.org/#special-query-percent-encode-set}
61+
* @param {number} code
62+
* @returns {boolean}
63+
*/
64+
function is_special_query_percent_encoded(code) {
65+
return code === 39 || is_query_percent_encoded(code);
66+
}
67+
68+
module.exports = {
69+
is_special_query_percent_encoded,
70+
percent_encode,
71+
};

lib/parse.js

Lines changed: 27 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,87 +7,40 @@
77
function parse(input) {
88
let result = Object.create(null);
99

10-
let currentKey = [""];
10+
let currentKey = "";
1111
let currentValue = "";
12-
let currentPairIsArray = false;
1312
let seenEqualOperator = false;
1413

1514
// Have a boundary of input.length + 1 to access last pair inside the loop.
1615
for (let i = 0; i < input.length + 1; i++) {
17-
let currentCharacter = input.charCodeAt(i);
18-
let currentKeyLength = currentKey.length;
19-
20-
if (isNaN(currentCharacter)) {
21-
currentCharacter = 38;
22-
}
23-
24-
switch (currentCharacter) {
25-
// &
26-
case 38: {
27-
let root = result;
28-
29-
for (let k = 0; k < currentKeyLength; k++) {
30-
let key = currentKey[k];
31-
32-
if (typeof root[key] === "undefined") {
33-
if (k === currentKeyLength - 1) {
34-
if (currentPairIsArray) {
35-
root[key] = [currentValue];
36-
} else {
37-
root[key] = currentValue;
38-
}
39-
} else {
40-
root[key] = {};
41-
root = root[key];
42-
}
43-
} else if (Array.isArray(root[key])) {
44-
root[key].push(currentValue);
45-
} else if (k === currentKeyLength - 1) {
46-
root[key] = [root[key], currentValue];
47-
} else {
48-
Object.assign(root[key], { [key]: {} });
49-
}
50-
}
51-
52-
// Reset reading key value pairs
53-
currentKey = [""];
54-
currentValue = "";
55-
seenEqualOperator = false;
56-
currentPairIsArray = false;
57-
break;
16+
let c = input.charCodeAt(i);
17+
18+
// Handle '&' and end of line to pass the current values to result
19+
if (c === 38 || isNaN(c)) {
20+
// Disallow empty key values.
21+
if (currentKey.length === 0) {
22+
continue
23+
} else if (typeof result[currentKey] === "undefined") {
24+
result[currentKey] = currentValue;
25+
} else if (Array.isArray(result[currentKey])) {
26+
result[currentKey].push(currentValue);
27+
} else {
28+
result[currentKey] = [result[currentKey], currentValue];
5829
}
59-
// ']'
60-
case 93: {
61-
// Ignore ']' character, since it's already handled inside '['.
62-
break;
63-
}
64-
// '['
65-
case 91: {
66-
let nextCharacter = input.charCodeAt(i + 1);
67-
68-
// Check if input is an array. Example: hello[]=world equals to { hello: ['world'] }
69-
if (nextCharacter === 93) {
70-
currentPairIsArray = true;
71-
i++;
72-
}
73-
// Check push a new key to keys. Only applicable when key is not an array, but consists of length > 0.
74-
else {
75-
currentKey.push("");
76-
}
7730

78-
break;
79-
}
80-
// '='
81-
case 61: {
82-
seenEqualOperator = true;
83-
break;
84-
}
85-
default: {
86-
if (seenEqualOperator) {
87-
currentValue += input[i];
88-
} else {
89-
currentKey[currentKeyLength - 1] += input[i];
90-
}
31+
// Reset reading key value pairs
32+
currentKey = "";
33+
currentValue = "";
34+
seenEqualOperator = false;
35+
}
36+
// Handle equal operator
37+
else if (c === 61) {
38+
seenEqualOperator = true;
39+
} else {
40+
if (seenEqualOperator) {
41+
currentValue += input[i];
42+
} else {
43+
currentKey += input[i];
9144
}
9245
}
9346
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"author": "Yagiz Nizipli <yagiz@nizipli.com>",
2020
"license": "MIT",
2121
"devDependencies": {
22+
"@types/node": "^18.7.14",
2223
"cronometro": "^1.1.2",
2324
"qs": "^6.11.0",
2425
"query-string": "^7.1.1",

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/basics.test.ts

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,25 @@
11
import qs from "../lib";
22
import { test, assert } from "vitest";
3+
import vm from 'node:vm'
34

4-
test("parse single key value pair", () => {
5-
assert.deepEqual(qs.parse("a=s"), { a: "s" });
6-
});
7-
8-
test("parse multiple key value pair", () => {
9-
assert.deepEqual(qs.parse("hello=world&foo=bar"), {
10-
hello: "world",
11-
foo: "bar",
12-
});
13-
});
14-
15-
test("should create an array with multiple same keys", () => {
16-
assert.deepEqual(qs.parse(
17-
"language=javascript&language=typescript&sort=true",
18-
), {
19-
language: ["javascript", "typescript"],
20-
sort: "true",
21-
});
22-
});
23-
24-
test("should parse [] notation", () => {
25-
assert.deepEqual(qs.parse("a[]=b&a[]=c"), {
26-
a: ["b", "c"],
27-
});
28-
});
29-
30-
test("should parse single [] notation", () => {
31-
assert.deepEqual(qs.parse("a[]=b"), {
32-
a: ["b"],
33-
});
34-
});
5+
const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})');
6+
const qsNoMungeTestCases = [
7+
['', {}],
8+
['foo=bar&foo=baz', { 'foo': ['bar', 'baz'] }],
9+
['foo=bar&foo=baz', foreignObject],
10+
['blah=burp', { 'blah': 'burp' }],
11+
['a=!-._~\'()*', { 'a': '!-._~\'()*' }],
12+
['a=abcdefghijklmnopqrstuvwxyz', { 'a': 'abcdefghijklmnopqrstuvwxyz' }],
13+
['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', { 'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }],
14+
['a=0123456789', { 'a': '0123456789' }],
15+
['gragh=1&gragh=3&goo=2', { 'gragh': ['1', '3'], 'goo': '2' }],
16+
['frappucino=muffin&goat%5B%5D=scone&pond=moose',
17+
{ 'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose' }],
18+
['trololol=yes&lololo=no', { 'trololol': 'yes', 'lololo': 'no' }],
19+
];
3520

36-
test("parse nested key value pairs", () => {
37-
assert.deepEqual(qs.parse("foo[bar]=baz"), {
38-
foo: {
39-
bar: "baz",
40-
},
41-
});
21+
test("should parse the basics", () => {
22+
qsNoMungeTestCases.forEach((t) => {
23+
assert.deepEqual(qs.parse(t[0]), t[1])
24+
})
4225
});

0 commit comments

Comments
 (0)