update
This commit is contained in:
+27
@@ -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)
|
||||
|
||||
+1
-1
@@ -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' });
|
||||
|
||||
+33
-33
File diff suppressed because one or more lines are too long
+1
@@ -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
@@ -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
@@ -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('✓'), 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user