Skip to content

Commit 20eba83

Browse files
committed
feat: ensure functionality with node.js tests
1 parent 212c51c commit 20eba83

4 files changed

Lines changed: 148 additions & 44 deletions

File tree

lib/errors.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

lib/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
"use strict";
22

3-
const errors = require("./errors");
43
const parse = require("./parse");
54

65
module.exports = {
7-
errors,
86
parse,
97
};

lib/parse.js

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
"use strict";
22

3-
const errors = require("./errors");
3+
/**
4+
* @callback
5+
* @param {string} value
6+
* @returns {string}
7+
*/
8+
function decode(value) {
9+
try {
10+
return decodeURIComponent(value);
11+
} catch {
12+
return value;
13+
}
14+
}
415

516
/**
617
* @callback parse
718
* @param {string} input
819
*/
920
function parse(input) {
21+
const result = Object.create(null);
22+
1023
if (typeof input !== "string") {
11-
throw errors.codes.FST_QS_INVALID_INPUT;
24+
return result;
1225
}
13-
const result = Object.create(null);
1426

1527
let key = "";
1628
let value = "";
17-
let separatorIndex = 0;
18-
let equalityIndex = 0;
19-
let shouldEncode = false;
29+
let startingIndex = -1;
30+
let equalityIndex = -1;
31+
let shouldDecode = false;
2032
let hasPlus = false;
2133

2234
// Have a boundary of input.length + 1 to access last pair inside the loop.
@@ -25,30 +37,34 @@ function parse(input) {
2537

2638
// Handle '&' and end of line to pass the current values to result
2739
if (c === 38 || isNaN(c)) {
28-
const hasOnlyKey = equalityIndex <= separatorIndex;
29-
const keySize =
30-
hasOnlyKey ? i - separatorIndex : equalityIndex - separatorIndex;
31-
32-
if (keySize > 0) {
33-
// Accept empty values, if key size is positive
34-
if (hasOnlyKey) {
35-
key = input.slice(separatorIndex, i);
36-
value = "";
37-
} else {
38-
key = input.slice(separatorIndex, equalityIndex);
39-
value = input.slice(equalityIndex + 1, i);
40-
}
40+
// Check if the current range consist of a single key
41+
if (equalityIndex <= startingIndex) {
42+
key = input.slice(startingIndex + 1, i);
43+
value = "";
44+
}
45+
// Check if the current range consist of only values.
46+
else if (equalityIndex === startingIndex) {
47+
key = "";
48+
value = input.slice(equalityIndex + 1, i);
49+
}
50+
// Range consist of both key and value
51+
else {
52+
key = input.slice(startingIndex + 1, equalityIndex);
53+
value = input.slice(equalityIndex + 1, i);
54+
}
4155

56+
// Add key/value pair only if the range size is greater than 1; a.k.a. contains at least "="
57+
if (i - startingIndex > 1) {
4258
// Optimization: Replace '+' with space
4359
if (hasPlus) {
4460
key = key.replace(/\+/g, " ");
4561
value = value.replace(/\+/g, " ");
4662
}
4763

4864
// Optimization: Do not decode if it's not necessary.
49-
if (shouldEncode) {
50-
key = decodeURIComponent(key);
51-
value = decodeURIComponent(value);
65+
if (shouldDecode) {
66+
key = decode(key);
67+
value = decode(value);
5268
}
5369

5470
if (result[key] === undefined) {
@@ -67,22 +83,27 @@ function parse(input) {
6783
// Reset reading key value pairs
6884
key = "";
6985
value = "";
70-
separatorIndex = i + 1;
71-
equalityIndex = i + 1;
72-
shouldEncode = false;
86+
startingIndex = i;
87+
equalityIndex = i;
88+
shouldDecode = false;
7389
hasPlus = false;
7490
}
7591
// Check '='
7692
else if (c === 61) {
77-
equalityIndex = i;
93+
// If '=' character occurs again, we should decode the input.
94+
if (equalityIndex > startingIndex) {
95+
shouldDecode = true;
96+
} else {
97+
equalityIndex = i;
98+
}
7899
}
79100
// Check '+', and replace it with empty space.
80101
else if (c === 43) {
81102
hasPlus = true;
82103
}
83104
// Check '%' character for encoding
84105
else if (c === 37) {
85-
shouldEncode = true;
106+
shouldDecode = true;
86107
}
87108
}
88109

test/basics.test.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import qs from "../lib";
22
import { test, assert } from "vitest";
33
import vm from "node:vm";
44

5+
function createWithNoPrototype(properties) {
6+
const noProto = Object.create(null);
7+
properties.forEach((property) => {
8+
noProto[property.key] = property.value;
9+
});
10+
return noProto;
11+
}
12+
513
const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})');
614
const qsNoMungeTestCases = [
715
["", {}],
@@ -19,15 +27,105 @@ const qsNoMungeTestCases = [
1927
],
2028
["trololol=yes&lololo=no", { trololol: "yes", lololo: "no" }],
2129
];
30+
const qsTestCases = [
31+
[
32+
"__proto__=1",
33+
"__proto__=1",
34+
createWithNoPrototype([{ key: "__proto__", value: "1" }]),
35+
],
36+
[
37+
"__defineGetter__=asdf",
38+
"__defineGetter__=asdf",
39+
JSON.parse('{"__defineGetter__":"asdf"}'),
40+
],
41+
[
42+
"foo=918854443121279438895193",
43+
"foo=918854443121279438895193",
44+
{ foo: "918854443121279438895193" },
45+
],
46+
["foo=bar", "foo=bar", { foo: "bar" }],
47+
["foo=bar&foo=quux", "foo=bar&foo=quux", { foo: ["bar", "quux"] }],
48+
["foo=1&bar=2", "foo=1&bar=2", { foo: "1", bar: "2" }],
49+
[
50+
"my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
51+
"my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F",
52+
{ "my weird field": "q1!2\"'w$5&7/z8)?" },
53+
],
54+
["foo%3Dbaz=bar", "foo%3Dbaz=bar", { "foo=baz": "bar" }],
55+
["foo=baz=bar", "foo=baz%3Dbar", { foo: "baz=bar" }],
56+
[
57+
"str=foo&arr=1&arr=2&arr=3&somenull=&undef=",
58+
"str=foo&arr=1&arr=2&arr=3&somenull=&undef=",
59+
{
60+
str: "foo",
61+
arr: ["1", "2", "3"],
62+
somenull: "",
63+
undef: "",
64+
},
65+
],
66+
[" foo = bar ", "%20foo%20=%20bar%20", { " foo ": " bar " }],
67+
["foo=%zx", "foo=%25zx", { foo: "%zx" }],
68+
["foo=%EF%BF%BD", "foo=%EF%BF%BD", { foo: "\ufffd" }],
69+
// See: https://github.com/joyent/node/issues/1707
70+
[
71+
"hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz",
72+
"hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz",
73+
{
74+
hasOwnProperty: "x",
75+
toString: "foo",
76+
valueOf: "bar",
77+
__defineGetter__: "baz",
78+
},
79+
],
80+
// See: https://github.com/joyent/node/issues/3058
81+
["foo&bar=baz", "foo=&bar=baz", { foo: "", bar: "baz" }],
82+
["a=b&c&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }],
83+
["a=b&c=&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }],
84+
["a=b&=c&d=e", "a=b&=c&d=e", { a: "b", "": "c", d: "e" }],
85+
["a=b&=&c=d", "a=b&=&c=d", { a: "b", "": "", c: "d" }],
86+
["&&foo=bar&&", "foo=bar", { foo: "bar" }],
87+
["&", "", {}],
88+
["&&&&", "", {}],
89+
["&=&", "=", { "": "" }],
90+
["&=&=", "=&=", { "": ["", ""] }],
91+
["=", "=", { "": "" }],
92+
["+", "%20=", { " ": "" }],
93+
["+=", "%20=", { " ": "" }],
94+
["+&", "%20=", { " ": "" }],
95+
["=+", "=%20", { "": " " }],
96+
["+=&", "%20=", { " ": "" }],
97+
["a&&b", "a=&b=", { a: "", b: "" }],
98+
["a=a&&b=b", "a=a&b=b", { a: "a", b: "b" }],
99+
["&a", "a=", { a: "" }],
100+
["&=", "=", { "": "" }],
101+
["a&a&", "a=&a=", { a: ["", ""] }],
102+
["a&a&a&", "a=&a=&a=", { a: ["", "", ""] }],
103+
["a&a&a&a&", "a=&a=&a=&a=", { a: ["", "", "", ""] }],
104+
["a=&a=value&a=", "a=&a=value&a=", { a: ["", "value", ""] }],
105+
["foo+bar=baz+quux", "foo%20bar=baz%20quux", { "foo bar": "baz quux" }],
106+
["+foo=+bar", "%20foo=%20bar", { " foo": " bar" }],
107+
["a+", "a%20=", { "a ": "" }],
108+
["=a+", "=a%20", { "": "a " }],
109+
["a+&", "a%20=", { "a ": "" }],
110+
["=a+&", "=a%20", { "": "a " }],
111+
["%20+", "%20%20=", { " ": "" }],
112+
["=%20+", "=%20%20", { "": " " }],
113+
["%20+&", "%20%20=", { " ": "" }],
114+
["=%20+&", "=%20%20", { "": " " }],
115+
[null, "", {}],
116+
[undefined, "", {}],
117+
];
22118

23119
test("should parse the basics", () => {
24120
qsNoMungeTestCases.forEach((t) => {
25121
assert.deepEqual(qs.parse(t[0]), t[1]);
26122
});
27123
});
28124

29-
test("should throw error on invalid type", () => {
30-
assert.throws(() => qs.parse(5), "Invalid Input");
125+
test("should succeed on node.js tests", () => {
126+
qsTestCases.forEach((t) => {
127+
assert.deepEqual(qs.parse(t[0]), t[2], t[0]);
128+
});
31129
});
32130

33131
test("handles & on first/last character", () => {

0 commit comments

Comments
 (0)