Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ljharb/qs
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0e838daa71f91fecda456441ac64e615f38bed8b
Choose a base ref
...
head repository: ljharb/qs
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 298bfa55d6db00ddea78dd0333509aadf9bb3077
Choose a head ref

Commits on Oct 13, 2017

  1. Change exports usage

    btd committed Oct 13, 2017
    Copy the full SHA
    6f0586f View commit details

Commits on Oct 14, 2017

  1. Copy the full SHA
    9dcec60 View commit details

Commits on Oct 15, 2017

  1. [Dev Deps] update eslint

    ljharb committed Oct 15, 2017
    Copy the full SHA
    037f368 View commit details

Commits on Dec 5, 2017

  1. [Fix] correctly parse nested arrays

    Fixes #212.
    Wes Roberts authored and ljharb committed Dec 5, 2017
    Copy the full SHA
    fafc2d2 View commit details

Commits on Mar 20, 2018

  1. Copy the full SHA
    73b3732 View commit details
  2. Copy the full SHA
    29477ba View commit details

Commits on May 2, 2018

  1. Copy the full SHA
    9a73e55 View commit details

Commits on May 3, 2018

  1. v6.5.2

    ljharb committed May 3, 2018
    Copy the full SHA
    eaabd05 View commit details

Commits on May 13, 2018

  1. Copy the full SHA
    1bfe04c View commit details
  2. [Tests] up to node v10.1, v9.11, v8.11, v6.14, v4.9; pin …

    …included builds to LTS
    ljharb committed May 13, 2018
    Copy the full SHA
    eee72e3 View commit details
  3. Copy the full SHA
    f85bce6 View commit details

Commits on Jul 26, 2018

  1. [refactor] stringify: Avoid arr = arr.concat(...), push to the exis…

    …ting instance (#269)
    papandreou authored and ljharb committed Jul 26, 2018
    Copy the full SHA
    55d217b View commit details

Commits on Sep 8, 2018

  1. Copy the full SHA
    b6956c9 View commit details

Commits on Sep 16, 2018

  1. [Fix] stringify: fix a crash with strictNullHandling and a custom…

    … `filter`/`serializeDate` (#279)
    Neaox authored and ljharb committed Sep 16, 2018
    Copy the full SHA
    c1c2a9d View commit details

Commits on Sep 17, 2018

  1. [Fix] utils: merge: fix crash when source is a truthy primitive…

    … & no options are provided
    ljharb committed Sep 17, 2018
    Copy the full SHA
    d1d1a97 View commit details

Commits on Jan 16, 2019

  1. Copy the full SHA
    107c302 View commit details

Commits on Jan 31, 2019

  1. Copy the full SHA
    ef27de4 View commit details

Commits on Feb 1, 2019

  1. Copy the full SHA
    49ad67f View commit details

Commits on Feb 3, 2019

  1. Copy the full SHA
    98c93d6 View commit details
  2. Copy the full SHA
    31bcb32 View commit details

Commits on Feb 14, 2019

  1. Copy the full SHA
    fd950b0 View commit details

Commits on Mar 29, 2019

  1. [Fix] fix for an impossible situation: when the formatter is called w…

    …ith a non-string value
    
    Note that all these tests passed already. Since the only time a
    formatter is called is in a context where it is concatenated with
    another string using `+`, this is a redundant step. However, for
    pedantic correctness and documentation, the contract for formatters is
    to always return a string.
    ljharb committed Mar 29, 2019
    Copy the full SHA
    45f6759 View commit details

Commits on Jun 26, 2019

  1. add FUNDING.yml

    This is an experiment; I intend to use 100% of funds to support the OSS community and my OSS projects' costs.
    ljharb committed Jun 26, 2019
    Copy the full SHA
    51b8a0b View commit details
  2. Copy the full SHA
    5639c20 View commit details

Commits on Mar 11, 2021

  1. [meta] fix README.md (#399)

    - `defaultEncoder`=> `defaultDecoder`
    mizozobu authored and ljharb committed Mar 11, 2021
    Copy the full SHA
    12ac1c4 View commit details

Commits on Sep 1, 2021

  1. Copy the full SHA
    1072d57 View commit details

Commits on Dec 27, 2021

  1. Copy the full SHA
    691e739 View commit details

Commits on Dec 28, 2021

  1. Copy the full SHA
    ed0f5dc View commit details

Commits on Jan 9, 2022

  1. Copy the full SHA
    0338716 View commit details

Commits on Jan 10, 2022

  1. [Dev Deps] backport from main

    ljharb committed Jan 10, 2022
    Copy the full SHA
    f814a7f View commit details
  2. v6.5.3

    ljharb committed Jan 10, 2022
    Copy the full SHA
    298bfa5 View commit details
Showing with 615 additions and 384 deletions.
  1. +14 −1 .editorconfig
  2. +0 −1 .eslintignore
  3. +22 −4 .eslintrc
  4. +12 −0 .github/FUNDING.yml
  5. +18 −0 .github/workflows/node-aught.yml
  6. +7 −0 .github/workflows/node-pretest.yml
  7. +18 −0 .github/workflows/node-tens.yml
  8. +15 −0 .github/workflows/rebase.yml
  9. +12 −0 .github/workflows/require-allow-edits.yml
  10. +3 −0 .gitignore
  11. +14 −4 .npmignore
  12. +13 −0 .nycrc
  13. +0 −167 .travis.yml
  14. +29 −0 CHANGELOG.md
  15. +0 −28 LICENSE
  16. +29 −0 LICENSE.md
  17. +54 −19 README.md
  18. +2 −2 component.json
  19. +117 −84 dist/qs.js
  20. +1 −1 lib/formats.js
  21. +6 −5 lib/parse.js
  22. +21 −14 lib/stringify.js
  23. +28 −15 lib/utils.js
  24. +19 −16 package.json
  25. +0 −15 test/.eslintrc
  26. +81 −5 test/parse.js
  27. +49 −3 test/stringify.js
  28. +31 −0 test/utils.js
15 changes: 14 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -7,11 +7,15 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 140
max_line_length = 160
quote_type = single

[test/*]
max_line_length = off

[LICENSE.md]
indent_size = off

[*.md]
max_line_length = off

@@ -28,3 +32,12 @@ indent_size = 2
[LICENSE]
indent_size = 2
max_line_length = off

[coverage/**/*]
indent_size = off
indent_style = off
indent = off
max_line_length = off

[.nycrc]
indent_style = tab
1 change: 0 additions & 1 deletion .eslintignore

This file was deleted.

26 changes: 22 additions & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -3,17 +3,35 @@

"extends": "@ljharb",

"ignorePatterns": [
"dist/",
],

"rules": {
"complexity": [2, 28],
"complexity": 0,
"consistent-return": 1,
"func-name-matching": 0,
"func-name-matching": 0,
"id-length": [2, { "min": 1, "max": 25, "properties": "never" }],
"indent": [2, 4],
"max-lines-per-function": 0,
"max-params": [2, 12],
"max-statements": [2, 45],
"multiline-comment-style": 0,
"no-continue": 1,
"no-magic-numbers": 0,
"no-param-reassign": 1,
"no-restricted-syntax": [2, "BreakStatement", "DebuggerStatement", "ForInStatement", "LabeledStatement", "WithStatement"],
"operator-linebreak": [2, "before"],
}
},

"overrides": [
{
"files": "test/**",
"rules": {
"max-lines-per-function": 0,
"max-statements": 0,
"no-extend-native": 0,
"function-paren-newline": 0,
},
},
],
}
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/qs
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with a single custom sponsorship URL
18 changes: 18 additions & 0 deletions .github/workflows/node-aught.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: 'Tests: node.js < 10'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/node.yml@main
with:
range: '< 10'
type: minors
command: npm run tests-only

node:
name: 'node < 10'
needs: [tests]
runs-on: ubuntu-latest
steps:
- run: 'echo tests completed'
7 changes: 7 additions & 0 deletions .github/workflows/node-pretest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: 'Tests: pretest/posttest'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/pretest.yml@main
18 changes: 18 additions & 0 deletions .github/workflows/node-tens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: 'Tests: node.js >= 10'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/node.yml@main
with:
range: '>= 10'
type: minors
command: npm run tests-only

node:
name: 'node >= 10'
needs: [tests]
runs-on: ubuntu-latest
steps:
- run: 'echo tests completed'
15 changes: 15 additions & 0 deletions .github/workflows/rebase.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Automatic Rebase

on: [pull_request]

jobs:
_:
name: "Automatic Rebase"

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: ljharb/rebase@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 changes: 12 additions & 0 deletions .github/workflows/require-allow-edits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Require “Allow Edits”

on: [pull_request_target]

jobs:
_:
name: "Require “Allow Edits”"

runs-on: ubuntu-latest

steps:
- uses: ljharb/require-allow-edits@main
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -10,3 +10,6 @@ dist/*
yarn.lock
package-lock.json
npm-shrinkwrap.json

.nyc_output/
coverage/
18 changes: 14 additions & 4 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
bower.json
component.json
.npmignore
.travis.yml
# gitignore
npm-debug.log
node_modules
.DS_Store

# Only apps should have lockfiles
yarn.lock
package-lock.json
npm-shrinkwrap.json

.nyc_output/
coverage/

.github/workflows
13 changes: 13 additions & 0 deletions .nycrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"all": true,
"check-coverage": false,
"reporter": ["text-summary", "text", "html", "json"],
"lines": 86,
"statements": 85.93,
"functions": 82.43,
"branches": 76.06,
"exclude": [
"coverage",
"dist"
]
}
167 changes: 0 additions & 167 deletions .travis.yml

This file was deleted.

29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
## **6.5.3**
- [Fix] `parse`: ignore `__proto__` keys (#428)
- [Fix]` `utils.merge`: avoid a crash with a null target and a truthy non-array source
- [Fix] correctly parse nested arrays
- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
- [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
- [Fix] `utils.merge`: avoid a crash with a null target and an array source
- [Refactor] `utils`: reduce observable [[Get]]s
- [Refactor] use cached `Array.isArray`
- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
- [Refactor] `parse`: only need to reassign the var once
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
- [readme] remove travis badge; add github actions/codecov badges; update URLs
- [Docs] Clean up license text so it’s properly detected as BSD-3-Clause
- [Docs] Clarify the need for "arrayLimit" option
- [meta] fix README.md (#399)
- [meta] add FUNDING.yml
- [actions] backport actions from main
- [Tests] always use `String(x)` over `x.toString()`
- [Tests] remove nonexistent tape option
- [Dev Deps] backport from main

## **6.5.2**
- [Fix] use `safer-buffer` instead of `Buffer` constructor
- [Refactor] utils: `module.exports` one thing, instead of mutating `exports` (#230)
- [Dev Deps] update `browserify`, `eslint`, `iconv-lite`, `safer-buffer`, `tape`, `browserify`

## **6.5.1**
- [Fix] Fix parsing & compacting very deep objects (#224)
- [Refactor] name utils functions
28 changes: 0 additions & 28 deletions LICENSE

This file was deleted.

29 changes: 29 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD 3-Clause License

Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
73 changes: 54 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# qs <sup>[![Version Badge][2]][1]</sup>

[![Build Status][3]][4]
[![dependency status][5]][6]
[![dev dependency status][7]][8]
[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![dependency status][deps-svg]][deps-url]
[![dev dependency status][dev-deps-svg]][dev-deps-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]

[![npm badge][11]][1]
[![npm badge][npm-badge-png]][package-url]

A querystring parsing and stringifying library with some added security.

@@ -182,7 +183,7 @@ assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
```

**qs** will also limit specifying indices in an array to a maximum index of `20`. Any array members with an index of greater than `20` will
instead be converted to an object with the index as the key:
instead be converted to an object with the index as the key. This is needed to handle cases when someone sent, for example, `a[999999999]` and it will take significant time to iterate over this huge array.

```javascript
var withMaxIndex = qs.parse('a[100]=b');
@@ -267,6 +268,30 @@ var decoded = qs.parse('x=z', { decoder: function (str) {
}})
```

You can encode keys and values using different logic by using the type argument provided to the encoder:

```javascript
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) {
if (type === 'key') {
return // Encoded key
} else if (type === 'value') {
return // Encoded value
}
}})
```

The type argument is also provided to the decoder:

```javascript
var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) {
if (type === 'key') {
return // Decoded key
} else if (type === 'value') {
return // Decoded value
}
}})
```

Examples beyond this point will be shown as though the output is not URI encoded for clarity. Please note that the return values in these cases *will* be URI encoded during real usage.

When arrays are stringified, by default they are given explicit indices:
@@ -458,18 +483,28 @@ assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC3986' }), 'a=b%20c');
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC1738' }), 'a=b+c');
```

[1]: https://npmjs.org/package/qs
[2]: http://versionbadg.es/ljharb/qs.svg
[3]: https://api.travis-ci.org/ljharb/qs.svg
[4]: https://travis-ci.org/ljharb/qs
[5]: https://david-dm.org/ljharb/qs.svg
[6]: https://david-dm.org/ljharb/qs
[7]: https://david-dm.org/ljharb/qs/dev-status.svg
[8]: https://david-dm.org/ljharb/qs?type=dev
[9]: https://ci.testling.com/ljharb/qs.png
[10]: https://ci.testling.com/ljharb/qs
[11]: https://nodei.co/npm/qs.png?downloads=true&stars=true
[license-image]: http://img.shields.io/npm/l/qs.svg
## Security

Please email [@ljharb](https://github.com/ljharb) or see https://tidelift.com/security if you have a potential security vulnerability to report.

## qs for enterprise

Available as part of the Tidelift Subscription

The maintainers of qs and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-qs?utm_source=npm-qs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

[package-url]: https://npmjs.org/package/qs
[npm-version-svg]: https://versionbadg.es/ljharb/qs.svg
[deps-svg]: https://david-dm.org/ljharb/qs.svg
[deps-url]: https://david-dm.org/ljharb/qs
[dev-deps-svg]: https://david-dm.org/ljharb/qs/dev-status.svg
[dev-deps-url]: https://david-dm.org/ljharb/qs#info=devDependencies
[npm-badge-png]: https://nodei.co/npm/qs.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/qs.svg
[license-url]: LICENSE
[downloads-image]: http://img.shields.io/npm/dm/qs.svg
[downloads-url]: http://npm-stat.com/charts.html?package=qs
[downloads-image]: https://img.shields.io/npm/dm/qs.svg
[downloads-url]: https://npm-stat.com/charts.html?package=qs
[codecov-image]: https://codecov.io/gh/ljharb/qs/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/ljharb/qs/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/qs
[actions-url]: https://github.com/ljharb/qs/actions
4 changes: 2 additions & 2 deletions component.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "qs",
"repository": "hapijs/qs",
"repository": "ljharb/qs",
"description": "query-string parser / stringifier with nesting support",
"version": "6.5.0",
"version": "6.5.3",
"keywords": ["querystring", "query", "parser"],
"main": "lib/index.js",
"scripts": [
201 changes: 117 additions & 84 deletions dist/qs.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/formats.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ module.exports = {
return replace.call(value, percentTwenties, '+');
},
RFC3986: function (value) {
return value;
return String(value);
}
},
RFC1738: 'RFC1738',
11 changes: 6 additions & 5 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -53,14 +53,15 @@ var parseObject = function (chain, val, options) {
var obj;
var root = chain[i];

if (root === '[]') {
obj = [];
obj = obj.concat(leaf);
if (root === '[]' && options.parseArrays) {
obj = [].concat(leaf);
} else {
obj = options.plainObjects ? Object.create(null) : {};
var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
var index = parseInt(cleanRoot, 10);
if (
if (!options.parseArrays && cleanRoot === '') {
obj = { 0: leaf };
} else if (
!isNaN(index)
&& root !== cleanRoot
&& String(index) === cleanRoot
@@ -69,7 +70,7 @@ var parseObject = function (chain, val, options) {
) {
obj = [];
obj[index] = leaf;
} else {
} else if (cleanRoot !== '__proto__') {
obj[cleanRoot] = leaf;
}
}
35 changes: 21 additions & 14 deletions lib/stringify.js
Original file line number Diff line number Diff line change
@@ -4,32 +4,38 @@ var utils = require('./utils');
var formats = require('./formats');

var arrayPrefixGenerators = {
brackets: function brackets(prefix) { // eslint-disable-line func-name-matching
brackets: function brackets(prefix) {
return prefix + '[]';
},
indices: function indices(prefix, key) { // eslint-disable-line func-name-matching
indices: function indices(prefix, key) {
return prefix + '[' + key + ']';
},
repeat: function repeat(prefix) { // eslint-disable-line func-name-matching
repeat: function repeat(prefix) {
return prefix;
}
};

var isArray = Array.isArray;
var push = Array.prototype.push;
var pushToArray = function (arr, valueOrArray) {
push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
};

var toISO = Date.prototype.toISOString;

var defaults = {
delimiter: '&',
encode: true,
encoder: utils.encode,
encodeValuesOnly: false,
serializeDate: function serializeDate(date) { // eslint-disable-line func-name-matching
serializeDate: function serializeDate(date) {
return toISO.call(date);
},
skipNulls: false,
strictNullHandling: false
};

var stringify = function stringify( // eslint-disable-line func-name-matching
var stringify = function stringify(
object,
prefix,
generateArrayPrefix,
@@ -48,7 +54,9 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
obj = filter(prefix, obj);
} else if (obj instanceof Date) {
obj = serializeDate(obj);
} else if (obj === null) {
}

if (obj === null) {
if (strictNullHandling) {
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix;
}
@@ -71,7 +79,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
}

var objKeys;
if (Array.isArray(filter)) {
if (isArray(filter)) {
objKeys = filter;
} else {
var keys = Object.keys(obj);
@@ -85,8 +93,8 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
continue;
}

if (Array.isArray(obj)) {
values = values.concat(stringify(
if (isArray(obj)) {
pushToArray(values, stringify(
obj[key],
generateArrayPrefix(prefix, key),
generateArrayPrefix,
@@ -101,7 +109,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
encodeValuesOnly
));
} else {
values = values.concat(stringify(
pushToArray(values, stringify(
obj[key],
prefix + (allowDots ? '.' + key : '[' + key + ']'),
generateArrayPrefix,
@@ -125,7 +133,7 @@ module.exports = function (object, opts) {
var obj = object;
var options = opts ? utils.assign({}, opts) : {};

if (options.encoder !== null && options.encoder !== undefined && typeof options.encoder !== 'function') {
if (options.encoder !== null && typeof options.encoder !== 'undefined' && typeof options.encoder !== 'function') {
throw new TypeError('Encoder has to be a function.');
}

@@ -150,7 +158,7 @@ module.exports = function (object, opts) {
if (typeof options.filter === 'function') {
filter = options.filter;
obj = filter('', obj);
} else if (Array.isArray(options.filter)) {
} else if (isArray(options.filter)) {
filter = options.filter;
objKeys = filter;
}
@@ -186,8 +194,7 @@ module.exports = function (object, opts) {
if (skipNulls && obj[key] === null) {
continue;
}

keys = keys.concat(stringify(
pushToArray(keys, stringify(
obj[key],
key,
generateArrayPrefix,
43 changes: 28 additions & 15 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ var compactQueue = function compactQueue(queue) {
return obj;
};

exports.arrayToObject = function arrayToObject(source, options) {
var arrayToObject = function arrayToObject(source, options) {
var obj = options && options.plainObjects ? Object.create(null) : {};
for (var i = 0; i < source.length; ++i) {
if (typeof source[i] !== 'undefined') {
@@ -45,16 +45,16 @@ exports.arrayToObject = function arrayToObject(source, options) {
return obj;
};

exports.merge = function merge(target, source, options) {
var merge = function merge(target, source, options) {
if (!source) {
return target;
}

if (typeof source !== 'object') {
if (Array.isArray(target)) {
target.push(source);
} else if (typeof target === 'object') {
if (options.plainObjects || options.allowPrototypes || !has.call(Object.prototype, source)) {
} else if (target && typeof target === 'object') {
if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) {
target[source] = true;
}
} else {
@@ -64,20 +64,21 @@ exports.merge = function merge(target, source, options) {
return target;
}

if (typeof target !== 'object') {
if (!target || typeof target !== 'object') {
return [target].concat(source);
}

var mergeTarget = target;
if (Array.isArray(target) && !Array.isArray(source)) {
mergeTarget = exports.arrayToObject(target, options);
mergeTarget = arrayToObject(target, options);
}

if (Array.isArray(target) && Array.isArray(source)) {
source.forEach(function (item, i) {
if (has.call(target, i)) {
if (target[i] && typeof target[i] === 'object') {
target[i] = exports.merge(target[i], item, options);
var targetItem = target[i];
if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
target[i] = merge(targetItem, item, options);
} else {
target.push(item);
}
@@ -92,30 +93,30 @@ exports.merge = function merge(target, source, options) {
var value = source[key];

if (has.call(acc, key)) {
acc[key] = exports.merge(acc[key], value, options);
acc[key] = merge(acc[key], value, options);
} else {
acc[key] = value;
}
return acc;
}, mergeTarget);
};

exports.assign = function assignSingleSource(target, source) {
var assign = function assignSingleSource(target, source) {
return Object.keys(source).reduce(function (acc, key) {
acc[key] = source[key];
return acc;
}, target);
};

exports.decode = function (str) {
var decode = function (str) {
try {
return decodeURIComponent(str.replace(/\+/g, ' '));
} catch (e) {
return str;
}
};

exports.encode = function encode(str) {
var encode = function encode(str) {
// This code was originally written by Brian White (mscdex) for the io.js core querystring library.
// It has been adapted here for stricter adherence to RFC 3986
if (str.length === 0) {
@@ -158,6 +159,7 @@ exports.encode = function encode(str) {

i += 1;
c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF));
/* eslint operator-linebreak: [2, "before"] */
out += hexTable[0xF0 | (c >> 18)]
+ hexTable[0x80 | ((c >> 12) & 0x3F)]
+ hexTable[0x80 | ((c >> 6) & 0x3F)]
@@ -167,7 +169,7 @@ exports.encode = function encode(str) {
return out;
};

exports.compact = function compact(value) {
var compact = function compact(value) {
var queue = [{ obj: { o: value }, prop: 'o' }];
var refs = [];

@@ -189,14 +191,25 @@ exports.compact = function compact(value) {
return compactQueue(queue);
};

exports.isRegExp = function isRegExp(obj) {
var isRegExp = function isRegExp(obj) {
return Object.prototype.toString.call(obj) === '[object RegExp]';
};

exports.isBuffer = function isBuffer(obj) {
var isBuffer = function isBuffer(obj) {
if (obj === null || typeof obj === 'undefined') {
return false;
}

return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
};

module.exports = {
arrayToObject: arrayToObject,
assign: assign,
compact: compact,
decode: decode,
encode: encode,
isBuffer: isBuffer,
isRegExp: isRegExp,
merge: merge
};
35 changes: 19 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -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.5.1",
"version": "6.5.3",
"repository": {
"type": "git",
"url": "https://github.com/ljharb/qs.git"
@@ -22,29 +22,32 @@
"engines": {
"node": ">=0.6"
},
"dependencies": {},
"devDependencies": {
"@ljharb/eslint-config": "^12.2.1",
"browserify": "^14.4.0",
"covert": "^1.1.0",
"editorconfig-tools": "^0.1.1",
"eslint": "^4.6.1",
"@ljharb/eslint-config": "^20.1.0",
"aud": "^1.1.5",
"browserify": "^16.5.2",
"eclint": "^2.8.1",
"eslint": "^8.6.0",
"evalmd": "^0.0.17",
"iconv-lite": "^0.4.18",
"iconv-lite": "^0.4.24",
"in-publish": "^2.0.1",
"mkdirp": "^0.5.1",
"nyc": "^10.3.2",
"qs-iconv": "^1.0.4",
"safe-publish-latest": "^1.1.1",
"tape": "^4.8.0"
"safe-publish-latest": "^2.0.0",
"safer-buffer": "^2.1.2",
"tape": "^5.4.0"
},
"scripts": {
"prepublish": "safe-publish-latest && npm run dist",
"prepublishOnly": "safe-publish-latest && npm run dist",
"prepublish": "not-in-publish || npm run prepublishOnly",
"pretest": "npm run --silent readme && npm run --silent lint",
"test": "npm run --silent coverage",
"tests-only": "node test",
"test": "npm run --silent tests-only",
"tests-only": "nyc tape 'test/**/*.js'",
"posttest": "aud --production",
"readme": "evalmd README.md",
"prelint": "editorconfig-tools check * lib/* test/*",
"lint": "eslint lib/*.js test/*.js",
"coverage": "covert test",
"postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git')",
"lint": "eslint --ext=js,mjs .",
"dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js"
},
"license": "BSD-3-Clause"
15 changes: 0 additions & 15 deletions test/.eslintrc

This file was deleted.

86 changes: 81 additions & 5 deletions test/parse.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ var test = require('tape');
var qs = require('../');
var utils = require('../lib/utils');
var iconv = require('iconv-lite');
var SaferBuffer = require('safer-buffer').Buffer;

test('parse()', function (t) {
t.test('parses a simple string', function (st) {
@@ -231,11 +232,19 @@ test('parse()', function (t) {
});

t.test('parses buffers correctly', function (st) {
var b = new Buffer('test');
var b = SaferBuffer.from('test');
st.deepEqual(qs.parse({ a: b }), { a: b });
st.end();
});

t.test('parses jquery-param strings', function (st) {
// readable = 'filter[0][]=int1&filter[0][]==&filter[0][]=77&filter[]=and&filter[2][]=int2&filter[2][]==&filter[2][]=8'
var encoded = 'filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8';
var expected = { filter: [['int1', '=', '77'], 'and', ['int2', '=', '8']] };
st.deepEqual(qs.parse(encoded), expected);
st.end();
});

t.test('continues parsing when no parent is found', function (st) {
st.deepEqual(qs.parse('[]=&a=b'), { 0: '', a: 'b' });
st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { 0: null, a: 'b' });
@@ -256,7 +265,7 @@ test('parse()', function (t) {
st.end();
});

t.test('should not throw when a native prototype has an enumerable property', { parallel: false }, function (st) {
t.test('should not throw when a native prototype has an enumerable property', function (st) {
Object.prototype.crash = '';
Array.prototype.crash = '';
st.doesNotThrow(qs.parse.bind(null, 'a=b'));
@@ -301,7 +310,14 @@ test('parse()', function (t) {
});

t.test('allows disabling array parsing', function (st) {
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { parseArrays: false }), { a: { 0: 'b', 1: 'c' } });
var indices = qs.parse('a[0]=b&a[1]=c', { parseArrays: false });
st.deepEqual(indices, { a: { 0: 'b', 1: 'c' } });
st.equal(Array.isArray(indices.a), false, 'parseArrays:false, indices case is not an array');

var emptyBrackets = qs.parse('a[]=b', { parseArrays: false });
st.deepEqual(emptyBrackets, { a: { 0: 'b' } });
st.equal(Array.isArray(emptyBrackets.a), false, 'parseArrays:false, empty brackets case is not an array');

st.end();
});

@@ -507,13 +523,73 @@ test('parse()', function (t) {

st.deepEqual(
qs.parse('a[b]=c&a=toString', { plainObjects: true }),
{ a: { b: 'c', toString: true } },
{ __proto__: null, a: { __proto__: null, b: 'c', toString: true } },
'can overwrite prototype with plainObjects true'
);

st.end();
});

t.test('dunder proto is ignored', function (st) {
var payload = 'categories[__proto__]=login&categories[__proto__]&categories[length]=42';
var result = qs.parse(payload, { allowPrototypes: true });

st.deepEqual(
result,
{
categories: {
length: '42'
}
},
'silent [[Prototype]] payload'
);

var plainResult = qs.parse(payload, { allowPrototypes: true, plainObjects: true });

st.deepEqual(
plainResult,
{
__proto__: null,
categories: {
__proto__: null,
length: '42'
}
},
'silent [[Prototype]] payload: plain objects'
);

var query = qs.parse('categories[__proto__]=cats&categories[__proto__]=dogs&categories[some][json]=toInject', { allowPrototypes: true });

st.notOk(Array.isArray(query.categories), 'is not an array');
st.notOk(query.categories instanceof Array, 'is not instanceof an array');
st.deepEqual(query.categories, { some: { json: 'toInject' } });
st.equal(JSON.stringify(query.categories), '{"some":{"json":"toInject"}}', 'stringifies as a non-array');

st.deepEqual(
qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true }),
{
foo: {
bar: 'stuffs'
}
},
'hidden values'
);

st.deepEqual(
qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true, plainObjects: true }),
{
__proto__: null,
foo: {
__proto__: null,
bar: 'stuffs'
}
},
'hidden values: plain objects'
);

st.end();
});

t.test('can return null objects', { skip: !Object.create }, function (st) {
var expected = Object.create(null);
expected.a = Object.create(null);
@@ -539,7 +615,7 @@ test('parse()', function (t) {
result.push(parseInt(parts[1], 16));
parts = reg.exec(str);
}
return iconv.decode(new Buffer(result), 'shift_jis').toString();
return String(iconv.decode(SaferBuffer.from(result), 'shift_jis'));
}
}), { : '大阪府' });
st.end();
52 changes: 49 additions & 3 deletions test/stringify.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ var test = require('tape');
var qs = require('../');
var utils = require('../lib/utils');
var iconv = require('iconv-lite');
var SaferBuffer = require('safer-buffer').Buffer;

test('stringify()', function (t) {
t.test('stringifies a querystring object', function (st) {
@@ -18,6 +19,15 @@ test('stringify()', function (t) {
st.end();
});

t.test('stringifies falsy values', function (st) {
st.equal(qs.stringify(undefined), '');
st.equal(qs.stringify(null), '');
st.equal(qs.stringify(null, { strictNullHandling: true }), '');
st.equal(qs.stringify(false), '');
st.equal(qs.stringify(0), '');
st.end();
});

t.test('adds query prefix', function (st) {
st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
st.end();
@@ -28,6 +38,13 @@ test('stringify()', function (t) {
st.end();
});

t.test('stringifies nested falsy values', function (st) {
st.equal(qs.stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D=');
st.equal(qs.stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), 'a%5Bb%5D%5Bc%5D');
st.equal(qs.stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false');
st.end();
});

t.test('stringifies a nested object', function (st) {
st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
@@ -336,8 +353,8 @@ test('stringify()', function (t) {
});

t.test('stringifies buffer values', function (st) {
st.equal(qs.stringify({ a: new Buffer('test') }), 'a=test');
st.equal(qs.stringify({ a: { b: new Buffer('test') } }), 'a%5Bb%5D=test');
st.equal(qs.stringify({ a: SaferBuffer.from('test') }), 'a=test');
st.equal(qs.stringify({ a: { b: SaferBuffer.from('test') } }), 'a%5Bb%5D=test');
st.end();
});

@@ -481,14 +498,20 @@ test('stringify()', function (t) {
});

t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
st.equal(qs.stringify({ a: new Buffer([1]) }, {
st.equal(qs.stringify({ a: SaferBuffer.from([1]) }, {
encoder: function (buffer) {
if (typeof buffer === 'string') {
return buffer;
}
return String.fromCharCode(buffer.readUInt8(0) + 97);
}
}), 'a=b');

st.equal(qs.stringify({ a: SaferBuffer.from('a b') }, {
encoder: function (buffer) {
return buffer;
}
}), 'a=a b');
st.end();
});

@@ -529,17 +552,20 @@ test('stringify()', function (t) {
t.test('RFC 1738 spaces serialization', function (st) {
st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC1738 }), 'a=b+c');
st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC1738 }), 'a+b=c+d');
st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC1738 }), 'a+b=a+b');
st.end();
});

t.test('RFC 3986 spaces serialization', function (st) {
st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC3986 }), 'a=b%20c');
st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC3986 }), 'a%20b=c%20d');
st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC3986 }), 'a%20b=a%20b');
st.end();
});

t.test('Backward compatibility to RFC 3986', function (st) {
st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }), 'a%20b=a%20b');
st.end();
});

@@ -592,5 +618,25 @@ test('stringify()', function (t) {
st.end();
});

t.test('strictNullHandling works with custom filter', function (st) {
var filter = function (prefix, value) {
return value;
};

var options = { strictNullHandling: true, filter: filter };
st.equal(qs.stringify({ key: null }, options), 'key');
st.end();
});

t.test('strictNullHandling works with null serializeDate', function (st) {
var serializeDate = function () {
return null;
};
var options = { strictNullHandling: true, serializeDate: serializeDate };
var date = new Date();
st.equal(qs.stringify({ key: date }, options), 'key');
st.end();
});

t.end();
});
31 changes: 31 additions & 0 deletions test/utils.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@ var test = require('tape');
var utils = require('../lib/utils');

test('merge()', function (t) {
t.deepEqual(utils.merge(null, true), [null, true], 'merges true into null');

t.deepEqual(utils.merge(null, [42]), [null, 42], 'merges null into an array');

t.deepEqual(utils.merge({ a: 'b' }, { a: 'c' }), { a: ['b', 'c'] }, 'merges two objects with the same key');

var oneMerged = utils.merge({ foo: 'bar' }, { foo: { first: '123' } });
@@ -18,6 +22,33 @@ test('merge()', function (t) {
var nestedArrays = utils.merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] });
t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] });

var noOptionsNonObjectSource = utils.merge({ foo: 'baz' }, 'bar');
t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });

t.test(
'avoids invoking array setters unnecessarily',
{ skip: typeof Object.defineProperty !== 'function' },
function (st) {
var setCount = 0;
var getCount = 0;
var observed = [];
Object.defineProperty(observed, 0, {
get: function () {
getCount += 1;
return { bar: 'baz' };
},
set: function () { setCount += 1; }
});
utils.merge(observed, [null]);
st.equal(setCount, 0);
st.equal(getCount, 1);
observed[0] = observed[0]; // eslint-disable-line no-self-assign
st.equal(setCount, 1);
st.equal(getCount, 2);
st.end();
}
);

t.end();
});