alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

+192 -3
View File
@@ -1475,6 +1475,73 @@ test('parse()', function (t) {
sst.end();
});
st.test('spreads a comma group appended to an already-overflowed object', function (sst) {
var result = qs.parse('a=1,2,3,4,5,6&a=7,8', { comma: true, arrayLimit: 5 });
sst.deepEqual(
result,
{ a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6', 6: '7', 7: '8' } },
'appended comma values are flattened, not nested'
);
sst.end();
});
st.test('keeps a `[]=` comma group as one element when appended to an already-overflowed key', function (sst) {
var result = qs.parse('a[]=1&a[]=2&a[]=3&a[]=4,5', { comma: true, arrayLimit: 2 });
sst.deepEqual(
result,
{ a: { 0: '1', 1: '2', 2: '3', 3: ['4', '5'] } },
'nested comma group stays a single element, not double-nested'
);
sst.end();
});
st.test('spreads every later comma group appended to an already-overflowed object', function (sst) {
var result = qs.parse('a=1,2,3,4,5,6&a=7,8&a=9,10', { comma: true, arrayLimit: 5 });
sst.deepEqual(
result,
{ a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6', 6: '7', 7: '8', 8: '9', 9: '10' } },
'each appended group is flattened in order'
);
sst.end();
});
st.test('spreads a comma group appended to an object overflowed under the default arrayLimit', function (sst) {
var values = [];
for (var i = 0; i <= 20; i += 1) {
values[values.length] = String(i);
}
var expected = {};
for (var j = 0; j < values.length; j += 1) {
expected[j] = values[j];
}
expected[21] = 'x';
expected[22] = 'y';
var result = qs.parse('a=' + values.join(',') + '&a=x,y', { comma: true });
sst.deepEqual(result, { a: expected }, 'appended values land at indices 21 and 22');
sst.end();
});
st.test('spreads a comma group appended to an already-overflowed object with plainObjects', function (sst) {
var result = qs.parse('a=1,2,3,4,5,6&a=7,8', { comma: true, arrayLimit: 5, plainObjects: true });
sst.deepEqual(
result,
{ __proto__: null, a: { __proto__: null, 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6', 6: '7', 7: '8' } },
'appended comma values are flattened into the null object'
);
sst.end();
});
st.test('still throws with throwOnLimitExceeded when a comma group is appended to an already-overflowed object', function (sst) {
sst['throws'](
function () {
qs.parse('a=1,2,3,4,5,6&a=7,8', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
},
/Array limit exceeded/,
'throws before spreading the appended group'
);
sst.end();
});
st.test('does not throw for comma groups nested under bracket notation, counting each group as one element', function (sst) {
var result = qs.parse('a[]=1,2,3&a[]=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: [['1', '2', '3'], ['4', '5', '6']] }, 'nested comma groups count as one element each');
@@ -1522,9 +1589,131 @@ test('parse()', function (t) {
sst.end();
});
st.test('does not throw for a bracketed comma group within arrayLimit', function (sst) {
var result = qs.parse('a[]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: [['1', '2', '3', '4', '5', '6']] }, 'a bracketed comma group is a single element');
st.test('throws before splitting when a bracketed comma group exceeds arrayLimit', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
'the inner group is itself an array and is subject to arrayLimit'
);
sst.end();
});
st.test('throws before splitting when a comma group under `[]=` exceeds arrayLimit', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'a bracket-push comma group over the limit throws like the flat form'
);
sst['throws'](
function () {
qs.parse('a[]=1,2', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'singular message at arrayLimit 1'
);
sst['throws'](
function () {
qs.parse('a[b][]=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'a nested bracket-push key throws'
);
sst['throws'](
function () {
qs.parse('a[][]=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'a doubly bracket-pushed key throws'
);
sst['throws'](
function () {
qs.parse('a[]=5&a[]=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'an oversized group appended after a scalar throws'
);
sst['throws'](
function () {
qs.parse('a[]=1,2,3,4&a[]=5', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'an oversized group throws even when a later part would fit'
);
sst['throws'](
function () {
qs.parse('a[]=1,2,3,4&a[]=5', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true, duplicates: 'first' });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'throws with duplicates: first'
);
sst['throws'](
function () {
qs.parse('a[]=5&a[]=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true, duplicates: 'last' });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'throws with duplicates: last'
);
sst['throws'](
function () {
qs.parse({ a: '1,2,3,4' }, { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'an oversized comma value in object input throws'
);
var big = 'a[]=' + new Array(1e5 + 1).join('1,') + '1';
sst['throws'](
function () {
qs.parse(big, { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'a very long bracket-push comma group throws'
);
sst.end();
});
st.test('does not throw for a comma group under `[]=` within arrayLimit', function (sst) {
sst.deepEqual(
qs.parse('a[]=1,2,3', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true }),
{ a: [['1', '2', '3']] },
'an inner group exactly at the limit stays a single nested element'
);
sst.deepEqual(
qs.parse('a[]=1,2,3&a[]=4,5,6&a[]=7', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true }),
{ a: [['1', '2', '3'], ['4', '5', '6'], '7'] },
'each group counts as one element of the outer array'
);
sst.deepEqual(
qs.parse('a[b][]=1,2,3', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true }),
{ a: { b: [['1', '2', '3']] } },
'nested bracket-push key within the limit'
);
sst.deepEqual(
qs.parse({ a: '1,2,3' }, { comma: true, arrayLimit: 3, throwOnLimitExceeded: true }),
{ a: ['1', '2', '3'] },
'object input within the limit'
);
sst.end();
});
st.test('keeps an oversized comma group under `[]=` as a nested array without throwOnLimitExceeded', function (sst) {
sst.deepEqual(
qs.parse('a[]=1,2,3,4', { comma: true, arrayLimit: 3 }),
{ a: [['1', '2', '3', '4']] },
'the inner group is not converted to an overflow object'
);
sst.end();
});
+317
View File
@@ -136,6 +136,144 @@ test('stringify()', function (t) {
st.end();
});
t.test('encodes dot in key of object with a primitive value when encodeDotInKeys and allowDots is provided', function (st) {
st.equal(
qs.stringify(
{ 'name.obj': 'John' },
{ allowDots: true, encodeDotInKeys: true }
),
'name%252Eobj=John',
'with allowDots true and encodeDotInKeys true'
);
st.equal(
qs.stringify(
{ 'name.obj': 'John' },
{ allowDots: false, encodeDotInKeys: true }
),
'name%252Eobj=John',
'with allowDots false and encodeDotInKeys true'
);
st.equal(
qs.stringify(
{ 'name.obj': 'John' },
{ encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
),
'name%2Eobj=John',
'with encodeValuesOnly true'
);
// With `encodeValuesOnly` the key encoder is skipped, so the dot is encoded once
// (`%2E`) rather than twice. That single `%2E` decodes back to a structural dot, so
// unlike the default output below this does not round-trip to the original dotted key
// (a pre-existing `encodeValuesOnly` + `encodeDotInKeys` limitation, not new here).
st.deepEqual(
qs.parse(
qs.stringify(
{ 'name.obj': 'John' },
{ encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
),
{ allowDots: true, decodeDotInKeys: true }
),
{ name: { obj: 'John' } },
'with encodeValuesOnly the single-encoded dot decodes structurally (does not round-trip)'
);
st.deepEqual(
qs.parse(
qs.stringify({ 'name.obj': 'John' }, { allowDots: true, encodeDotInKeys: true }),
{ allowDots: true, decodeDotInKeys: true }
),
{ 'name.obj': 'John' },
'round-trips a dotted key with a primitive value'
);
st.equal(
qs.stringify({ 'a.b': null }, { allowDots: true, encodeDotInKeys: true, strictNullHandling: true }),
'a%252Eb',
'encodes a dotted key with a null value'
);
st.equal(
qs.stringify({ 'a.b': '1', 'c.d': '2' }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb=1&c%252Ed=2',
'encodes dots in every top-level key, not just the first'
);
st.equal(
qs.stringify({ 'a.b.c': 'x' }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec=x',
'encodes every dot in a key'
);
var primitiveKey = qs.stringify({ 'a.b': 'c' }, { allowDots: true, encodeDotInKeys: true }).split('=')[0];
st.equal(
qs.stringify({ 'a.b': { x: 'c' } }, { allowDots: true, encodeDotInKeys: true }).indexOf(primitiveKey + '.'),
0,
'a top-level dotted key encodes identically for primitive and object values'
);
st.end();
});
// Characterization of the CURRENT (buggy) behavior: with `allowDots` + `encodeDotInKeys`,
// `stringify` percent-encodes the structural `.` separators that `allowDots` inserts between
// nested keys, not just the literal dots inside a key, which breaks the stringify -> parse
// round-trip. These assertions pin that behavior so it cannot change silently: the fix in
// https://github.com/ljharb/qs/pull/564 changes every output below, so landing that fix MUST
// update this test (that is the point — it forces the behavior change to be explicit).
t.test('[characterization] encodeDotInKeys currently over-encodes structural separator dots (pre-#564)', function (st) {
st.equal(
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb.c=d',
'over-encodes the a->b separator (#564: should become a.b.c=d)'
);
st.equal(
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec.d=e',
'clobbers every separator but the last (#564: should become a.b.c.d=e)'
);
st.equal(
qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec.d=e',
'top-level dotted key: deeper separators also get encoded (#564: should become a%252Eb.c.d=e)'
);
st.equal(
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec.d=e',
'nested dotted key + object value: the separator around it is encoded (#564: should become a.b%252Ec.d=e)'
);
st.equal(
qs.stringify({ a: { b: { 'c.d': { e: 'f' } } } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec%252Ed.e=f',
'deeply nested dotted key: all separators encoded (#564: should become a.b.c%252Ed.e=f)'
);
st.equal(
qs.stringify({ a: { b: [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%5B0%5D=1&a%252Eb%5B1%5D=2',
'array value under nested keys: separator encoded (#564: should become a.b%5B0%5D=1&a.b%5B1%5D=2)'
);
st.equal(
qs.stringify({ a: { 'b.c': [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
'a%252Eb%252Ec%5B0%5D=1&a%252Eb%252Ec%5B1%5D=2',
'array value under nested dotted key (#564: should become a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2)'
);
st.equal(
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
'a%5Bb%252Ec%5D%5Bd%5D=e',
'allowDots:false object value encodes the nested-key dot, unlike the primitive-value case (#564: should become a%5Bb.c%5D%5Bd%5D=e)'
);
st.deepEqual(
qs.parse(
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
{ allowDots: true, decodeDotInKeys: true }
),
{ 'a.b': { c: 'd' } },
'the over-encoding breaks the round-trip: {a:{b:{c}}} comes back as {"a.b":{c}} (#564 restores it)'
);
st.end();
});
t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
st.equal(
qs.stringify(
@@ -328,6 +466,39 @@ test('stringify()', function (t) {
st.end();
});
t.test('allowEmptyArrays only short-circuits arrays with no own keys', function (st) {
st.equal(
qs.stringify({ a: [] }, { allowEmptyArrays: true }),
'a[]',
'a genuinely empty array is still emitted as `[]`'
);
var withKey = [];
withKey.extra = 'x';
st.equal(
qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
'a%5Bextra%5D=x',
'an own key on an empty array is serialized rather than dropped'
);
st.equal(
qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
qs.stringify({ a: withKey }),
'the allowEmptyArrays fast path matches the default path when the array has own keys'
);
var container = {};
var cyclic = [];
cyclic.back = container;
container.a = cyclic;
st['throws'](
function () { qs.stringify(container, { allowEmptyArrays: true }); },
RangeError,
'a cycle through an empty array with an own key is still detected'
);
st.end();
});
t.test('stringifies an array value with one item vs multiple items', function (st) {
st.test('non-array item', function (s2t) {
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
@@ -761,6 +932,33 @@ test('stringify()', function (t) {
st.end();
});
t.test('does not throw on a non-callable `constructor.isBuffer` key', function (st) {
var input = { a: { constructor: { isBuffer: 'x' } } };
st.doesNotThrow(function () { qs.stringify(input); });
st.equal(qs.stringify(input), 'a%5Bconstructor%5D%5BisBuffer%5D=x');
st.equal(qs.stringify({ a: { constructor: { isBuffer: { b: 'c' } } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5Bb%5D=c');
st.equal(qs.stringify({ a: { constructor: { isBuffer: ['b'] } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5B0%5D=b');
st.equal(qs.stringify({ a: { constructor: { isBuffer: true } } }), 'a%5Bconstructor%5D%5BisBuffer%5D=true');
st.end();
});
t.test('round-trips a parsed `constructor[isBuffer]` key', function (st) {
var str = 'x%5Bconstructor%5D%5BisBuffer%5D=y';
var plain = qs.parse(str, { plainObjects: true });
st.deepEqual(plain, { __proto__: null, x: { __proto__: null, constructor: { __proto__: null, isBuffer: 'y' } } }, 'plainObjects keeps the key');
st.doesNotThrow(function () { qs.stringify(plain); });
st.equal(qs.stringify(plain), str, 'plainObjects round-trips');
var proto = qs.parse(str, { allowPrototypes: true });
st.deepEqual(proto, { x: { constructor: { isBuffer: 'y' } } }, 'allowPrototypes keeps the key');
st.doesNotThrow(function () { qs.stringify(proto); });
st.equal(qs.stringify(proto), str, 'allowPrototypes round-trips');
st.equal(qs.stringify(qs.parse(str)), '', 'default options drop the key');
st.end();
});
t.test('stringifies an object using an alternative delimiter', function (st) {
st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
st.end();
@@ -919,6 +1117,66 @@ test('stringify()', function (t) {
st.end();
});
t.test('serializes Date values unless filter transforms them', function (st) {
var obj = {
a: new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260)),
b: [new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260))]
};
var iso = '2023-03-07T09%3A02%3A41.260Z';
st.equal(
qs.stringify(obj, { filter: function (prefix, value) { return value; } }),
'a=' + iso + '&b%5B0%5D=' + iso,
'Dates returned unchanged by a filter are serialized'
);
st.equal(
qs.stringify({ a: new Date(0) }, { filter: function (prefix, value) { return value instanceof Date ? 'kept' : value; } }),
'a=kept',
'a filter that transforms a Date wins over default serialization'
);
st.end();
});
t.test('applies default Date handling to whatever a filter function returns', function (st) {
var date = new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260));
var iso = '2023-03-07T09%3A02%3A41.260Z';
var identity = function (prefix, value) { return value; };
st.equal(
qs.stringify({ a: { b: date } }, { filter: identity }),
'a%5Bb%5D=' + iso,
'a nested Date is serialized through a filter'
);
st.equal(
qs.stringify(
{ a: date },
{ filter: identity, serializeDate: function (d) { return String(d.getTime()); } }
),
'a=1678179761260',
'`serializeDate` is honored after a filter'
);
st.equal(
qs.stringify({ a: [date, 'x'] }, { filter: identity, arrayFormat: 'comma' }),
'a=' + iso + '%2Cx',
'Dates inside a comma array are serialized through a filter, not stringified as locale text'
);
st.equal(
qs.stringify({ a: 'x' }, { filter: function (prefix, value) { return prefix === 'a' ? date : value; } }),
'a=' + iso,
'a Date produced by the filter is serialized'
);
st.equal(
qs.stringify({ a: date, b: 1 }, { filter: ['a'] }),
'a=' + iso,
'an array filter still serializes Dates'
);
st['throws'](
function () { qs.stringify({ a: new Date(NaN) }, { filter: identity }); },
RangeError,
'an invalid Date through a filter throws like the no-filter path'
);
st.end();
});
t.test('can disable uri encoding', function (st) {
st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
@@ -1445,4 +1703,63 @@ test('stringifies empty keys', function (t) {
st.end();
});
t.test('limits nesting depth with the `depth` option', function (st) {
st.equal(
qs.stringify({ a: { b: { c: 'd' } } }),
'a%5Bb%5D%5Bc%5D=d',
'the default depth is unlimited and does not alter output'
);
st.equal(
qs.stringify({ a: { b: { c: 'd' } } }, { depth: 2 }),
'a%5Bb%5D%5Bc%5D=d',
'does not throw when nesting is within the depth limit'
);
st.equal(
qs.stringify({ a: { b: 'c' } }, { depth: Infinity }),
'a%5Bb%5D=c',
'an explicit Infinity depth is unlimited'
);
st.equal(
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: null }),
qs.stringify({ a: { b: { c: { d: 'e' } } } }),
'a non-number depth is ignored and treated as unlimited'
);
st.equal(
qs.stringify({ a: 'b' }, { depth: 0 }),
'a=b',
'a flat object serializes with a depth of 0'
);
var deep = {};
var cursor = deep;
for (var i = 0; i < 250; i++) {
cursor.a = {};
cursor = cursor.a;
}
cursor.b = 'c';
st.equal(typeof qs.stringify(deep), 'string', 'the default depth serializes deeply nested objects without throwing');
st['throws'](
function () { qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 }); },
RangeError,
'throws a RangeError when nesting exceeds the depth limit'
);
st['throws'](
function () { qs.stringify({ a: { b: 'c' } }, { depth: 0 }); },
RangeError,
'a depth of 0 throws for any nested object'
);
var caught;
try {
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 });
} catch (e) {
caught = e;
}
st.ok(caught instanceof RangeError, 'the thrown value is a RangeError');
st.equal(caught.message, 'Input depth exceeded depth option of 2', 'the RangeError names the exceeded depth');
st.end();
});
});
+12
View File
@@ -604,6 +604,18 @@ test('isBuffer()', function (t) {
var fakeBuffer = { constructor: Buffer };
t.equal(utils.isBuffer(fakeBuffer), false, 'fake buffer is not a buffer');
forEach(['x', 1, true, {}, [], { isBuffer: true }], function (notCallable) {
var obj = { constructor: { isBuffer: notCallable } };
t.doesNotThrow(function () { utils.isBuffer(obj); }, 'non-callable `constructor.isBuffer` (' + inspect(notCallable) + ') does not throw');
t.equal(utils.isBuffer(obj), false, 'non-callable `constructor.isBuffer` (' + inspect(notCallable) + ') is not a buffer');
});
var nullObject = { __proto__: null, constructor: { __proto__: null, isBuffer: 'y' } };
t.equal(utils.isBuffer(nullObject), false, 'null object with non-callable `constructor.isBuffer` is not a buffer');
var duckBuffer = { constructor: { isBuffer: function () { return true; } } };
t.equal(utils.isBuffer(duckBuffer), true, 'callable `constructor.isBuffer` is still honored');
var saferBuffer = SaferBuffer.from('abc');
t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer instance is a buffer');