qs 6.15.1 → 6.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/qs.js +23 -23
- package/eslint.config.mjs +1 -0
- package/lib/parse.js +71 -30
- package/lib/stringify.js +11 -4
- package/lib/utils.js +47 -8
- package/package.json +7 -6
- package/test/parse.js +279 -0
- package/test/stringify.js +129 -0
- package/test/utils.js +194 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "qs",
|
|
3
3
|
"description": "A querystring parser that supports nesting and arrays, with a depth limit",
|
|
4
4
|
"homepage": "https://github.com/ljharb/qs",
|
|
5
|
-
"version": "6.15.
|
|
5
|
+
"version": "6.15.3",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/ljharb/qs.git"
|
|
@@ -31,18 +31,19 @@
|
|
|
31
31
|
"node": ">=0.6"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
34
|
+
"es-define-property": "^1.0.1",
|
|
35
|
+
"side-channel": "^1.1.1"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@browserify/envify": "^6.0.0",
|
|
38
39
|
"@browserify/uglifyify": "^6.0.0",
|
|
39
|
-
"@ljharb/eslint-config": "^22.2.
|
|
40
|
+
"@ljharb/eslint-config": "^22.2.3",
|
|
40
41
|
"browserify": "^16.5.2",
|
|
41
42
|
"bundle-collapser": "^1.4.0",
|
|
42
43
|
"common-shakeify": "~1.0.0",
|
|
43
44
|
"eclint": "^2.8.1",
|
|
44
45
|
"es-value-fixtures": "^1.7.1",
|
|
45
|
-
"eslint": "^
|
|
46
|
+
"eslint": "^10.5.0",
|
|
46
47
|
"evalmd": "^0.0.19",
|
|
47
48
|
"for-each": "^0.3.5",
|
|
48
49
|
"glob": "=10.3.7",
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"jackspeak": "=2.1.1",
|
|
57
58
|
"jiti": "^0.0.0",
|
|
58
59
|
"mkdirp": "^0.5.5",
|
|
59
|
-
"mock-property": "^1.1.
|
|
60
|
+
"mock-property": "^1.1.2",
|
|
60
61
|
"module-deps": "^6.2.3",
|
|
61
62
|
"npmignore": "^0.3.5",
|
|
62
63
|
"nyc": "^10.3.2",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"qs-iconv": "^1.0.4",
|
|
65
66
|
"safe-publish-latest": "^2.0.0",
|
|
66
67
|
"safer-buffer": "^2.1.2",
|
|
67
|
-
"tape": "^5.
|
|
68
|
+
"tape": "^5.10.2",
|
|
68
69
|
"unassertify": "^3.0.1"
|
|
69
70
|
},
|
|
70
71
|
"scripts": {
|
package/test/parse.js
CHANGED
|
@@ -14,6 +14,12 @@ var hasProto = require('has-proto')();
|
|
|
14
14
|
var qs = require('../');
|
|
15
15
|
var utils = require('../lib/utils');
|
|
16
16
|
|
|
17
|
+
var characterizeParse = function characterizeParse(st, input, opts, expected, label) {
|
|
18
|
+
var result;
|
|
19
|
+
st.doesNotThrow(function () { result = qs.parse(input, opts); }, label + ': does not throw');
|
|
20
|
+
st.deepEqual(result, expected, label + ': parses to the current lenient output');
|
|
21
|
+
};
|
|
22
|
+
|
|
17
23
|
test('parse()', function (t) {
|
|
18
24
|
t.test('parses a simple string', function (st) {
|
|
19
25
|
st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
|
|
@@ -210,6 +216,9 @@ test('parse()', function (t) {
|
|
|
210
216
|
t.test('uses original key when depth = 0', function (st) {
|
|
211
217
|
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
|
|
212
218
|
st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
|
|
219
|
+
st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
|
|
220
|
+
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
|
|
221
|
+
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
|
|
213
222
|
st.end();
|
|
214
223
|
});
|
|
215
224
|
|
|
@@ -263,6 +272,94 @@ test('parse()', function (t) {
|
|
|
263
272
|
st.end();
|
|
264
273
|
});
|
|
265
274
|
|
|
275
|
+
t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
|
|
276
|
+
// A bracket pair inside a bracket group should be treated literally as part of the key
|
|
277
|
+
st.deepEqual(
|
|
278
|
+
qs.parse('search[withbracket[]]=foobar'),
|
|
279
|
+
{ search: { 'withbracket[]': 'foobar' } },
|
|
280
|
+
'treats inner [] literally when inside a bracket group'
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Single-level variant
|
|
284
|
+
st.deepEqual(
|
|
285
|
+
qs.parse('a[b[]]=c'),
|
|
286
|
+
{ a: { 'b[]': 'c' } },
|
|
287
|
+
'keeps "b[]" as a literal key'
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Nested with an array push on the outer level
|
|
291
|
+
st.deepEqual(
|
|
292
|
+
qs.parse('list[][x[]]=y'),
|
|
293
|
+
{ list: [{ 'x[]': 'y' }] },
|
|
294
|
+
'preserves inner [] while still treating outer [] as array push'
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Multiple nested bracket pairs: inner [] remains literal as part of the key
|
|
298
|
+
st.deepEqual(
|
|
299
|
+
qs.parse('a[b[c[]]]=d'),
|
|
300
|
+
{ a: { 'b[c[]]': 'd' } },
|
|
301
|
+
'treats "b[c[]]" as a literal key inside the bracket group'
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
|
|
305
|
+
st.deepEqual(
|
|
306
|
+
qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
|
|
307
|
+
{ a: { 'b[c[]]': { '[d]': 'e' } } },
|
|
308
|
+
'respects depth: 1 and preserves literal inner [] in the parsed key'
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Unterminated inner bracket group is wrapped as a literal remainder segment
|
|
312
|
+
st.deepEqual(
|
|
313
|
+
qs.parse('a[[]b=c'),
|
|
314
|
+
{ a: { '[[]b': 'c' } },
|
|
315
|
+
'handles unterminated inner bracket groups without throwing'
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
st.end();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
t.test('currently parses unbalanced bracket keys after a parent leniently to literal segments (issue #558)', function (st) {
|
|
322
|
+
characterizeParse(st, 'a[bc=v', undefined, { a: { '[bc': 'v' } }, 'unclosed group after a parent');
|
|
323
|
+
characterizeParse(st, 'a[=v', undefined, { a: { '[': 'v' } }, 'bare unclosed bracket after a parent');
|
|
324
|
+
characterizeParse(st, 'a[b][c=v', undefined, { a: { b: { '[c': 'v' } } }, 'unclosed group after a valid one');
|
|
325
|
+
characterizeParse(st, 'a[b]c[d=v', undefined, { a: { b: { '[d': 'v' } } }, 'unclosed group after text following a valid one');
|
|
326
|
+
characterizeParse(st, 'filters[customtags:Env: Prod=v', undefined, { filters: { '[customtags:Env: Prod': 'v' } }, 'the issue #558 reproduction');
|
|
327
|
+
characterizeParse(st, '][a=v', undefined, { ']': { '[a': 'v' } }, 'stray close bracket before an unclosed group');
|
|
328
|
+
characterizeParse(st, 'a][b=v', undefined, { 'a]': { '[b': 'v' } }, 'stray close bracket inside the parent');
|
|
329
|
+
st.end();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
t.test('currently parses unbalanced bracket keys containing inner brackets leniently (issue #558)', function (st) {
|
|
333
|
+
characterizeParse(st, 'a[b[c=v', undefined, { a: { '[b[c': 'v' } }, 'unclosed group containing an inner bracket');
|
|
334
|
+
characterizeParse(st, 'a[b[c]=v', undefined, { a: { '[b[c]': 'v' } }, 'unbalanced group with an inner bracket and one close');
|
|
335
|
+
characterizeParse(st, 'a[b][c[d=v', undefined, { a: { b: { '[c[d': 'v' } } }, 'unclosed inner-bracket group after a valid one');
|
|
336
|
+
st.end();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
t.test('currently parses bracket-prefixed unbalanced keys leniently (issue #558)', function (st) {
|
|
340
|
+
characterizeParse(st, '[abc=v', undefined, { '[abc': 'v' }, 'key starting with an unclosed bracket');
|
|
341
|
+
characterizeParse(st, '[[]b=v', undefined, { '[[]b': 'v' }, 'key starting with an unbalanced bracket group');
|
|
342
|
+
st.end();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
t.test('lenient unbalanced-bracket handling currently depends on the depth option (issue #558)', function (st) {
|
|
346
|
+
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 5 }, { a: { b: { d: { '[f': 'v' } } } }, 'consumes groups up to the depth budget then keeps the unclosed remainder literal');
|
|
347
|
+
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 1 }, { a: { b: { '[d]e[f': 'v' } } }, 'a lower depth keeps more of the unclosed remainder literal');
|
|
348
|
+
characterizeParse(st, 'a[bc=v', { depth: 0 }, { 'a[bc': 'v' }, 'depth 0 keeps the entire key literal');
|
|
349
|
+
st.end();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
t.test('currently parses an allowDots key with a trailing unclosed bracket leniently (issue #558)', function (st) {
|
|
353
|
+
characterizeParse(st, 'a.b[c=v', { allowDots: true }, { a: { b: { '[c': 'v' } } }, 'allowDots expands the dot then keeps the unclosed bracket literal');
|
|
354
|
+
st.end();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
t.test('valid and stray-close bracket keys are unaffected by unbalanced-bracket handling', function (st) {
|
|
358
|
+
characterizeParse(st, 'a]b=v', undefined, { 'a]b': 'v' }, 'stray close bracket with no open bracket stays a flat key');
|
|
359
|
+
characterizeParse(st, 'a[b]extra=v', undefined, { a: { b: 'v' } }, 'text after a balanced group is ignored');
|
|
360
|
+
st.end();
|
|
361
|
+
});
|
|
362
|
+
|
|
266
363
|
t.test('allows to specify array indices', function (st) {
|
|
267
364
|
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
|
|
268
365
|
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
|
|
@@ -677,6 +774,21 @@ test('parse()', function (t) {
|
|
|
677
774
|
st.end();
|
|
678
775
|
});
|
|
679
776
|
|
|
777
|
+
t.test('does not crash on multi-step circular references', function (st) {
|
|
778
|
+
var a = {};
|
|
779
|
+
a.b = { c: { d: a } };
|
|
780
|
+
|
|
781
|
+
var parsed;
|
|
782
|
+
|
|
783
|
+
st.doesNotThrow(function () {
|
|
784
|
+
parsed = qs.parse({ foo: a });
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
st.equal('foo' in parsed, true, 'parsed has "foo" property');
|
|
788
|
+
st.equal(parsed.foo.b.c.d, parsed.foo, 'the multi-step cycle is preserved');
|
|
789
|
+
st.end();
|
|
790
|
+
});
|
|
791
|
+
|
|
680
792
|
t.test('does not crash when parsing deep objects', function (st) {
|
|
681
793
|
var parsed;
|
|
682
794
|
var str = 'foo';
|
|
@@ -910,6 +1022,22 @@ test('parse()', function (t) {
|
|
|
910
1022
|
st.end();
|
|
911
1023
|
});
|
|
912
1024
|
|
|
1025
|
+
t.test('object-valued input with own `__proto__` does not mutate sub-object [[Prototype]]', function (st) {
|
|
1026
|
+
// JSON.parse creates own data `__proto__` properties (via CreateDataProperty),
|
|
1027
|
+
// which would trigger the Object.prototype.__proto__ accessor if merged via `acc[key] = value`.
|
|
1028
|
+
var out = qs.parse({
|
|
1029
|
+
'user[name]': 'alice',
|
|
1030
|
+
user: JSON.parse('{"__proto__":{"isAdmin":true}}')
|
|
1031
|
+
}, { allowPrototypes: false });
|
|
1032
|
+
|
|
1033
|
+
st.equal(out.user.name, 'alice', 'name from bracket key is preserved');
|
|
1034
|
+
st.equal(out.user.isAdmin, undefined, 'attacker-controlled inherited property is not exposed');
|
|
1035
|
+
st.equal(Object.getPrototypeOf(out.user), Object.prototype, 'sub-object [[Prototype]] is unchanged');
|
|
1036
|
+
st.equal(Object.prototype.isAdmin, undefined, 'Object.prototype is not polluted');
|
|
1037
|
+
|
|
1038
|
+
st.end();
|
|
1039
|
+
});
|
|
1040
|
+
|
|
913
1041
|
t.test('can return null objects', { skip: !hasProto }, function (st) {
|
|
914
1042
|
var expected = {
|
|
915
1043
|
__proto__: null,
|
|
@@ -1260,6 +1388,157 @@ test('parse()', function (t) {
|
|
|
1260
1388
|
sst.end();
|
|
1261
1389
|
});
|
|
1262
1390
|
|
|
1391
|
+
st.test('throws when cumulative comma + duplicate-key combine exceeds arrayLimit', function (sst) {
|
|
1392
|
+
sst['throws'](
|
|
1393
|
+
function () {
|
|
1394
|
+
qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1395
|
+
},
|
|
1396
|
+
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
|
1397
|
+
'throws when comma groups within the limit cumulatively exceed it across duplicate keys'
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
sst['throws'](
|
|
1401
|
+
function () {
|
|
1402
|
+
qs.parse('a=v,v,v,v,v&a=v,v,v,v,v&a=v,v,v,v,v', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1403
|
+
},
|
|
1404
|
+
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
|
1405
|
+
'throws on a subsequent part once the cumulative array is already over the limit'
|
|
1406
|
+
);
|
|
1407
|
+
sst.end();
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
st.test('throws when plain duplicate keys combine past arrayLimit at the boundary', function (sst) {
|
|
1411
|
+
sst['throws'](
|
|
1412
|
+
function () { qs.parse('a=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1413
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1414
|
+
'duplicate scalar keys'
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
sst['throws'](
|
|
1418
|
+
function () { qs.parse('a[]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1419
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1420
|
+
'duplicate bracket keys'
|
|
1421
|
+
);
|
|
1422
|
+
sst.end();
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
st.test('throws when mixed index and key notation merge past arrayLimit', function (sst) {
|
|
1426
|
+
sst['throws'](
|
|
1427
|
+
function () { qs.parse('a=x&a[0]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1428
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1429
|
+
'scalar then index that overflows on merge'
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
sst['throws'](
|
|
1433
|
+
function () { qs.parse('a[0]=1&a[1]=2&a=3', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1434
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1435
|
+
'indexed array then scalar that overflows on merge'
|
|
1436
|
+
);
|
|
1437
|
+
sst.end();
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
st.test('enforces arrayLimit on merge at the boundary, consistently with combine', function (sst) {
|
|
1441
|
+
sst['throws'](
|
|
1442
|
+
function () { qs.parse('a[0]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1443
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1444
|
+
'a trailing scalar merged into an at-limit array throws'
|
|
1445
|
+
);
|
|
1446
|
+
sst.deepEqual(
|
|
1447
|
+
qs.parse('a[0]=x&a=y', { arrayLimit: 1 }),
|
|
1448
|
+
{ a: { 0: 'x', 1: 'y' } },
|
|
1449
|
+
'and converts to an overflow object without throwOnLimitExceeded'
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
sst['throws'](
|
|
1453
|
+
function () { qs.parse('a[0]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1454
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1455
|
+
'mixed index and bracket notation merged past the limit throws'
|
|
1456
|
+
);
|
|
1457
|
+
sst.deepEqual(
|
|
1458
|
+
qs.parse('a[0]=x&a[]=y', { arrayLimit: 1 }),
|
|
1459
|
+
{ a: { 0: 'x', 1: 'y' } },
|
|
1460
|
+
'mixed index and bracket notation converts like duplicate-bracket combine'
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1463
|
+
sst.end();
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
st.test('does not throw when cumulative comma combine stays within arrayLimit', function (sst) {
|
|
1467
|
+
var result = qs.parse('a=1,2,3&a=4', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1468
|
+
sst.deepEqual(result, { a: ['1', '2', '3', '4'] }, 'combined array within limit is preserved');
|
|
1469
|
+
sst.end();
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
st.test('silently combines to an overflow object when throwOnLimitExceeded is not set', function (sst) {
|
|
1473
|
+
var result = qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5 });
|
|
1474
|
+
sst.deepEqual(result, { a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6' } }, 'converts to object without throwing');
|
|
1475
|
+
sst.end();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
st.test('does not throw for comma groups nested under bracket notation, counting each group as one element', function (sst) {
|
|
1479
|
+
var result = qs.parse('a[]=1,2,3&a[]=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1480
|
+
sst.deepEqual(result, { a: [['1', '2', '3'], ['4', '5', '6']] }, 'nested comma groups count as one element each');
|
|
1481
|
+
sst.end();
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
st.test('throws before splitting when a single comma value exceeds arrayLimit', function (sst) {
|
|
1485
|
+
sst['throws'](
|
|
1486
|
+
function () {
|
|
1487
|
+
qs.parse('a=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1488
|
+
},
|
|
1489
|
+
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
|
1490
|
+
'a flat comma value over the limit throws'
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
sst['throws'](
|
|
1494
|
+
function () {
|
|
1495
|
+
qs.parse('a=1,2', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
|
|
1496
|
+
},
|
|
1497
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1498
|
+
'singular message at arrayLimit 1'
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
sst['throws'](
|
|
1502
|
+
function () {
|
|
1503
|
+
qs.parse('a[b]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1504
|
+
},
|
|
1505
|
+
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
|
1506
|
+
'a non-bracket nested key comma value over the limit throws'
|
|
1507
|
+
);
|
|
1508
|
+
sst.end();
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
st.test('does not throw for a single comma value within arrayLimit', function (sst) {
|
|
1512
|
+
sst.deepEqual(
|
|
1513
|
+
qs.parse('a=1,2,3', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
|
1514
|
+
{ a: ['1', '2', '3'] },
|
|
1515
|
+
'within the limit'
|
|
1516
|
+
);
|
|
1517
|
+
sst.deepEqual(
|
|
1518
|
+
qs.parse('a=1,2,3,4,5', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
|
1519
|
+
{ a: ['1', '2', '3', '4', '5'] },
|
|
1520
|
+
'exactly at the limit'
|
|
1521
|
+
);
|
|
1522
|
+
sst.end();
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
st.test('does not throw for a bracketed comma group within arrayLimit', function (sst) {
|
|
1526
|
+
var result = qs.parse('a[]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1527
|
+
sst.deepEqual(result, { a: [['1', '2', '3', '4', '5', '6']] }, 'a bracketed comma group is a single element');
|
|
1528
|
+
sst.end();
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
st.test('throws for a bracketed comma group when arrayLimit is 0', function (sst) {
|
|
1532
|
+
sst['throws'](
|
|
1533
|
+
function () {
|
|
1534
|
+
qs.parse('a[]=1,2,3', { comma: true, arrayLimit: 0, throwOnLimitExceeded: true });
|
|
1535
|
+
},
|
|
1536
|
+
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
|
1537
|
+
'a single bracketed element still exceeds arrayLimit 0'
|
|
1538
|
+
);
|
|
1539
|
+
sst.end();
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1263
1542
|
st.end();
|
|
1264
1543
|
});
|
|
1265
1544
|
|
package/test/stringify.js
CHANGED
|
@@ -651,6 +651,49 @@ test('stringify()', function (t) {
|
|
|
651
651
|
st.end();
|
|
652
652
|
});
|
|
653
653
|
|
|
654
|
+
t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
|
|
655
|
+
st.doesNotThrow(
|
|
656
|
+
function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
|
657
|
+
'does not pass a raw null array entry to the encoder'
|
|
658
|
+
);
|
|
659
|
+
st.doesNotThrow(
|
|
660
|
+
function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
|
661
|
+
'does not pass a raw undefined array entry to the encoder'
|
|
662
|
+
);
|
|
663
|
+
st.doesNotThrow(
|
|
664
|
+
function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
|
665
|
+
'does not crash on a single-null array'
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
st.equal(
|
|
669
|
+
qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
670
|
+
'a=,b',
|
|
671
|
+
'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
|
|
672
|
+
);
|
|
673
|
+
st.equal(
|
|
674
|
+
qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
675
|
+
'a=,b',
|
|
676
|
+
'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
|
|
677
|
+
);
|
|
678
|
+
st.equal(
|
|
679
|
+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
680
|
+
'a=',
|
|
681
|
+
'single-null array stringifies as empty value'
|
|
682
|
+
);
|
|
683
|
+
st.equal(
|
|
684
|
+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
|
|
685
|
+
'a',
|
|
686
|
+
'strictNullHandling drops the equals sign for a single-null array'
|
|
687
|
+
);
|
|
688
|
+
st.equal(
|
|
689
|
+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
|
|
690
|
+
'',
|
|
691
|
+
'skipNulls drops a single-null array entirely'
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
st.end();
|
|
695
|
+
});
|
|
696
|
+
|
|
654
697
|
t.test('stringifies a null object', { skip: !hasProto }, function (st) {
|
|
655
698
|
st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
|
|
656
699
|
st.end();
|
|
@@ -825,6 +868,35 @@ test('stringify()', function (t) {
|
|
|
825
868
|
st.end();
|
|
826
869
|
});
|
|
827
870
|
|
|
871
|
+
t.test('skips null/undefined entries in filter=array', function (st) {
|
|
872
|
+
st.doesNotThrow(
|
|
873
|
+
function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
|
|
874
|
+
'does not pass a raw undefined filter entry to the encoder'
|
|
875
|
+
);
|
|
876
|
+
st.doesNotThrow(
|
|
877
|
+
function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
|
|
878
|
+
'does not pass a raw null filter entry to the encoder'
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
st.equal(
|
|
882
|
+
qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
|
|
883
|
+
'a=b&c=d',
|
|
884
|
+
'undefined filter entry is skipped, remaining keys are kept'
|
|
885
|
+
);
|
|
886
|
+
st.equal(
|
|
887
|
+
qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
|
|
888
|
+
'a=b&c=d',
|
|
889
|
+
'null filter entry is skipped, remaining keys are kept'
|
|
890
|
+
);
|
|
891
|
+
st.equal(
|
|
892
|
+
qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
|
|
893
|
+
'',
|
|
894
|
+
'filter array containing only null yields empty string'
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
st.end();
|
|
898
|
+
});
|
|
899
|
+
|
|
828
900
|
t.test('supports custom representations when filter=function', function (st) {
|
|
829
901
|
var calls = 0;
|
|
830
902
|
var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
|
|
@@ -1111,6 +1183,28 @@ test('stringify()', function (t) {
|
|
|
1111
1183
|
st.end();
|
|
1112
1184
|
});
|
|
1113
1185
|
|
|
1186
|
+
t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
|
|
1187
|
+
st.equal(
|
|
1188
|
+
qs.stringify(
|
|
1189
|
+
{ 'a b': null, 'c d': 'e f' },
|
|
1190
|
+
{ strictNullHandling: false, format: 'RFC1738' }
|
|
1191
|
+
),
|
|
1192
|
+
'a+b=&c+d=e+f',
|
|
1193
|
+
'without: as expected'
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
st.equal(
|
|
1197
|
+
qs.stringify(
|
|
1198
|
+
{ 'a b': null, 'c d': 'e f' },
|
|
1199
|
+
{ strictNullHandling: true, format: 'RFC1738' }
|
|
1200
|
+
),
|
|
1201
|
+
'a+b&c+d=e+f',
|
|
1202
|
+
'with: as expected'
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
st.end();
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1114
1208
|
t.test('throws if an invalid charset is specified', function (st) {
|
|
1115
1209
|
st['throws'](function () {
|
|
1116
1210
|
qs.stringify({ a: 'b' }, { charset: 'foobar' });
|
|
@@ -1146,6 +1240,12 @@ test('stringify()', function (t) {
|
|
|
1146
1240
|
'adds the right sentinel when instructed to and the charset is iso-8859-1'
|
|
1147
1241
|
);
|
|
1148
1242
|
|
|
1243
|
+
st.equal(
|
|
1244
|
+
qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
|
|
1245
|
+
'utf8=%E2%9C%93;a=1;b=2',
|
|
1246
|
+
'uses the configured delimiter after the sentinel'
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1149
1249
|
st.end();
|
|
1150
1250
|
});
|
|
1151
1251
|
|
|
@@ -1316,4 +1416,33 @@ test('stringifies empty keys', function (t) {
|
|
|
1316
1416
|
|
|
1317
1417
|
st.end();
|
|
1318
1418
|
});
|
|
1419
|
+
|
|
1420
|
+
t.test('round-trips keys containing percent-encoded bracket text', function (st) {
|
|
1421
|
+
var cases = [
|
|
1422
|
+
{ 'a%5Bb': 'c' },
|
|
1423
|
+
{ 'a%5Db': 'c' },
|
|
1424
|
+
{ 'a%255Bb': 'c' },
|
|
1425
|
+
{ 'a%255Db': 'c' },
|
|
1426
|
+
{ a: { 'b%5Bc': 'd' } },
|
|
1427
|
+
{ a: { 'b%255Bc': 'd' } },
|
|
1428
|
+
{ 'a%5B%255Bb': 'c' }
|
|
1429
|
+
];
|
|
1430
|
+
for (var i = 0; i < cases.length; i++) {
|
|
1431
|
+
st.deepEqual(
|
|
1432
|
+
qs.parse(qs.stringify(cases[i])),
|
|
1433
|
+
cases[i],
|
|
1434
|
+
'round-trips ' + JSON.stringify(cases[i])
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
st.end();
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
|
|
1442
|
+
st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
|
|
1443
|
+
st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
|
|
1444
|
+
st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
|
|
1445
|
+
|
|
1446
|
+
st.end();
|
|
1447
|
+
});
|
|
1319
1448
|
});
|