Skip to content

Commit a419ce5

Browse files
techouseljharb
authored andcommitted
[Fix] parse: handle nested bracket groups and add regression tests
1 parent 3f5e1c5 commit a419ce5

2 files changed

Lines changed: 101 additions & 22 deletions

File tree

lib/parse.js

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,12 @@ var parseObject = function (chain, val, options, valuesParsed) {
214214
return leaf;
215215
};
216216

217-
var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
218-
var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey;
217+
// Split a key like "a[b][c[]]" into ['a', '[b]', '[c[]]'] while preserving
218+
// qs parse semantics for depth/prototype guards.
219+
var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
220+
var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;
219221

222+
// depth <= 0 keeps the whole key as one segment
220223
if (options.depth <= 0) {
221224
if (!options.plainObjects && has.call(Object.prototype, key)) {
222225
if (!options.allowPrototypes) {
@@ -227,47 +230,74 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
227230
return [key];
228231
}
229232

230-
var brackets = /(\[[^[\]]*])/;
231-
var child = /(\[[^[\]]*])/g;
232-
233-
var segment = brackets.exec(key);
234-
var parent = segment ? key.slice(0, segment.index) : key;
235-
236-
var keys = [];
233+
var segments = [];
237234

235+
// parent before the first '[' (may be empty if key starts with '[')
236+
var first = key.indexOf('[');
237+
var parent = first >= 0 ? key.slice(0, first) : key;
238238
if (parent) {
239239
if (!options.plainObjects && has.call(Object.prototype, parent)) {
240240
if (!options.allowPrototypes) {
241241
return;
242242
}
243243
}
244244

245-
keys[keys.length] = parent;
245+
segments[segments.length] = parent;
246246
}
247247

248-
var i = 0;
249-
while ((segment = child.exec(key)) !== null && i < options.depth) {
250-
i += 1;
251-
252-
var segmentContent = segment[1].slice(1, -1);
253-
if (!options.plainObjects && has.call(Object.prototype, segmentContent)) {
254-
if (!options.allowPrototypes) {
255-
return;
248+
var n = key.length;
249+
var open = first;
250+
var collected = 0;
251+
252+
while (open >= 0 && collected < options.depth) {
253+
var level = 1;
254+
var i = open + 1;
255+
var close = -1;
256+
257+
// balance nested '[' and ']' inside this bracket group using a nesting level counter
258+
while (i < n && close < 0) {
259+
var cu = key.charCodeAt(i);
260+
if (cu === 0x5B) { // '['
261+
level += 1;
262+
} else if (cu === 0x5D) { // ']'
263+
level -= 1;
264+
if (level === 0) {
265+
close = i; // found matching close; loop will exit by condition
266+
}
256267
}
268+
i += 1;
257269
}
258270

259-
keys[keys.length] = segment[1];
271+
if (close < 0) {
272+
// Unterminated group: wrap the raw remainder in one bracket pair so it stays
273+
// a single literal segment (e.g. "[[]b" -> "[[]b]"); we do not infer missing ']'.
274+
segments[segments.length] = '[' + key.slice(open) + ']';
275+
return segments;
276+
}
277+
278+
var seg = key.slice(open, close + 1);
279+
// prototype guard for the content of this group
280+
var content = seg.slice(1, -1);
281+
if (!options.plainObjects && has.call(Object.prototype, content) && !options.allowPrototypes) {
282+
return;
283+
}
284+
285+
segments[segments.length] = seg;
286+
collected += 1;
287+
288+
// find the next '[' after this balanced group
289+
open = key.indexOf('[', close + 1);
260290
}
261291

262-
if (segment) {
292+
if (open >= 0) {
263293
if (options.strictDepth === true) {
264294
throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
265295
}
266296

267-
keys[keys.length] = '[' + key.slice(segment.index) + ']';
297+
segments[segments.length] = '[' + key.slice(open) + ']';
268298
}
269299

270-
return keys;
300+
return segments;
271301
};
272302

273303
var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {

test/parse.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ test('parse()', function (t) {
210210
t.test('uses original key when depth = 0', function (st) {
211211
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
212212
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' });
213+
st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
214+
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
215+
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
213216
st.end();
214217
});
215218

@@ -263,6 +266,52 @@ test('parse()', function (t) {
263266
st.end();
264267
});
265268

269+
t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
270+
// A bracket pair inside a bracket group should be treated literally as part of the key
271+
st.deepEqual(
272+
qs.parse('search[withbracket[]]=foobar'),
273+
{ search: { 'withbracket[]': 'foobar' } },
274+
'treats inner [] literally when inside a bracket group'
275+
);
276+
277+
// Single-level variant
278+
st.deepEqual(
279+
qs.parse('a[b[]]=c'),
280+
{ a: { 'b[]': 'c' } },
281+
'keeps "b[]" as a literal key'
282+
);
283+
284+
// Nested with an array push on the outer level
285+
st.deepEqual(
286+
qs.parse('list[][x[]]=y'),
287+
{ list: [{ 'x[]': 'y' }] },
288+
'preserves inner [] while still treating outer [] as array push'
289+
);
290+
291+
// Multiple nested bracket pairs: inner [] remains literal as part of the key
292+
st.deepEqual(
293+
qs.parse('a[b[c[]]]=d'),
294+
{ a: { 'b[c[]]': 'd' } },
295+
'treats "b[c[]]" as a literal key inside the bracket group'
296+
);
297+
298+
// Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
299+
st.deepEqual(
300+
qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
301+
{ a: { 'b[c[]]': { '[d]': 'e' } } },
302+
'respects depth: 1 and preserves literal inner [] in the parsed key'
303+
);
304+
305+
// Unterminated inner bracket group is wrapped as a literal remainder segment
306+
st.deepEqual(
307+
qs.parse('a[[]b=c'),
308+
{ a: { '[[]b': 'c' } },
309+
'handles unterminated inner bracket groups without throwing'
310+
);
311+
312+
st.end();
313+
});
314+
266315
t.test('allows to specify array indices', function (st) {
267316
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
268317
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });

0 commit comments

Comments
 (0)