From d11a8ec7f7c23e899bb1be34426c30f0d630a09c Mon Sep 17 00:00:00 2001 From: Alexander Ryzhikov Date: Sat, 5 Mar 2022 07:01:08 +0200 Subject: [PATCH] fix: allow parsing of empty keys in objects --- lib/parse.js | 9 +++++++-- test/parse.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index a4ac4fa0..87a31963 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -7,6 +7,7 @@ var isArray = Array.isArray; var defaults = { allowDots: false, + allowEmptyKeys: false, allowPrototypes: false, allowSparse: false, arrayLimit: 20, @@ -83,6 +84,9 @@ var parseValues = function parseQueryStringValues(str, options) { var key, val; if (pos === -1) { key = options.decoder(part, defaults.decoder, charset, 'key'); + if (key === '') { + continue; + } val = options.strictNullHandling ? null : ''; } else { key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key'); @@ -148,7 +152,7 @@ var parseObject = function (chain, val, options, valuesParsed) { }; var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) { - if (!givenKey) { + if (options.allowEmptyKeys ? givenKey !== '' && !givenKey : !givenKey) { return; } @@ -168,7 +172,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars // Stash the parent if it exists var keys = []; - if (parent) { + if (parent || (options.allowEmptyKeys && parent === '')) { // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties if (!options.plainObjects && has.call(Object.prototype, parent)) { if (!options.allowPrototypes) { @@ -217,6 +221,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) { return { allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + allowEmptyKeys: typeof opts.allowEmptyKeys === 'undefined' ? defaults.allowEmptyKeys : !!opts.allowEmptyKeys, allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes, allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse, arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit, diff --git a/test/parse.js b/test/parse.js index 9734f428..5a6d9aa7 100644 --- a/test/parse.js +++ b/test/parse.js @@ -843,3 +843,52 @@ test('parse()', function (t) { t.end(); }); + +test('parse/stringify empty key WIP', function (t) { + t.test('parses an object with empty string key', function (st) { + st.deepEqual(qs.parse('=a', { allowEmptyKeys: true }), { '': 'a' }); + st.deepEqual(qs.parse('=', { allowEmptyKeys: true }), { '': '' }); + + st.deepEqual(qs.parse('=&', { allowEmptyKeys: true }), { '': '' }); + st.deepEqual(qs.parse('&=&', { allowEmptyKeys: true }), { '': '' }); + st.deepEqual(qs.parse('&=', { allowEmptyKeys: true }), { '': '' }); + st.deepEqual(qs.parse('&', { allowEmptyKeys: true }), { }); + st.deepEqual(qs.parse('=&&&', { allowEmptyKeys: true }), { '': '' }); + + st.deepEqual(qs.parse('[deep]=a&[deep]=2', { allowEmptyKeys: true }), { '': { deep: ['a', '2'] } }); + st.deepEqual(qs.parse('=a&=b', { allowEmptyKeys: true }), { '': ['a', 'b'] }); + st.deepEqual(qs.parse('%5B0%5D=a&%5B1%5D=b', { allowEmptyKeys: true }), { '': ['a', 'b'] }); + st.deepEqual(qs.parse('=a&foo=b', { allowEmptyKeys: true }), { '': 'a', foo: 'b' }); + st.deepEqual(qs.parse('[]=a&[]=b& []=1', { allowEmptyKeys: true }), { '': ['a', 'b'], ' ': ['1'] }); + + st.deepEqual(qs.parse('=&a[]=b', { allowEmptyKeys: true }), { '': '', a: ['b'] }); + st.deepEqual(qs.parse('=&a[]=b&a[1]=c', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + st.deepEqual(qs.parse('=&a[]=b&a[]=c&a[2]=d', { allowEmptyKeys: true }), { '': '', a: ['b', 'c', 'd'] }); + + st.deepEqual(qs.parse('a=b&a[]=c&=', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + st.deepEqual(qs.parse('a[]=b&a=c&=', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + st.deepEqual(qs.parse('a[0]=b&a=c&=', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + st.deepEqual(qs.parse('a=b&a[0]=c&=', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + + st.deepEqual(qs.parse('a[]=b&a=c&=', { allowEmptyKeys: true }), { '': '', a: ['b', 'c'] }); + + st.end(); + + }); + + t.test('stringifies an object with empty string key', function (st) { + st.deepEqual(qs.stringify({ '': 'a' }), '=a'); + st.deepEqual(qs.stringify({ '': ['a', 'b'] }), '%5B0%5D=a&%5B1%5D=b'); + st.deepEqual(qs.stringify({ '': ['a', 'b'], a: [1, 2] }, { encode: false }), '[0]=a&[1]=b&a[0]=1&a[1]=2'); + + st.end(); + }); + t.test('edge case with object/arrays', function (st) { + + st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3'); + + st.deepEqual(qs.parse('[][0]=2&[][1]=3', { allowEmptyKeys: true }), { '': { '': [2, 3] } }); + + st.end(); + }); +});