This commit is contained in:
2026-07-26 21:23:30 +00:00
parent 6fa0e1b9a6
commit 7c262aeb8b
60 changed files with 2359 additions and 582 deletions
+27
View File
@@ -1,3 +1,30 @@
## **6.15.3**
- [Fix] `parse`: enforce `throwOnLimitExceeded` for cumulative array growth via `combine`/`merge`
- [Fix] `utils`: respect encoding of surrogate pairs across chunks (#559)
- [Robustness] `parse`: throw the `arrayLimit` error before splitting oversized comma values
- [Robustness] `utils.merge` / `utils.assign`: avoid invoking `__proto__` setter when copying own properties
- [Robustness] `utils`: enforce `arrayLimit` consistently across `merge`'s array paths
- [Perf] `utils`: make `compact` O(n) via a side-channel visited-set instead of `Array.indexOf`
- [Deps] update `side-channel`
- [Dev Deps] update `eslint`, `mock-property`, `tape`
- [Tests] `parse`: characterize current lenient handling of unbalanced bracket keys (#558)
## **6.15.2**
- [Fix] `stringify`: skip null/undefined entries in `arrayFormat: 'comma'` + `encodeValuesOnly` instead of crashing in `encoder`
- [Fix] `stringify`: use configured `delimiter` after `charsetSentinel` (#555)
- [Fix] `stringify`: apply `formatter` to encoded key under `strictNullHandling` (#554)
- [Fix] `stringify`: skip null/undefined filter-array entries instead of crashing in `encoder` (#551)
- [Fix] `parse`: handle nested bracket groups and add regression tests (#530)
- [readme] fix grammar (#550)
- [Dev Deps] update `@ljharb/eslint-config`
- [Tests] add regression tests for keys containing percent-encoded bracket text
## **6.15.1**
- [Fix] `parse`: `parameterLimit: Infinity` with `throwOnLimitExceeded: true` silently drops all parameters
- [Deps] update `@ljharb/eslint-config`
- [Dev Deps] update `@ljharb/eslint-config`, `iconv-lite`
- [Tests] increase coverage
## **6.15.0**
- [New] `parse`: add `strictMerge` option to wrap object/primitive conflicts in an array (#425, #122)
- [Fix] `duplicates` option should not apply to bracket notation keys (#514)
Generated Vendored
+1 -1
View File
@@ -183,7 +183,7 @@ var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decod
assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }});
```
Option `allowEmptyArrays` can be used to allowing empty array values in object
Option `allowEmptyArrays` can be used to allow empty array values in an object
```javascript
var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true });
assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' });
Generated Vendored
+33 -33
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -11,6 +11,7 @@ export default [
rules: {
complexity: 'off',
'consistent-return': 'warn',
eqeqeq: ['error', 'allow-null'],
'func-name-matching': 'off',
'id-length': [
'error',
+72 -31
View File
@@ -36,8 +36,19 @@ var interpretNumericEntities = function (str) {
});
};
var parseArrayValue = function (val, options, currentArrayLength) {
var parseArrayValue = function (val, options, currentArrayLength, isFlatArrayValue) {
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
if (isFlatArrayValue && options.throwOnLimitExceeded) {
var commaCount = 0;
var commaIndex = val.indexOf(',');
while (commaIndex > -1) {
commaCount += 1;
if (commaCount >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
commaIndex = val.indexOf(',', commaIndex + 1);
}
}
return val.split(',');
}
@@ -67,10 +78,10 @@ var parseValues = function parseQueryStringValues(str, options) {
var limit = options.parameterLimit === Infinity ? void undefined : options.parameterLimit;
var parts = cleanStr.split(
options.delimiter,
options.throwOnLimitExceeded ? limit + 1 : limit
options.throwOnLimitExceeded && typeof limit !== 'undefined' ? limit + 1 : limit
);
if (options.throwOnLimitExceeded && parts.length > limit) {
if (options.throwOnLimitExceeded && typeof limit !== 'undefined' && parts.length > limit) {
throw new RangeError('Parameter limit exceeded. Only ' + limit + ' parameter' + (limit === 1 ? '' : 's') + ' allowed.');
}
@@ -114,7 +125,8 @@ var parseValues = function parseQueryStringValues(str, options) {
parseArrayValue(
part.slice(pos + 1),
options,
isArray(obj[key]) ? obj[key].length : 0
isArray(obj[key]) ? obj[key].length : 0,
part.indexOf('[]=') === -1
),
function (encodedVal) {
return options.decoder(encodedVal, defaults.decoder, charset, 'value');
@@ -132,10 +144,7 @@ var parseValues = function parseQueryStringValues(str, options) {
}
if (options.comma && isArray(val) && val.length > options.arrayLimit) {
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
val = utils.combine([], val, options.arrayLimit, options.plainObjects);
val = utils.combine([], val, options.arrayLimit, options.plainObjects, options.throwOnLimitExceeded);
}
if (key !== null) {
@@ -145,7 +154,8 @@ var parseValues = function parseQueryStringValues(str, options) {
obj[key],
val,
options.arrayLimit,
options.plainObjects
options.plainObjects,
options.throwOnLimitExceeded
);
} else if (!existing || options.duplicates === 'last') {
obj[key] = val;
@@ -180,7 +190,8 @@ var parseObject = function (chain, val, options, valuesParsed) {
[],
leaf,
options.arrayLimit,
options.plainObjects
options.plainObjects,
options.throwOnLimitExceeded
);
}
} else {
@@ -214,9 +225,12 @@ var parseObject = function (chain, val, options, valuesParsed) {
return leaf;
};
var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey;
// Split a key like "a[b][c[]]" into ['a', '[b]', '[c[]]'] while preserving
// qs parse semantics for depth/prototype guards.
var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;
// depth <= 0 keeps the whole key as one segment
if (options.depth <= 0) {
if (!options.plainObjects && has.call(Object.prototype, key)) {
if (!options.allowPrototypes) {
@@ -227,14 +241,11 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
return [key];
}
var brackets = /(\[[^[\]]*])/;
var child = /(\[[^[\]]*])/g;
var segment = brackets.exec(key);
var parent = segment ? key.slice(0, segment.index) : key;
var keys = [];
var segments = [];
// parent before the first '[' (may be empty if key starts with '[')
var first = key.indexOf('[');
var parent = first >= 0 ? key.slice(0, first) : key;
if (parent) {
if (!options.plainObjects && has.call(Object.prototype, parent)) {
if (!options.allowPrototypes) {
@@ -242,32 +253,62 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
}
}
keys[keys.length] = parent;
segments[segments.length] = parent;
}
var i = 0;
while ((segment = child.exec(key)) !== null && i < options.depth) {
i += 1;
var n = key.length;
var open = first;
var collected = 0;
var segmentContent = segment[1].slice(1, -1);
if (!options.plainObjects && has.call(Object.prototype, segmentContent)) {
if (!options.allowPrototypes) {
return;
while (open >= 0 && collected < options.depth) {
var level = 1;
var i = open + 1;
var close = -1;
// balance nested '[' and ']' inside this bracket group using a nesting level counter
while (i < n && close < 0) {
var cu = key.charCodeAt(i);
if (cu === 0x5B) { // '['
level += 1;
} else if (cu === 0x5D) { // ']'
level -= 1;
if (level === 0) {
close = i; // found matching close; loop will exit by condition
}
}
i += 1;
}
keys[keys.length] = segment[1];
if (close < 0) {
// Unterminated group: wrap the raw remainder in one bracket pair so it stays
// a single literal segment (e.g. "[[]b" -> "[[]b]"); we do not infer missing ']'.
segments[segments.length] = '[' + key.slice(open) + ']';
return segments;
}
var seg = key.slice(open, close + 1);
// prototype guard for the content of this group
var content = seg.slice(1, -1);
if (!options.plainObjects && has.call(Object.prototype, content) && !options.allowPrototypes) {
return;
}
segments[segments.length] = seg;
collected += 1;
// find the next '[' after this balanced group
open = key.indexOf('[', close + 1);
}
if (segment) {
if (open >= 0) {
if (options.strictDepth === true) {
throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
}
keys[keys.length] = '[' + key.slice(segment.index) + ']';
segments[segments.length] = '[' + key.slice(open) + ']';
}
return keys;
return segments;
};
var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
+11 -4
View File
@@ -118,7 +118,7 @@ var stringify = function stringify(
if (obj === null) {
if (strictNullHandling) {
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix;
return formatter(encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix);
}
obj = '';
@@ -142,7 +142,9 @@ var stringify = function stringify(
if (generateArrayPrefix === 'comma' && isArray(obj)) {
// we need to join elements in
if (encodeValuesOnly && encoder) {
obj = utils.maybeMap(obj, encoder);
obj = utils.maybeMap(obj, function (v) {
return v == null ? v : encoder(v);
});
}
objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
} else if (isArray(filter)) {
@@ -312,6 +314,11 @@ module.exports = function (object, opts) {
var sideChannel = getSideChannel();
for (var i = 0; i < objKeys.length; ++i) {
var key = objKeys[i];
if (typeof key === 'undefined' || key === null) {
continue;
}
var value = obj[key];
if (options.skipNulls && value === null) {
@@ -345,10 +352,10 @@ module.exports = function (object, opts) {
if (options.charsetSentinel) {
if (options.charset === 'iso-8859-1') {
// encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
prefix += 'utf8=%26%2310003%3B&';
prefix += 'utf8=%26%2310003%3B' + options.delimiter;
} else {
// encodeURIComponent('✓')
prefix += 'utf8=%E2%9C%93&';
prefix += 'utf8=%E2%9C%93' + options.delimiter;
}
}
+47 -8
View File
@@ -2,6 +2,7 @@
var formats = require('./formats');
var getSideChannel = require('side-channel');
var defineProperty = require('es-define-property');
var has = Object.prototype.hasOwnProperty;
var isArray = Array.isArray;
@@ -66,6 +67,19 @@ var arrayToObject = function arrayToObject(source, options) {
return obj;
};
var setProperty = function setProperty(obj, key, value) {
if (key === '__proto__' && defineProperty) {
defineProperty(obj, key, {
configurable: true,
enumerable: true,
value: value,
writable: true
});
} else {
obj[key] = value;
}
};
var merge = function merge(target, source, options) {
/* eslint no-param-reassign: 0 */
if (!source) {
@@ -75,7 +89,10 @@ var merge = function merge(target, source, options) {
if (typeof source !== 'object' && typeof source !== 'function') {
if (isArray(target)) {
var nextIndex = target.length;
if (options && typeof options.arrayLimit === 'number' && nextIndex > options.arrayLimit) {
if (options && typeof options.arrayLimit === 'number' && nextIndex >= options.arrayLimit) {
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return markOverflow(arrayToObject(target.concat(source), options), nextIndex);
}
target[nextIndex] = source;
@@ -115,6 +132,9 @@ var merge = function merge(target, source, options) {
}
var combined = [target].concat(source);
if (options && typeof options.arrayLimit === 'number' && combined.length > options.arrayLimit) {
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return markOverflow(arrayToObject(combined, options), combined.length - 1);
}
return combined;
@@ -138,6 +158,12 @@ var merge = function merge(target, source, options) {
target[i] = item;
}
});
if (options && typeof options.arrayLimit === 'number' && target.length > options.arrayLimit) {
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return markOverflow(arrayToObject(target, options), target.length - 1);
}
return target;
}
@@ -145,9 +171,9 @@ var merge = function merge(target, source, options) {
var value = source[key];
if (has.call(acc, key)) {
acc[key] = merge(acc[key], value, options);
setProperty(acc, key, merge(acc[key], value, options));
} else {
acc[key] = value;
setProperty(acc, key, value);
}
if (isOverflow(source) && !isOverflow(acc)) {
@@ -166,7 +192,7 @@ var merge = function merge(target, source, options) {
var assign = function assignSingleSource(target, source) {
return Object.keys(source).reduce(function (acc, key) {
acc[key] = source[key];
setProperty(acc, key, source[key]);
return acc;
}, target);
};
@@ -212,6 +238,13 @@ var encode = function encode(str, defaultEncoder, charset, kind, format) {
var out = '';
for (var j = 0; j < string.length; j += limit) {
var segment = string.length >= limit ? string.slice(j, j + limit) : string;
if (j + limit < string.length) {
var last = segment.charCodeAt(segment.length - 1);
if (last >= 0xD800 && last <= 0xDBFF) {
segment = segment.slice(0, -1);
j -= 1;
}
}
var arr = [];
for (var i = 0; i < segment.length; ++i) {
@@ -265,7 +298,7 @@ var encode = function encode(str, defaultEncoder, charset, kind, format) {
var compact = function compact(value) {
var queue = [{ obj: { o: value }, prop: 'o' }];
var refs = [];
var refs = getSideChannel();
for (var i = 0; i < queue.length; ++i) {
var item = queue[i];
@@ -275,9 +308,9 @@ var compact = function compact(value) {
for (var j = 0; j < keys.length; ++j) {
var key = keys[j];
var val = obj[key];
if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
if (typeof val === 'object' && val !== null && !refs.has(val)) {
queue[queue.length] = { obj: obj, prop: key };
refs[refs.length] = val;
refs.set(val, true);
}
}
}
@@ -299,9 +332,12 @@ var isBuffer = function isBuffer(obj) {
return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
};
var combine = function combine(a, b, arrayLimit, plainObjects) {
var combine = function combine(a, b, arrayLimit, plainObjects, throwOnLimitExceeded) {
// If 'a' is already an overflow object, add to it
if (isOverflow(a)) {
if (throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + arrayLimit + ' element' + (arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
var newIndex = getMaxIndex(a) + 1;
a[newIndex] = b;
setMaxIndex(a, newIndex);
@@ -310,6 +346,9 @@ var combine = function combine(a, b, arrayLimit, plainObjects) {
var result = [].concat(a, b);
if (result.length > arrayLimit) {
if (throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + arrayLimit + ' element' + (arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return markOverflow(arrayToObject(result, { plainObjects: plainObjects }), result.length - 1);
}
return result;
+8 -7
View File
@@ -2,7 +2,7 @@
"name": "qs",
"description": "A querystring parser that supports nesting and arrays, with a depth limit",
"homepage": "https://github.com/ljharb/qs",
"version": "6.15.0",
"version": "6.15.3",
"repository": {
"type": "git",
"url": "https://github.com/ljharb/qs.git"
@@ -31,18 +31,19 @@
"node": ">=0.6"
},
"dependencies": {
"side-channel": "^1.1.0"
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"devDependencies": {
"@browserify/envify": "^6.0.0",
"@browserify/uglifyify": "^6.0.0",
"@ljharb/eslint-config": "^22.1.3",
"@ljharb/eslint-config": "^22.2.3",
"browserify": "^16.5.2",
"bundle-collapser": "^1.4.0",
"common-shakeify": "~1.0.0",
"eclint": "^2.8.1",
"es-value-fixtures": "^1.7.1",
"eslint": "^9.39.2",
"eslint": "^10.5.0",
"evalmd": "^0.0.19",
"for-each": "^0.3.5",
"glob": "=10.3.7",
@@ -51,12 +52,12 @@
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.2.0",
"has-symbols": "^1.1.0",
"iconv-lite": "^0.5.1",
"iconv-lite": "^0.5.2",
"in-publish": "^2.0.1",
"jackspeak": "=2.1.1",
"jiti": "^0.0.0",
"mkdirp": "^0.5.5",
"mock-property": "^1.1.0",
"mock-property": "^1.1.2",
"module-deps": "^6.2.3",
"npmignore": "^0.3.5",
"nyc": "^10.3.2",
@@ -64,7 +65,7 @@
"qs-iconv": "^1.0.4",
"safe-publish-latest": "^2.0.0",
"safer-buffer": "^2.1.2",
"tape": "^5.9.0",
"tape": "^5.10.2",
"unassertify": "^3.0.1"
},
"scripts": {
+365
View File
@@ -14,6 +14,12 @@ var hasProto = require('has-proto')();
var qs = require('../');
var utils = require('../lib/utils');
var characterizeParse = function characterizeParse(st, input, opts, expected, label) {
var result;
st.doesNotThrow(function () { result = qs.parse(input, opts); }, label + ': does not throw');
st.deepEqual(result, expected, label + ': parses to the current lenient output');
};
test('parse()', function (t) {
t.test('parses a simple string', function (st) {
st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
@@ -210,6 +216,21 @@ test('parse()', function (t) {
t.test('uses original key when depth = 0', function (st) {
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
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' });
st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
st.end();
});
t.test('ignores prototype keys when depth = 0 and allowPrototypes is false', function (st) {
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {});
st.deepEqual(qs.parse('hasOwnProperty=bar', { depth: 0 }), {});
st.deepEqual(qs.parse('toString=foo&a=b', { depth: 0 }), { a: 'b' });
st.end();
});
t.test('allows prototype keys when depth = 0 and allowPrototypes is true', function (st) {
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
st.end();
});
@@ -251,6 +272,94 @@ test('parse()', function (t) {
st.end();
});
t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
// A bracket pair inside a bracket group should be treated literally as part of the key
st.deepEqual(
qs.parse('search[withbracket[]]=foobar'),
{ search: { 'withbracket[]': 'foobar' } },
'treats inner [] literally when inside a bracket group'
);
// Single-level variant
st.deepEqual(
qs.parse('a[b[]]=c'),
{ a: { 'b[]': 'c' } },
'keeps "b[]" as a literal key'
);
// Nested with an array push on the outer level
st.deepEqual(
qs.parse('list[][x[]]=y'),
{ list: [{ 'x[]': 'y' }] },
'preserves inner [] while still treating outer [] as array push'
);
// Multiple nested bracket pairs: inner [] remains literal as part of the key
st.deepEqual(
qs.parse('a[b[c[]]]=d'),
{ a: { 'b[c[]]': 'd' } },
'treats "b[c[]]" as a literal key inside the bracket group'
);
// Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
st.deepEqual(
qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
{ a: { 'b[c[]]': { '[d]': 'e' } } },
'respects depth: 1 and preserves literal inner [] in the parsed key'
);
// Unterminated inner bracket group is wrapped as a literal remainder segment
st.deepEqual(
qs.parse('a[[]b=c'),
{ a: { '[[]b': 'c' } },
'handles unterminated inner bracket groups without throwing'
);
st.end();
});
t.test('currently parses unbalanced bracket keys after a parent leniently to literal segments (issue #558)', function (st) {
characterizeParse(st, 'a[bc=v', undefined, { a: { '[bc': 'v' } }, 'unclosed group after a parent');
characterizeParse(st, 'a[=v', undefined, { a: { '[': 'v' } }, 'bare unclosed bracket after a parent');
characterizeParse(st, 'a[b][c=v', undefined, { a: { b: { '[c': 'v' } } }, 'unclosed group after a valid one');
characterizeParse(st, 'a[b]c[d=v', undefined, { a: { b: { '[d': 'v' } } }, 'unclosed group after text following a valid one');
characterizeParse(st, 'filters[customtags:Env: Prod=v', undefined, { filters: { '[customtags:Env: Prod': 'v' } }, 'the issue #558 reproduction');
characterizeParse(st, '][a=v', undefined, { ']': { '[a': 'v' } }, 'stray close bracket before an unclosed group');
characterizeParse(st, 'a][b=v', undefined, { 'a]': { '[b': 'v' } }, 'stray close bracket inside the parent');
st.end();
});
t.test('currently parses unbalanced bracket keys containing inner brackets leniently (issue #558)', function (st) {
characterizeParse(st, 'a[b[c=v', undefined, { a: { '[b[c': 'v' } }, 'unclosed group containing an inner bracket');
characterizeParse(st, 'a[b[c]=v', undefined, { a: { '[b[c]': 'v' } }, 'unbalanced group with an inner bracket and one close');
characterizeParse(st, 'a[b][c[d=v', undefined, { a: { b: { '[c[d': 'v' } } }, 'unclosed inner-bracket group after a valid one');
st.end();
});
t.test('currently parses bracket-prefixed unbalanced keys leniently (issue #558)', function (st) {
characterizeParse(st, '[abc=v', undefined, { '[abc': 'v' }, 'key starting with an unclosed bracket');
characterizeParse(st, '[[]b=v', undefined, { '[[]b': 'v' }, 'key starting with an unbalanced bracket group');
st.end();
});
t.test('lenient unbalanced-bracket handling currently depends on the depth option (issue #558)', function (st) {
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');
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');
characterizeParse(st, 'a[bc=v', { depth: 0 }, { 'a[bc': 'v' }, 'depth 0 keeps the entire key literal');
st.end();
});
t.test('currently parses an allowDots key with a trailing unclosed bracket leniently (issue #558)', function (st) {
characterizeParse(st, 'a.b[c=v', { allowDots: true }, { a: { b: { '[c': 'v' } } }, 'allowDots expands the dot then keeps the unclosed bracket literal');
st.end();
});
t.test('valid and stray-close bracket keys are unaffected by unbalanced-bracket handling', function (st) {
characterizeParse(st, 'a]b=v', undefined, { 'a]b': 'v' }, 'stray close bracket with no open bracket stays a flat key');
characterizeParse(st, 'a[b]extra=v', undefined, { a: { b: 'v' } }, 'text after a balanced group is ignored');
st.end();
});
t.test('allows to specify array indices', function (st) {
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
@@ -665,6 +774,21 @@ test('parse()', function (t) {
st.end();
});
t.test('does not crash on multi-step circular references', function (st) {
var a = {};
a.b = { c: { d: a } };
var parsed;
st.doesNotThrow(function () {
parsed = qs.parse({ foo: a });
});
st.equal('foo' in parsed, true, 'parsed has "foo" property');
st.equal(parsed.foo.b.c.d, parsed.foo, 'the multi-step cycle is preserved');
st.end();
});
t.test('does not crash when parsing deep objects', function (st) {
var parsed;
var str = 'foo';
@@ -898,6 +1022,22 @@ test('parse()', function (t) {
st.end();
});
t.test('object-valued input with own `__proto__` does not mutate sub-object [[Prototype]]', function (st) {
// JSON.parse creates own data `__proto__` properties (via CreateDataProperty),
// which would trigger the Object.prototype.__proto__ accessor if merged via `acc[key] = value`.
var out = qs.parse({
'user[name]': 'alice',
user: JSON.parse('{"__proto__":{"isAdmin":true}}')
}, { allowPrototypes: false });
st.equal(out.user.name, 'alice', 'name from bracket key is preserved');
st.equal(out.user.isAdmin, undefined, 'attacker-controlled inherited property is not exposed');
st.equal(Object.getPrototypeOf(out.user), Object.prototype, 'sub-object [[Prototype]] is unchanged');
st.equal(Object.prototype.isAdmin, undefined, 'Object.prototype is not polluted');
st.end();
});
t.test('can return null objects', { skip: !hasProto }, function (st) {
var expected = {
__proto__: null,
@@ -1074,6 +1214,15 @@ test('parse()', function (t) {
};
st.deepEqual(qs.parse('KeY=vAlUe', { decoder: decoder }), { key: 'VALUE' });
var noopDecoder = function () { return 'x'; };
noopDecoder();
st['throws'](
function () { decoder('x', noopDecoder, 'utf-8', 'unknown'); },
'this should never happen! type: unknown',
'decoder throws for unexpected type'
);
st.end();
});
@@ -1103,6 +1252,14 @@ test('parse()', function (t) {
new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
'throws error when parameter limit is exceeded'
);
sst['throws'](
function () {
qs.parse('a=1&b=2', { parameterLimit: 1, throwOnLimitExceeded: true });
},
new RangeError('Parameter limit exceeded. Only 1 parameter allowed.'),
'throws error with singular "parameter" when parameterLimit is 1'
);
sst.end();
});
@@ -1124,6 +1281,12 @@ test('parse()', function (t) {
sst.end();
});
st.test('allows unlimited parameters when parameterLimit is Infinity and throwOnLimitExceeded is true', function (sst) {
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation or throwing');
sst.end();
});
st.end();
});
@@ -1189,6 +1352,14 @@ test('parse()', function (t) {
'throws error when a sparse index exceeds arrayLimit'
);
sst['throws'](
function () {
qs.parse('a[2]=b', { arrayLimit: 1, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws error with singular "element" when arrayLimit is 1'
);
sst.end();
});
@@ -1206,6 +1377,168 @@ test('parse()', function (t) {
sst.end();
});
st.test('throws when duplicate bracket keys exceed arrayLimit with throwOnLimitExceeded', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
'throws error when duplicate bracket notation exceeds array limit'
);
sst.end();
});
st.test('throws when cumulative comma + duplicate-key combine exceeds arrayLimit', function (sst) {
sst['throws'](
function () {
qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
'throws when comma groups within the limit cumulatively exceed it across duplicate keys'
);
sst['throws'](
function () {
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 });
},
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
'throws on a subsequent part once the cumulative array is already over the limit'
);
sst.end();
});
st.test('throws when plain duplicate keys combine past arrayLimit at the boundary', function (sst) {
sst['throws'](
function () { qs.parse('a=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'duplicate scalar keys'
);
sst['throws'](
function () { qs.parse('a[]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'duplicate bracket keys'
);
sst.end();
});
st.test('throws when mixed index and key notation merge past arrayLimit', function (sst) {
sst['throws'](
function () { qs.parse('a=x&a[0]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'scalar then index that overflows on merge'
);
sst['throws'](
function () { qs.parse('a[0]=1&a[1]=2&a=3', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'indexed array then scalar that overflows on merge'
);
sst.end();
});
st.test('enforces arrayLimit on merge at the boundary, consistently with combine', function (sst) {
sst['throws'](
function () { qs.parse('a[0]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'a trailing scalar merged into an at-limit array throws'
);
sst.deepEqual(
qs.parse('a[0]=x&a=y', { arrayLimit: 1 }),
{ a: { 0: 'x', 1: 'y' } },
'and converts to an overflow object without throwOnLimitExceeded'
);
sst['throws'](
function () { qs.parse('a[0]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'mixed index and bracket notation merged past the limit throws'
);
sst.deepEqual(
qs.parse('a[0]=x&a[]=y', { arrayLimit: 1 }),
{ a: { 0: 'x', 1: 'y' } },
'mixed index and bracket notation converts like duplicate-bracket combine'
);
sst.end();
});
st.test('does not throw when cumulative comma combine stays within arrayLimit', function (sst) {
var result = qs.parse('a=1,2,3&a=4', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: ['1', '2', '3', '4'] }, 'combined array within limit is preserved');
sst.end();
});
st.test('silently combines to an overflow object when throwOnLimitExceeded is not set', function (sst) {
var result = qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5 });
sst.deepEqual(result, { a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6' } }, 'converts to object without throwing');
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');
sst.end();
});
st.test('throws before splitting when a single comma value 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.'),
'a flat comma value over the limit throws'
);
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,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
'a non-bracket nested key comma value over the limit throws'
);
sst.end();
});
st.test('does not throw for a single comma value within arrayLimit', function (sst) {
sst.deepEqual(
qs.parse('a=1,2,3', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
{ a: ['1', '2', '3'] },
'within the limit'
);
sst.deepEqual(
qs.parse('a=1,2,3,4,5', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
{ a: ['1', '2', '3', '4', '5'] },
'exactly at the limit'
);
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');
sst.end();
});
st.test('throws for a bracketed comma group when arrayLimit is 0', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1,2,3', { comma: true, arrayLimit: 0, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
'a single bracketed element still exceeds arrayLimit 0'
);
sst.end();
});
st.end();
});
@@ -1462,6 +1795,14 @@ test('comma + arrayLimit', function (t) {
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'throws error when comma-split exceeds array limit'
);
st['throws'](
function () {
qs.parse('a=1,2,3', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws error with singular "element" when arrayLimit is 1'
);
st.end();
});
@@ -1564,5 +1905,29 @@ test('mixed array and object notation', function (t) {
st.end();
});
t.test('uses existing array length for currentArrayLength when parsing object input with bracket keys', function (st) {
var input = {};
var arr = ['x', 'y'];
arr.a = ['z', 'w'];
input['a[]'] = arr;
st.deepEqual(qs.parse(input), { a: ['x', 'y'] }, 'parses object input with bracket keys using existing array values');
st.end();
});
t.test('throws with singular message when object input bracket key exceeds arrayLimit of 1', function (st) {
var input = {};
var arr = ['x'];
arr.a = ['z', 'w'];
input['a[]'] = arr;
st['throws'](
function () {
qs.parse(input, { throwOnLimitExceeded: true, arrayLimit: 1 });
},
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws singular error for object input exceeding arrayLimit 1'
);
st.end();
});
t.end();
});
+138
View File
@@ -651,6 +651,49 @@ test('stringify()', function (t) {
st.end();
});
t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
st.doesNotThrow(
function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
'does not pass a raw null array entry to the encoder'
);
st.doesNotThrow(
function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
'does not pass a raw undefined array entry to the encoder'
);
st.doesNotThrow(
function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
'does not crash on a single-null array'
);
st.equal(
qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
'a=,b',
'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
);
st.equal(
qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
'a=,b',
'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
);
st.equal(
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
'a=',
'single-null array stringifies as empty value'
);
st.equal(
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
'a',
'strictNullHandling drops the equals sign for a single-null array'
);
st.equal(
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
'',
'skipNulls drops a single-null array entirely'
);
st.end();
});
t.test('stringifies a null object', { skip: !hasProto }, function (st) {
st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
st.end();
@@ -825,6 +868,35 @@ test('stringify()', function (t) {
st.end();
});
t.test('skips null/undefined entries in filter=array', function (st) {
st.doesNotThrow(
function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
'does not pass a raw undefined filter entry to the encoder'
);
st.doesNotThrow(
function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
'does not pass a raw null filter entry to the encoder'
);
st.equal(
qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
'a=b&c=d',
'undefined filter entry is skipped, remaining keys are kept'
);
st.equal(
qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
'a=b&c=d',
'null filter entry is skipped, remaining keys are kept'
);
st.equal(
qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
'',
'filter array containing only null yields empty string'
);
st.end();
});
t.test('supports custom representations when filter=function', function (st) {
var calls = 0;
var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
@@ -1111,6 +1183,28 @@ test('stringify()', function (t) {
st.end();
});
t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
st.equal(
qs.stringify(
{ 'a b': null, 'c d': 'e f' },
{ strictNullHandling: false, format: 'RFC1738' }
),
'a+b=&c+d=e+f',
'without: as expected'
);
st.equal(
qs.stringify(
{ 'a b': null, 'c d': 'e f' },
{ strictNullHandling: true, format: 'RFC1738' }
),
'a+b&c+d=e+f',
'with: as expected'
);
st.end();
});
t.test('throws if an invalid charset is specified', function (st) {
st['throws'](function () {
qs.stringify({ a: 'b' }, { charset: 'foobar' });
@@ -1146,6 +1240,12 @@ test('stringify()', function (t) {
'adds the right sentinel when instructed to and the charset is iso-8859-1'
);
st.equal(
qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
'utf8=%E2%9C%93;a=1;b=2',
'uses the configured delimiter after the sentinel'
);
st.end();
});
@@ -1188,6 +1288,15 @@ test('stringify()', function (t) {
};
st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
var noopEncoder = function () { return 'x'; };
noopEncoder();
st['throws'](
function () { encoder('x', noopEncoder, 'utf-8', 'unknown'); },
'this should never happen! type: unknown',
'encoder throws for unexpected type'
);
st.end();
});
@@ -1307,4 +1416,33 @@ test('stringifies empty keys', function (t) {
st.end();
});
t.test('round-trips keys containing percent-encoded bracket text', function (st) {
var cases = [
{ 'a%5Bb': 'c' },
{ 'a%5Db': 'c' },
{ 'a%255Bb': 'c' },
{ 'a%255Db': 'c' },
{ a: { 'b%5Bc': 'd' } },
{ a: { 'b%255Bc': 'd' } },
{ 'a%5B%255Bb': 'c' }
];
for (var i = 0; i < cases.length; i++) {
st.deepEqual(
qs.parse(qs.stringify(cases[i])),
cases[i],
'round-trips ' + JSON.stringify(cases[i])
);
}
st.end();
});
t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
st.end();
});
});
+225 -3
View File
@@ -31,6 +31,7 @@ test('merge()', function (t) {
t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });
var func = function f() {};
func();
t.deepEqual(
utils.merge(func, { foo: 'bar' }),
[func, { foo: 'bar' }],
@@ -64,6 +65,7 @@ test('merge()', function (t) {
observed[0] = observed[0]; // eslint-disable-line no-self-assign
st.equal(setCount, 1);
st.equal(getCount, 2);
st.end();
}
);
@@ -77,6 +79,7 @@ test('merge()', function (t) {
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
var merged = utils.merge(overflow, 'd');
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'adds primitive at next numeric index');
s2t.end();
});
@@ -92,6 +95,7 @@ test('merge()', function (t) {
var obj = { foo: 'bar' };
var merged = utils.merge(obj, 'baz');
s2t.deepEqual(merged, { foo: 'bar', baz: true }, 'adds primitive as key with value true');
s2t.end();
});
@@ -109,6 +113,17 @@ test('merge()', function (t) {
var merged = utils.merge('c', overflow);
s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
s2t.deepEqual(merged, { 0: 'c', 1: 'a', 2: 'b' }, 'creates object with primitive at 0, source values shifted');
s2t.end();
});
st.test('merges overflow object into primitive with plainObjects', function (s2t) {
var overflow = utils.combine(['a'], 'b', 0, false);
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
var merged = utils.merge('c', overflow, { plainObjects: true });
s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
s2t.deepEqual(merged, { __proto__: null, 0: 'c', 1: 'a', 2: 'b' }, 'creates null-proto object with primitive at 0');
s2t.end();
});
@@ -118,6 +133,7 @@ test('merge()', function (t) {
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
var merged = utils.merge('a', overflow);
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'shifts all source indices by 1');
s2t.end();
});
@@ -125,6 +141,95 @@ test('merge()', function (t) {
var obj = { foo: 'bar' };
var merged = utils.merge('a', obj);
s2t.deepEqual(merged, ['a', { foo: 'bar' }], 'creates array with primitive and object');
s2t.end();
});
st.test('merges primitive into array that exceeds arrayLimit', function (s2t) {
var arr = ['a', 'b', 'c'];
var merged = utils.merge(arr, 'd', { arrayLimit: 1 });
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'converts to overflow object with primitive appended');
s2t.end();
});
st.test('merges array into primitive that exceeds arrayLimit', function (s2t) {
var merged = utils.merge('a', ['b', 'c'], { arrayLimit: 1 });
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c' }, 'converts to overflow object');
s2t.end();
});
st.test('merges primitive into array at the arrayLimit boundary, consistently with combine', function (s2t) {
var merged = utils.merge(['a'], 'b', { arrayLimit: 1 });
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow at the boundary');
s2t.deepEqual(merged, { 0: 'a', 1: 'b' }, 'converts to overflow object instead of a length-2 array');
s2t.end();
});
st.test('merges two arrays that exceed arrayLimit into an overflow object', function (s2t) {
var merged = utils.merge(['a'], ['b'], { arrayLimit: 1 });
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
s2t.deepEqual(merged, { 0: 'a', 1: 'b' }, 'array-into-array merge enforces arrayLimit like combine');
s2t.end();
});
st.test('throws at the arrayLimit boundary when merging a primitive into an array with throwOnLimitExceeded', function (s2t) {
s2t['throws'](
function () { utils.merge(['a'], 'b', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws when the resulting length would exceed arrayLimit'
);
s2t.end();
});
st.test('throws when merging two arrays past arrayLimit with throwOnLimitExceeded', function (s2t) {
s2t['throws'](
function () { utils.merge(['a'], ['b'], { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'array-into-array merge throws rather than silently exceeding arrayLimit'
);
s2t['throws'](
function () { utils.merge(['a', 'b', 'c'], ['d', 'e', 'f'], { arrayLimit: 2, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
'uses the plural message when arrayLimit is not 1'
);
s2t.end();
});
st.test('throws instead of merging primitive into over-limit array when throwOnLimitExceeded is set', function (s2t) {
s2t['throws'](
function () { utils.merge(['a', 'b', 'c'], 'd', { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws rather than converting to an overflow object'
);
s2t['throws'](
function () { utils.merge(['a', 'b', 'c'], 'd', { arrayLimit: 2, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
'uses the plural message when arrayLimit is not 1'
);
s2t.end();
});
st.test('throws instead of merging array into primitive when throwOnLimitExceeded is set', function (s2t) {
s2t['throws'](
function () { utils.merge('a', ['b', 'c'], { arrayLimit: 1, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'throws rather than converting to an overflow object'
);
s2t['throws'](
function () { utils.merge('a', ['b', 'c', 'd'], { arrayLimit: 2, throwOnLimitExceeded: true }); },
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
'uses the plural message when arrayLimit is not 1'
);
s2t.end();
});
@@ -249,6 +354,47 @@ test('combine()', function (t) {
st.end();
});
t.test('with throwOnLimitExceeded', function (st) {
st.test('throws when concatenation exceeds the limit', function (s2t) {
s2t['throws'](
function () { utils.combine(['a', 'b', 'c'], 'd', 3, false, true); },
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'throws instead of converting to an overflow object'
);
s2t['throws'](
function () { utils.combine([], 'a', 0, false, true); },
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
'throws with the correct count at arrayLimit 0'
);
s2t.end();
});
st.test('throws when adding to an existing overflow object', function (s2t) {
var overflow = utils.combine(['a', 'b'], 'c', 0, false);
s2t.ok(utils.isOverflow(overflow), 'initial object is marked as overflow');
s2t['throws'](
function () { utils.combine(overflow, 'd', 0, false, true); },
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
'throws rather than appending to the overflow object'
);
s2t['throws'](
function () { utils.combine(overflow, 'd', 1, false, true); },
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
'uses the singular message at arrayLimit 1'
);
s2t.end();
});
st.test('does not throw when within the limit', function (s2t) {
var combined = utils.combine(['a'], 'b', 5, false, true);
s2t.deepEqual(combined, ['a', 'b'], 'returns the array unchanged when under the limit');
s2t.end();
});
st.end();
});
t.test('with existing overflow object', function (st) {
st.test('adds to existing overflow object at next index', function (s2t) {
// Create overflow object first via combine: 3 elements (indices 0-2) with limit 0
@@ -347,6 +493,79 @@ test('encode', function (t) {
'encodes a long string'
);
var boundary = '';
var expected = '';
for (var j = 0; j < 1023; j++) {
boundary += 'a';
expected += 'a';
}
boundary += '😀';
expected += '%F0%9F%98%80';
t.equal(
utils.encode(boundary),
expected,
'encodes a surrogate pair split across long-string chunks'
);
var laterBoundary = '';
var laterExpected = '';
for (var k = 0; k < 2047; k++) {
laterBoundary += 'a';
laterExpected += 'a';
}
laterBoundary += '😀';
laterExpected += '%F0%9F%98%80';
t.equal(
utils.encode(laterBoundary),
laterExpected,
'encodes a surrogate pair split across a later chunk boundary'
);
var twoPairs = '';
for (k = 0; k < 1023; k++) {
twoPairs += 'a';
}
twoPairs += '😀';
for (k = 0; k < 1022; k++) {
twoPairs += 'b';
}
twoPairs += '😀';
t.equal(
(utils.encode(twoPairs).match(/%F0%9F%98%80/g) || []).length,
2,
'encodes two surrogate pairs each split across a chunk boundary'
);
var roundTrip = '';
for (k = 0; k < 1023; k++) {
roundTrip += 'a';
}
roundTrip += '😀';
t.equal(
decodeURIComponent(utils.encode(roundTrip)),
roundTrip,
'a boundary-split surrogate pair round-trips through decodeURIComponent'
);
var loneBoundary = '';
var loneExpected = '';
for (k = 0; k < 1023; k++) {
loneBoundary += 'a';
loneExpected += 'a';
}
loneBoundary += '\uD83DX';
loneExpected += '%F0%9F%91%98';
t.equal(
utils.encode(loneBoundary),
loneExpected,
'a lone high surrogate at a chunk boundary encodes the same as mid-chunk'
);
t.equal(
utils.encode('\x28\x29'),
'%28%29',
@@ -376,7 +595,9 @@ test('encode', function (t) {
});
test('isBuffer()', function (t) {
forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g], function (x) {
var fn = function () {};
fn();
forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], fn, /a/g], function (x) {
t.equal(utils.isBuffer(x), false, inspect(x) + ' is not a buffer');
});
@@ -386,8 +607,9 @@ test('isBuffer()', function (t) {
var saferBuffer = SaferBuffer.from('abc');
t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer instance is a buffer');
var buffer = Buffer.from && Buffer.alloc ? Buffer.from('abc') : new Buffer('abc');
t.equal(utils.isBuffer(buffer), true, 'real Buffer instance is a buffer');
var buffer = SaferBuffer.from('abc');
t.notEqual(saferBuffer, buffer, 'different buffer instances');
t.equal(utils.isBuffer(buffer), true, 'another Buffer instance is a buffer');
t.end();
});