Repository: sindresorhus/query-string (js)

Issue:
Bug: comma separated arrays not parsed correctly from encoded url
Test case:

```ts
it('should parse arrays correctly', () => {
    const search =
      '?testA%5B%5D=1&testB%5B%5D=a%2Cb%2Cc%2Cd%2Ce%2Cf&testC=true';

    const result = queryString.parse(search, {
      parseBooleans: true,
      parseNumbers: true,
      arrayFormat: 'bracket-separator',
    });

    expect(result).toEqual({
      testA: [1],
      testB: ['a', 'b', 'c', 'd', 'e', 'f'],
      testC: true,
    });
  });
```

results in:

![image](https://github.com/user-attachments/assets/8c40a304-30ee-4ab5-bb50-88a2a3d187a6)

when it's unencoded it works fine:

```ts
it('should parse arrays correctly', () => {
    const search =
      '?testA%5B%5D=1&testB%5B%5D=a,b,c,d,e,f&testC=true';

    const result = queryString.parse(search, {
      parseBooleans: true,
      parseNumbers: true,
      arrayFormat: 'bracket-separator',
    });

    expect(result).toEqual({
      testA: [1],
      testB: ['a', 'b', 'c', 'd', 'e', 'f'],
      testC: true,
    });
  });
```

Library version: 9.1.0



Pull request description:
FIX: Decode back-separator value before splitting it with separator
The original implementation was splitting the received value before it then decoded the value. Because the value was URL encoded, the split function could not find the separator to successfully split. Adjusted the order of operations which fixes #388

- Adjusted order of operations, ensuring we decode before splitting the value
- Implemented test case to guard against this regression.

Patch:
diff --git a/base.js b/base.js
index ccbdb6e..ad9e7b1 100644
--- a/base.js
+++ b/base.js
@@ -221,7 +221,7 @@ function parserForArrayFormat(options) {
 
 				const arrayValue = value === null
 					? []
-					: value.split(options.arrayFormatSeparator).map(item => decode(item, options));
+					: decode(value, options).split(options.arrayFormatSeparator);
 
 				if (accumulator[key] === undefined) {
 					accumulator[key] = arrayValue;


Test patch:
diff --git a/test/parse.js b/test/parse.js
index 3409255..6e2261a 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -110,7 +110,8 @@ test('handle multiple values and preserve appearance order with indexes', t => {
 });
 
 test('query strings params including embedded `=`', t => {
-	t.deepEqual(queryString.parse('?param=https%3A%2F%2Fsomeurl%3Fid%3D2837'), {param: 'https://someurl?id=2837'});
+	const value = 'https://someurl?id=2837';
+	t.deepEqual(queryString.parse(`param=${encodeURIComponent(value)}`), {param: 'https://someurl?id=2837'});
 });
 
 test('object properties', t => {
@@ -215,6 +216,16 @@ test('query strings having a brackets+separator array and format option as `brac
 	}), {foo: ['']});
 });
 
+test('query strings having a brackets+separator array and format option as `bracket-separator` with a URL encoded value', t => {
+	const key = 'foo[]';
+	const value = 'a,b,c,d,e,f';
+	t.deepEqual(queryString.parse(`?${encodeURIComponent(key)}=${encodeURIComponent(value)}`, {
+		arrayFormat: 'bracket-separator',
+	}), {
+		foo: ['a', 'b', 'c', 'd', 'e', 'f'],
+	});
+});
+
 test('query strings having = within parameters (i.e. GraphQL IDs)', t => {
 	t.deepEqual(queryString.parse('foo=bar=&foo=ba=z='), {foo: ['bar=', 'ba=z=']});
 });
@@ -305,7 +316,8 @@ test('decode keys and values', t => {
 });
 
 test('disable decoding of keys and values', t => {
-	t.deepEqual(queryString.parse('tags=postal%20office,burger%2C%20fries%20and%20coke', {decode: false}), {tags: 'postal%20office,burger%2C%20fries%20and%20coke'});
+	const value = 'postal office,burger, fries and coke';
+	t.deepEqual(queryString.parse(`tags=${encodeURIComponent(value)}`, {decode: false}), {tags: 'postal%20office%2Cburger%2C%20fries%20and%20coke'});
 });
 
 test('number value returns as string by default', t => {
@@ -376,7 +388,8 @@ test('parse throws TypeError for invalid arrayFormatSeparator', t => {
 });
 
 test('query strings having comma encoded and format option as `comma`', t => {
-	t.deepEqual(queryString.parse('foo=zero%2Cone,two%2Cthree', {arrayFormat: 'comma'}), {
+	const values = ['zero,one', 'two,three'];
+	t.deepEqual(queryString.parse(`foo=${encodeURIComponent(values[0])},${encodeURIComponent(values[1])}`, {arrayFormat: 'comma'}), {
 		foo: [
 			'zero,one',
 			'two,three',
@@ -392,7 +405,8 @@ test('value should not be decoded twice with `arrayFormat` option set as `separa
 
 // See https://github.com/sindresorhus/query-string/issues/242
 test('value separated by encoded comma will not be parsed as array with `arrayFormat` option set to `comma`', t => {
-	t.deepEqual(queryString.parse('id=1%2C2%2C3', {arrayFormat: 'comma', parseNumbers: true}), {
+	const value = '1,2,3';
+	t.deepEqual(queryString.parse(`id=${encodeURIComponent(value)}`, {arrayFormat: 'comma', parseNumbers: true}), {
 		id: [1, 2, 3],
 	});
 });
@@ -406,7 +420,8 @@ test('query strings having (:list) colon-list-separator arrays including null va
 });
 
 test('types option: can override a parsed number to be a string ', t => {
-	t.deepEqual(queryString.parse('phoneNumber=%2B380951234567', {
+	const phoneNumber = '+380951234567';
+	t.deepEqual(queryString.parse(`phoneNumber=${encodeURIComponent(phoneNumber)}`, {
 		parseNumbers: true,
 		types: {
 			phoneNumber: 'string',
@@ -426,7 +441,7 @@ test('types option: can override a parsed boolean value to be a string', t => {
 });
 
 test('types option: can override parsed numbers arrays to be string[]', t => {
-	t.deepEqual(queryString.parse('ids=999%2C998%2C997&items=1%2C2%2C3', {
+	t.deepEqual(queryString.parse('ids=999,998,997&items=1,2,3', {
 		arrayFormat: 'comma',
 		parseNumbers: true,
 		types: {
@@ -439,7 +454,7 @@ test('types option: can override parsed numbers arrays to be string[]', t => {
 });
 
 test('types option: can override string arrays to be number[]', t => {
-	t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
+	t.deepEqual(queryString.parse('ids=1,2,3&items=1,2,3', {
 		arrayFormat: 'comma',
 		types: {
 			ids: 'number[]',
@@ -451,7 +466,7 @@ test('types option: can override string arrays to be number[]', t => {
 });
 
 test('types option: can override an array to be string', t => {
-	t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
+	t.deepEqual(queryString.parse('ids=001,002,003&items=1,2,3', {
 		arrayFormat: 'comma',
 		parseNumbers: true,
 		types: {
@@ -488,7 +503,7 @@ test('types option: when value is not of specified type, it will safely parse th
 });
 
 test('types option: array types will have no effect if arrayFormat is set to "none"', t => {
-	t.deepEqual(queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
+	t.deepEqual(queryString.parse('ids=001,002,003&foods=apple,orange,mango', {
 		arrayFormat: 'none',
 		types: {
 			ids: 'number[]',
@@ -512,7 +527,7 @@ test('types option: will parse the value as number if specified in type but pars
 });
 
 test('types option: all supported types work in conjunction with one another', t => {
-	t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&numbers=1%2C2%2C3&double=5&number=20', {
+	t.deepEqual(queryString.parse('ids=001,002,003&items=1,2,3&price=22.00&numbers=1,2,3&double=5&number=20', {
 		arrayFormat: 'comma',
 		types: {
 			ids: 'string',