docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 180
|
||||
quote_type = single
|
||||
|
||||
[test/*]
|
||||
max_line_length = off
|
||||
|
||||
[LICENSE.md]
|
||||
indent_size = off
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
|
||||
[*.json]
|
||||
max_line_length = off
|
||||
|
||||
[Makefile]
|
||||
max_line_length = off
|
||||
|
||||
[CHANGELOG.md]
|
||||
indent_style = space
|
||||
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
|
||||
|
||||
[tea.yaml]
|
||||
indent_size = 2
|
||||
+12
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Security
|
||||
|
||||
Please file a private vulnerability report via GitHub, email [@ljharb](https://github.com/ljharb), or see https://tidelift.com/security if you have a potential security vulnerability to report.
|
||||
|
||||
## Incident Response Plan
|
||||
|
||||
Please see our [Incident Response Plan](https://github.com/ljharb/.github/blob/main/INCIDENT_RESPONSE_PLAN.md).
|
||||
|
||||
## Threat Model
|
||||
|
||||
Please see [THREAT_MODEL.md](./THREAT_MODEL.md).
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
## Threat Model for qs (querystring parsing library)
|
||||
|
||||
### 1. Library Overview
|
||||
|
||||
- **Library Name:** qs
|
||||
- **Brief Description:** A JavaScript library for parsing and stringifying URL query strings, supporting nested objects and arrays. It is widely used in Node.js and web applications for processing query parameters[2][6][8].
|
||||
- **Key Public APIs/Functions:** `qs.parse()`, `qs.stringify()`
|
||||
|
||||
### 2. Define Scope
|
||||
|
||||
This threat model focuses on the core parsing and stringifying functionality, specifically the handling of nested objects and arrays, option validation, and cycle management in stringification.
|
||||
|
||||
### 3. Conceptual System Diagram
|
||||
|
||||
```
|
||||
Caller Application → qs.parse(input, options) → Parsing Engine → Output Object
|
||||
│
|
||||
└→ Options Handling
|
||||
|
||||
Caller Application → qs.stringify(obj, options) → Stringifying Engine → Output String
|
||||
│
|
||||
└→ Options Handling
|
||||
└→ Cycle Tracking
|
||||
```
|
||||
|
||||
**Trust Boundaries:**
|
||||
- **Input string (parse):** May come from untrusted sources (e.g., user input, network requests)
|
||||
- **Input object (stringify):** May contain cycles, which can lead to infinite loops during stringification
|
||||
- **Options:** Provided by the caller
|
||||
- **Cycle Tracking:** Used only during stringification to detect and handle circular references
|
||||
|
||||
### 4. Identify Assets
|
||||
|
||||
- **Integrity of parsed output:** Prevent malicious manipulation of the output object structure, especially ensuring builtins/globals are not modified as a result of parse[3][4][8].
|
||||
- **Confidentiality of processed data:** Avoid leaking sensitive information through errors or output.
|
||||
- **Availability/performance for host application:** Prevent crashes or resource exhaustion in the consuming application.
|
||||
- **Security of host application:** Prevent the library from being a vector for attacks (e.g., prototype pollution, DoS).
|
||||
- **Reputation of library:** Maintain trust by avoiding supply chain attacks and vulnerabilities[1].
|
||||
|
||||
### 5. Identify Threats
|
||||
|
||||
| Component / API / Interaction | S | T | R | I | D | E |
|
||||
|---------------------------------------|----|----|----|----|----|----|
|
||||
| Public API Call (`parse`) | – | ✓ | – | ✓ | ✓ | ✓ |
|
||||
| Public API Call (`stringify`) | – | ✓ | – | ✓ | ✓ | – |
|
||||
| Options Handling | ✓ | ✓ | – | ✓ | – | ✓ |
|
||||
| Dependency Interaction | – | – | – | – | ✓ | – |
|
||||
|
||||
**Key Threats:**
|
||||
- **Tampering:** Malicious input can, if not prevented, alter parsed output (e.g., prototype pollution via `__proto__`, modification of builtins/globals)[3][4][8].
|
||||
- **Information Disclosure:** Error messages may expose internal details or sensitive data.
|
||||
- **Denial of Service:** Large or malformed input can exhaust memory or CPU.
|
||||
- **Elevation of Privilege:** Prototype pollution can lead to unintended privilege escalation in the host application[3][4][8].
|
||||
|
||||
### 6. Mitigation/Countermeasures
|
||||
|
||||
| Threat Identified | Proposed Mitigation |
|
||||
|---------------------------------------------------|---------------------|
|
||||
| Tampering (malicious input, prototype pollution) | Strict input validation; keep `allowPrototypes: false` by default; use `plainObjects` for output; ensure builtins/globals are never modified by parse[4][8]. |
|
||||
| Information Disclosure (error messages) | Generic error messages without stack traces or internal paths. |
|
||||
| Denial of Service (memory/CPU exhaustion) | Enforce `arrayLimit` and `parameterLimit` with safe defaults; enable `throwOnLimitExceeded`; limit nesting depth[7]. |
|
||||
| Elevation of Privilege (prototype pollution) | Keep `allowPrototypes: false`; validate options against allowlist; use `plainObjects` to avoid prototype pollution[4][8]. |
|
||||
|
||||
### 7. Risk Ranking
|
||||
|
||||
- **High:** Denial of Service via array parsing or malformed input (historical vulnerability)
|
||||
- **Medium:** Prototype pollution via options or input (if `allowPrototypes` enabled)
|
||||
- **Low:** Information disclosure in errors
|
||||
|
||||
### 8. Next Steps & Review
|
||||
|
||||
1. **Audit option validation logic.**
|
||||
2. **Add depth limiting to nested parsing and stringification.**
|
||||
3. **Implement fuzz testing for parser and stringifier edge cases.**
|
||||
4. **Regularly review dependencies for vulnerabilities.**
|
||||
5. **Keep documentation and threat model up to date.**
|
||||
6. **Ensure builtins/globals are never modified as a result of parse.**
|
||||
7. **Support round-trip consistency between parse and stringify as a non-security goal, with the right options[5][9].**
|
||||
+13
@@ -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"
|
||||
]
|
||||
}
|
||||
+833
@@ -0,0 +1,833 @@
|
||||
## **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)
|
||||
|
||||
## **6.14.2**
|
||||
- [Fix] `parse`: mark overflow objects for indexed notation exceeding `arrayLimit` (#546)
|
||||
- [Fix] `arrayLimit` means max count, not max index, in `combine`/`merge`/`parseArrayValue`
|
||||
- [Fix] `parse`: throw on `arrayLimit` exceeded with indexed notation when `throwOnLimitExceeded` is true (#529)
|
||||
- [Fix] `parse`: enforce `arrayLimit` on `comma`-parsed values
|
||||
- [Fix] `parse`: fix error message to reflect arrayLimit as max index; remove extraneous comments (#545)
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [meta] fix changelog typo (`arrayLength` → `arrayLimit`)
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.14.1**
|
||||
- [Fix] ensure `arrayLimit` applies to `[]` notation as well
|
||||
- [Fix] `parse`: when a custom decoder returns `null` for a key, ignore that key
|
||||
- [Refactor] `parse`: extract key segment splitting helper
|
||||
- [meta] add threat model
|
||||
- [actions] add workflow permissions
|
||||
- [Tests] `stringify`: increase coverage
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `npmignore`, `es-value-fixtures`, `for-each`, `object-inspect`
|
||||
|
||||
## **6.14.0**
|
||||
- [New] `parse`: add `throwOnParameterLimitExceeded` option (#517)
|
||||
- [Refactor] `parse`: use `utils.combine` more
|
||||
- [patch] `parse`: add explicit `throwOnLimitExceeded` default
|
||||
- [actions] use shared action; re-add finishers
|
||||
- [meta] Fix changelog formatting bug
|
||||
- [Deps] update `side-channel`
|
||||
- [Dev Deps] update `es-value-fixtures`, `has-bigints`, `has-proto`, `has-symbols`
|
||||
- [Tests] increase coverage
|
||||
|
||||
## **6.13.3**
|
||||
[Fix] fix regressions from robustness refactor
|
||||
[actions] update reusable workflows
|
||||
|
||||
## **6.13.2**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.13.1**
|
||||
- [Fix] `stringify`: avoid a crash when a `filter` key is `null`
|
||||
- [Fix] `utils.merge`: functions should not be stringified into keys
|
||||
- [Fix] `parse`: avoid a crash with interpretNumericEntities: true, comma: true, and iso charset
|
||||
- [Fix] `stringify`: ensure a non-string `filter` does not crash
|
||||
- [Refactor] use `__proto__` syntax instead of `Object.create` for null objects
|
||||
- [Refactor] misc cleanup
|
||||
- [Tests] `utils.merge`: add some coverage
|
||||
- [Tests] fix a test case
|
||||
- [actions] split out node 10-20, and 20+
|
||||
- [Dev Deps] update `es-value-fixtures`, `mock-property`, `object-inspect`, `tape`
|
||||
|
||||
## **6.13.0**
|
||||
- [New] `parse`: add `strictDepth` option (#511)
|
||||
- [Tests] use `npm audit` instead of `aud`
|
||||
|
||||
## **6.12.5**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.12.4**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.12.3**
|
||||
- [Fix] `parse`: properly account for `strictNullHandling` when `allowEmptyArrays`
|
||||
- [meta] fix changelog indentation
|
||||
|
||||
## **6.12.2**
|
||||
- [Fix] `parse`: parse encoded square brackets (#506)
|
||||
- [readme] add CII best practices badge
|
||||
|
||||
## **6.12.1**
|
||||
- [Fix] `parse`: Disable `decodeDotInKeys` by default to restore previous behavior (#501)
|
||||
- [Performance] `utils`: Optimize performance under large data volumes, reduce memory usage, and speed up processing (#502)
|
||||
- [Refactor] `utils`: use `+=`
|
||||
- [Tests] increase coverage
|
||||
|
||||
## **6.12.0**
|
||||
|
||||
- [New] `parse`/`stringify`: add `decodeDotInKeys`/`encodeDotKeys` options (#488)
|
||||
- [New] `parse`: add `duplicates` option
|
||||
- [New] `parse`/`stringify`: add `allowEmptyArrays` option to allow [] in object values (#487)
|
||||
- [Refactor] `parse`/`stringify`: move allowDots config logic to its own variable
|
||||
- [Refactor] `stringify`: move option-handling code into `normalizeStringifyOptions`
|
||||
- [readme] update readme, add logos (#484)
|
||||
- [readme] `stringify`: clarify default `arrayFormat` behavior
|
||||
- [readme] fix line wrapping
|
||||
- [readme] remove dead badges
|
||||
- [Deps] update `side-channel`
|
||||
- [meta] make the dist build 50% smaller
|
||||
- [meta] add `sideEffects` flag
|
||||
- [meta] run build in prepack, not prepublish
|
||||
- [Tests] `parse`: remove useless tests; add coverage
|
||||
- [Tests] `stringify`: increase coverage
|
||||
- [Tests] use `mock-property`
|
||||
- [Tests] `stringify`: improve coverage
|
||||
- [Dev Deps] update `@ljharb/eslint-config `, `aud`, `has-override-mistake`, `has-property-descriptors`, `mock-property`, `npmignore`, `object-inspect`, `tape`
|
||||
- [Dev Deps] pin `glob`, since v10.3.8+ requires a broken `jackspeak`
|
||||
- [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6
|
||||
|
||||
## **6.11.4**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.11.3**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.11.2**
|
||||
- [Fix] `parse`: Fix parsing when the global Object prototype is frozen (#473)
|
||||
- [Tests] add passing test cases with empty keys (#473)
|
||||
|
||||
## **6.11.1**
|
||||
- [Fix] `stringify`: encode comma values more consistently (#463)
|
||||
- [readme] add usage of `filter` option for injecting custom serialization, i.e. of custom types (#447)
|
||||
- [meta] remove extraneous code backticks (#457)
|
||||
- [meta] fix changelog markdown
|
||||
- [actions] update checkout action
|
||||
- [actions] restrict action permissions
|
||||
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `object-inspect`, `tape`
|
||||
|
||||
## **6.11.0**
|
||||
- [New] [Fix] `stringify`: revert 0e903c0; add `commaRoundTrip` option (#442)
|
||||
- [readme] fix version badge
|
||||
|
||||
## **6.10.7**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.10.6**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.10.5**
|
||||
- [Fix] `stringify`: with `arrayFormat: comma`, properly include an explicit `[]` on a single-item array (#434)
|
||||
|
||||
## **6.10.4**
|
||||
- [Fix] `stringify`: with `arrayFormat: comma`, include an explicit `[]` on a single-item array (#441)
|
||||
- [meta] use `npmignore` to autogenerate an npmignore file
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `has-symbol`, `object-inspect`, `tape`
|
||||
|
||||
## **6.10.3**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [actions] reuse common workflows
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `object-inspect`, `tape`
|
||||
|
||||
## **6.10.2**
|
||||
- [Fix] `stringify`: actually fix cyclic references (#426)
|
||||
- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Docs] add note and links for coercing primitive values (#408)
|
||||
- [actions] update codecov uploader
|
||||
- [actions] update workflows
|
||||
- [Tests] clean up stringify tests slightly
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `object-inspect`, `safe-publish-latest`, `tape`
|
||||
|
||||
## **6.10.1**
|
||||
- [Fix] `stringify`: avoid exception on repeated object values (#402)
|
||||
|
||||
## **6.10.0**
|
||||
- [New] `stringify`: throw on cycles, instead of an infinite loop (#395, #394, #393)
|
||||
- [New] `parse`: add `allowSparse` option for collapsing arrays with missing indices (#312)
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] only run `npm run dist` in publish, not install
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `has-symbols`, `tape`
|
||||
- [Tests] fix tests on node v0.6
|
||||
- [Tests] use `ljharb/actions/node/install` instead of `ljharb/actions/node/run`
|
||||
- [Tests] Revert "[meta] ignore eclint transitive audit warning"
|
||||
|
||||
## **6.9.9**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.9.8**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.9.7**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Docs] add note and links for coercing primitive values (#408)
|
||||
- [Tests] clean up stringify tests slightly
|
||||
- [meta] fix README.md (#399)
|
||||
- Revert "[meta] ignore eclint transitive audit warning"
|
||||
- [actions] backport actions from main
|
||||
- [Dev Deps] backport updates from main
|
||||
|
||||
## **6.9.6**
|
||||
- [Fix] restore `dist` dir; mistakenly removed in d4f6c32
|
||||
|
||||
## **6.9.5**
|
||||
- [Fix] `stringify`: do not encode parens for RFC1738
|
||||
- [Fix] `stringify`: fix arrayFormat comma with empty array/objects (#350)
|
||||
- [Refactor] `format`: remove `util.assign` call
|
||||
- [meta] add "Allow Edits" workflow; update rebase workflow
|
||||
- [actions] switch Automatic Rebase workflow to `pull_request_target` event
|
||||
- [Tests] `stringify`: add tests for #378
|
||||
- [Tests] migrate tests to Github Actions
|
||||
- [Tests] run `nyc` on all tests; use `tape` runner
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `mkdirp`, `object-inspect`, `tape`; add `aud`
|
||||
|
||||
## **6.9.4**
|
||||
- [Fix] `stringify`: when `arrayFormat` is `comma`, respect `serializeDate` (#364)
|
||||
- [Refactor] `stringify`: reduce branching (part of #350)
|
||||
- [Refactor] move `maybeMap` to `utils`
|
||||
- [Dev Deps] update `browserify`, `tape`
|
||||
|
||||
## **6.9.3**
|
||||
- [Fix] proper comma parsing of URL-encoded commas (#361)
|
||||
- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
|
||||
|
||||
## **6.9.2**
|
||||
- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
|
||||
- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
|
||||
- [meta] ignore eclint transitive audit warning
|
||||
- [meta] fix indentation in package.json
|
||||
- [meta] add tidelift marketing copy
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `object-inspect`, `has-symbols`, `tape`, `mkdirp`, `iconv-lite`
|
||||
- [actions] add automatic rebasing / merge commit blocking
|
||||
|
||||
## **6.9.1**
|
||||
- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
|
||||
- [Fix] `parse`: with comma true, do not split non-string values (#334)
|
||||
- [meta] add `funding` field
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`
|
||||
- [Tests] use shared travis-ci config
|
||||
|
||||
## **6.9.0**
|
||||
- [New] `parse`/`stringify`: Pass extra key/value argument to `decoder` (#333)
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `evalmd`
|
||||
- [Tests] `parse`: add passing `arrayFormat` tests
|
||||
- [Tests] add `posttest` using `npx aud` to run `npm audit` without a lockfile
|
||||
- [Tests] up to `node` `v12.10`, `v11.15`, `v10.16`, `v8.16`
|
||||
- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
|
||||
|
||||
## **6.8.5**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.8.4**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.8.3**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Tests] clean up stringify tests slightly
|
||||
- [Docs] add note and links for coercing primitive values (#408)
|
||||
- [meta] fix README.md (#399)
|
||||
- [actions] backport actions from main
|
||||
- [Dev Deps] backport updates from main
|
||||
- [Refactor] `stringify`: reduce branching
|
||||
- [meta] do not publish workflow files
|
||||
|
||||
## **6.8.2**
|
||||
- [Fix] proper comma parsing of URL-encoded commas (#361)
|
||||
- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
|
||||
|
||||
## **6.8.1**
|
||||
- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
|
||||
- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
|
||||
- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
|
||||
- [fix] `parse`: with comma true, do not split non-string values (#334)
|
||||
- [meta] add tidelift marketing copy
|
||||
- [meta] add `funding` field
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`, `safe-publish-latest`, `evalmd`, `has-symbols`, `iconv-lite`, `mkdirp`, `object-inspect`
|
||||
- [Tests] `parse`: add passing `arrayFormat` tests
|
||||
- [Tests] use shared travis-ci configs
|
||||
- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
|
||||
- [actions] add automatic rebasing / merge commit blocking
|
||||
|
||||
## **6.8.0**
|
||||
- [New] add `depth=false` to preserve the original key; [Fix] `depth=0` should preserve the original key (#326)
|
||||
- [New] [Fix] stringify symbols and bigints
|
||||
- [Fix] ensure node 0.12 can stringify Symbols
|
||||
- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
|
||||
- [Refactor] `formats`: tiny bit of cleanup.
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `safe-publish-latest`, `iconv-lite`, `tape`
|
||||
- [Tests] add tests for `depth=0` and `depth=false` behavior, both current and intuitive/intended (#326)
|
||||
- [Tests] use `eclint` instead of `editorconfig-tools`
|
||||
- [docs] readme: add security note
|
||||
- [meta] add github sponsorship
|
||||
- [meta] add FUNDING.yml
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
|
||||
## **6.7.5**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.7.4**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.7.3**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Docs] add note and links for coercing primitive values (#408)
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] do not publish workflow files
|
||||
- [actions] backport actions from main
|
||||
- [Dev Deps] backport updates from main
|
||||
- [Tests] use `nyc` for coverage
|
||||
- [Tests] clean up stringify tests slightly
|
||||
|
||||
## **6.7.2**
|
||||
- [Fix] proper comma parsing of URL-encoded commas (#361)
|
||||
- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
|
||||
|
||||
## **6.7.1**
|
||||
- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
|
||||
- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
|
||||
- [fix] `parse`: with comma true, do not split non-string values (#334)
|
||||
- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
|
||||
- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
|
||||
- [Refactor] `formats`: tiny bit of cleanup.
|
||||
- readme: add security note
|
||||
- [meta] add tidelift marketing copy
|
||||
- [meta] add `funding` field
|
||||
- [meta] add FUNDING.yml
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`, `safe-publish-latest`, `evalmd`, `iconv-lite`, `mkdirp`, `object-inspect`, `browserify`
|
||||
- [Tests] `parse`: add passing `arrayFormat` tests
|
||||
- [Tests] use shared travis-ci configs
|
||||
- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
|
||||
- [Tests] add tests for `depth=0` and `depth=false` behavior, both current and intuitive/intended
|
||||
- [Tests] use `eclint` instead of `editorconfig-tools`
|
||||
- [actions] add automatic rebasing / merge commit blocking
|
||||
|
||||
## **6.7.0**
|
||||
- [New] `stringify`/`parse`: add `comma` as an `arrayFormat` option (#276, #219)
|
||||
- [Fix] correctly parse nested arrays (#212)
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source, also with an array source
|
||||
- [Robustness] `stringify`: cache `Object.prototype.hasOwnProperty`
|
||||
- [Refactor] `utils`: `isBuffer`: small tweak; add tests
|
||||
- [Refactor] use cached `Array.isArray`
|
||||
- [Refactor] `parse`/`stringify`: make a function to normalize the options
|
||||
- [Refactor] `utils`: reduce observable [[Get]]s
|
||||
- [Refactor] `stringify`/`utils`: cache `Array.isArray`
|
||||
- [Tests] always use `String(x)` over `x.toString()`
|
||||
- [Tests] fix Buffer tests to work in node < 4.5 and node < 5.10
|
||||
- [Tests] temporarily allow coverage to fail
|
||||
|
||||
## **6.6.3**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.6.2**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.6.1**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [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
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
|
||||
- [Fix] correctly parse nested arrays
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [Robustness] `stringify`: cache `Object.prototype.hasOwnProperty`
|
||||
- [Refactor] `formats`: tiny bit of cleanup.
|
||||
- [Refactor] `utils`: `isBuffer`: small tweak; add tests
|
||||
- [Refactor]: `stringify`/`utils`: cache `Array.isArray`
|
||||
- [Refactor] `utils`: reduce observable [[Get]]s
|
||||
- [Refactor] use cached `Array.isArray`
|
||||
- [Refactor] `parse`/`stringify`: make a function to normalize the options
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Docs] Clarify the need for "arrayLimit" option
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] do not publish workflow files
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
- [meta] add FUNDING.yml
|
||||
- [meta] Fixes typo in CHANGELOG.md
|
||||
- [actions] backport actions from main
|
||||
- [Tests] fix Buffer tests to work in node < 4.5 and node < 5.10
|
||||
- [Tests] always use `String(x)` over `x.toString()`
|
||||
- [Dev Deps] backport from main
|
||||
|
||||
## **6.6.0**
|
||||
- [New] Add support for iso-8859-1, utf8 "sentinel" and numeric entities (#268)
|
||||
- [New] move two-value combine to a `utils` function (#189)
|
||||
- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
|
||||
- [Fix] when `parseArrays` is false, properly handle keys ending in `[]` (#260)
|
||||
- [Fix] `stringify`: do not crash in an obscure combo of `interpretNumericEntities`, a bad custom `decoder`, & `iso-8859-1`
|
||||
- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
|
||||
- [refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
|
||||
- [Refactor] `parse`: only need to reassign the var once
|
||||
- [Refactor] `parse`/`stringify`: clean up `charset` options checking; fix defaults
|
||||
- [Refactor] add missing defaults
|
||||
- [Refactor] `parse`: one less `concat` call
|
||||
- [Refactor] `utils`: `compactQueue`: make it explicitly side-effecting
|
||||
- [Dev Deps] update `browserify`, `eslint`, `@ljharb/eslint-config`, `iconv-lite`, `safe-publish-latest`, `tape`
|
||||
- [Tests] up to `node` `v10.10`, `v9.11`, `v8.12`, `v6.14`, `v4.9`; pin included builds to LTS
|
||||
|
||||
## **6.5.5**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.5.4**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **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
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`
|
||||
- [Tests] up to `node` `v8.4`; use `nvm install-latest-npm` so newer npm doesn’t break older node
|
||||
- [Tests] Use precise dist for Node.js 0.6 runtime (#225)
|
||||
- [Tests] make 0.6 required, now that it’s passing
|
||||
- [Tests] on `node` `v8.2`; fix npm on node 0.6
|
||||
|
||||
## **6.5.0**
|
||||
- [New] add `utils.assign`
|
||||
- [New] pass default encoder/decoder to custom encoder/decoder functions (#206)
|
||||
- [New] `parse`/`stringify`: add `ignoreQueryPrefix`/`addQueryPrefix` options, respectively (#213)
|
||||
- [Fix] Handle stringifying empty objects with addQueryPrefix (#217)
|
||||
- [Fix] do not mutate `options` argument (#207)
|
||||
- [Refactor] `parse`: cache index to reuse in else statement (#182)
|
||||
- [Docs] add various badges to readme (#208)
|
||||
- [Dev Deps] update `eslint`, `browserify`, `iconv-lite`, `tape`
|
||||
- [Tests] up to `node` `v8.1`, `v7.10`, `v6.11`; npm v4.6 breaks on node < v1; npm v5+ breaks on node < v4
|
||||
- [Tests] add `editorconfig-tools`
|
||||
|
||||
## **6.4.3**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.4.2**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace runkit CI badge with shields.io check-runs badge
|
||||
- [readme] replace travis CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.4.1**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
|
||||
- [Fix] use `safer-buffer` instead of `Buffer` constructor
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and an array source
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
|
||||
- [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 `[]`
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [Refactor] use cached `Array.isArray`
|
||||
- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
|
||||
- [readme] remove travis badge; add github actions/codecov badges; update URLs
|
||||
- [Docs] Clarify the need for "arrayLimit" option
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
- [meta] add FUNDING.yml
|
||||
- [actions] backport actions from main
|
||||
- [Tests] remove nonexistent tape option
|
||||
- [Dev Deps] backport from main
|
||||
|
||||
## **6.4.0**
|
||||
- [New] `qs.stringify`: add `encodeValuesOnly` option
|
||||
- [Fix] follow `allowPrototypes` option during merge (#201, #201)
|
||||
- [Fix] support keys starting with brackets (#202, #200)
|
||||
- [Fix] chmod a-x
|
||||
- [Dev Deps] update `eslint`
|
||||
- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
|
||||
- [eslint] reduce warnings
|
||||
|
||||
## **6.3.5**
|
||||
- [Fix] fix regressions from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.3.4**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace travis CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.3.3**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [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
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
|
||||
- [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 `[]`
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [Refactor] use cached `Array.isArray`
|
||||
- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
|
||||
- [Docs] Clarify the need for "arrayLimit" option
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
- [meta] add FUNDING.yml
|
||||
- [actions] backport actions from main
|
||||
- [Tests] use `safer-buffer` instead of `Buffer` constructor
|
||||
- [Tests] remove nonexistent tape option
|
||||
- [Dev Deps] backport from main
|
||||
|
||||
## **6.3.2**
|
||||
- [Fix] follow `allowPrototypes` option during merge (#201, #200)
|
||||
- [Dev Deps] update `eslint`
|
||||
- [Fix] chmod a-x
|
||||
- [Fix] support keys starting with brackets (#202, #200)
|
||||
- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
|
||||
|
||||
## **6.3.1**
|
||||
- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties (thanks, @snyk!)
|
||||
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `iconv-lite`, `qs-iconv`, `tape`
|
||||
- [Tests] on all node minors; improve test matrix
|
||||
- [Docs] document stringify option `allowDots` (#195)
|
||||
- [Docs] add empty object and array values example (#195)
|
||||
- [Docs] Fix minor inconsistency/typo (#192)
|
||||
- [Docs] document stringify option `sort` (#191)
|
||||
- [Refactor] `stringify`: throw faster with an invalid encoder
|
||||
- [Refactor] remove unnecessary escapes (#184)
|
||||
- Remove contributing.md, since `qs` is no longer part of `hapi` (#183)
|
||||
|
||||
## **6.3.0**
|
||||
- [New] Add support for RFC 1738 (#174, #173)
|
||||
- [New] `stringify`: Add `serializeDate` option to customize Date serialization (#159)
|
||||
- [Fix] ensure `utils.merge` handles merging two arrays
|
||||
- [Refactor] only constructors should be capitalized
|
||||
- [Refactor] capitalized var names are for constructors only
|
||||
- [Refactor] avoid using a sparse array
|
||||
- [Robustness] `formats`: cache `String#replace`
|
||||
- [Dev Deps] update `browserify`, `eslint`, `@ljharb/eslint-config`; add `safe-publish-latest`
|
||||
- [Tests] up to `node` `v6.8`, `v4.6`; improve test matrix
|
||||
- [Tests] flesh out arrayLimit/arrayFormat tests (#107)
|
||||
- [Tests] skip Object.create tests when null objects are not available
|
||||
- [Tests] Turn on eslint for test files (#175)
|
||||
|
||||
## **6.2.6**
|
||||
- [Fix] fix regression from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.2.5**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace travis CI badge with shields.io check-runs badge
|
||||
- [actions] fix rebase workflow permissions
|
||||
|
||||
## **6.2.4**
|
||||
- [Fix] `parse`: ignore `__proto__` keys (#428)
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and an array source
|
||||
- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
|
||||
- [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 `[]`
|
||||
- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
|
||||
- [Refactor] use cached `Array.isArray`
|
||||
- [Docs] Clarify the need for "arrayLimit" option
|
||||
- [meta] fix README.md (#399)
|
||||
- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
|
||||
- [meta] add FUNDING.yml
|
||||
- [actions] backport actions from main
|
||||
- [Tests] use `safer-buffer` instead of `Buffer` constructor
|
||||
- [Tests] remove nonexistent tape option
|
||||
- [Dev Deps] backport from main
|
||||
|
||||
## **6.2.3**
|
||||
- [Fix] follow `allowPrototypes` option during merge (#201, #200)
|
||||
- [Fix] chmod a-x
|
||||
- [Fix] support keys starting with brackets (#202, #200)
|
||||
- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
|
||||
|
||||
## **6.2.2**
|
||||
- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
|
||||
|
||||
## **6.2.1**
|
||||
- [Fix] ensure `key[]=x&key[]&key[]=y` results in 3, not 2, values
|
||||
- [Refactor] Be explicit and use `Object.prototype.hasOwnProperty.call`
|
||||
- [Tests] remove `parallelshell` since it does not reliably report failures
|
||||
- [Tests] up to `node` `v6.3`, `v5.12`
|
||||
- [Dev Deps] update `tape`, `eslint`, `@ljharb/eslint-config`, `qs-iconv`
|
||||
|
||||
## [**6.2.0**](https://github.com/ljharb/qs/issues?milestone=36&state=closed)
|
||||
- [New] pass Buffers to the encoder/decoder directly (#161)
|
||||
- [New] add "encoder" and "decoder" options, for custom param encoding/decoding (#160)
|
||||
- [Fix] fix compacting of nested sparse arrays (#150)
|
||||
|
||||
## **6.1.4**
|
||||
- [Fix] fix regression from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.1.3**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace travis CI badge with shields.io check-runs badge
|
||||
|
||||
## **6.1.2**
|
||||
- [Fix] follow `allowPrototypes` option during merge (#201, #200)
|
||||
- [Fix] chmod a-x
|
||||
- [Fix] support keys starting with brackets (#202, #200)
|
||||
- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
|
||||
|
||||
## **6.1.1**
|
||||
- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
|
||||
|
||||
## [**6.1.0**](https://github.com/ljharb/qs/issues?milestone=35&state=closed)
|
||||
- [New] allowDots option for `stringify` (#151)
|
||||
- [Fix] "sort" option should work at a depth of 3 or more (#151)
|
||||
- [Fix] Restore `dist` directory; will be removed in v7 (#148)
|
||||
|
||||
## **6.0.6**
|
||||
- [Fix] fix regression from robustness refactor
|
||||
- [meta] add `npmignore` to autogenerate an npmignore file
|
||||
- [actions] update reusable workflows
|
||||
|
||||
## **6.0.5**
|
||||
- [Robustness] avoid `.push`, use `void`
|
||||
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
||||
- [readme] replace travis CI badge with shields.io check-runs badge
|
||||
|
||||
## **6.0.4**
|
||||
- [Fix] follow `allowPrototypes` option during merge (#201, #200)
|
||||
- [Fix] chmod a-x
|
||||
- [Fix] support keys starting with brackets (#202, #200)
|
||||
- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
|
||||
|
||||
## **6.0.3**
|
||||
- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
|
||||
- [Fix] Restore `dist` directory; will be removed in v7 (#148)
|
||||
|
||||
## [**6.0.2**](https://github.com/ljharb/qs/issues?milestone=33&state=closed)
|
||||
- Revert ES6 requirement and restore support for node down to v0.8.
|
||||
|
||||
## [**6.0.1**](https://github.com/ljharb/qs/issues?milestone=32&state=closed)
|
||||
- [**#127**](https://github.com/ljharb/qs/pull/127) Fix engines definition in package.json
|
||||
|
||||
## [**6.0.0**](https://github.com/ljharb/qs/issues?milestone=31&state=closed)
|
||||
- [**#124**](https://github.com/ljharb/qs/issues/124) Use ES6 and drop support for node < v4
|
||||
|
||||
## **5.2.1**
|
||||
- [Fix] ensure `key[]=x&key[]&key[]=y` results in 3, not 2, values
|
||||
|
||||
## [**5.2.0**](https://github.com/ljharb/qs/issues?milestone=30&state=closed)
|
||||
- [**#64**](https://github.com/ljharb/qs/issues/64) Add option to sort object keys in the query string
|
||||
|
||||
## [**5.1.0**](https://github.com/ljharb/qs/issues?milestone=29&state=closed)
|
||||
- [**#117**](https://github.com/ljharb/qs/issues/117) make URI encoding stringified results optional
|
||||
- [**#106**](https://github.com/ljharb/qs/issues/106) Add flag `skipNulls` to optionally skip null values in stringify
|
||||
|
||||
## [**5.0.0**](https://github.com/ljharb/qs/issues?milestone=28&state=closed)
|
||||
- [**#114**](https://github.com/ljharb/qs/issues/114) default allowDots to false
|
||||
- [**#100**](https://github.com/ljharb/qs/issues/100) include dist to npm
|
||||
|
||||
## [**4.0.0**](https://github.com/ljharb/qs/issues?milestone=26&state=closed)
|
||||
- [**#98**](https://github.com/ljharb/qs/issues/98) make returning plain objects and allowing prototype overwriting properties optional
|
||||
|
||||
## [**3.1.0**](https://github.com/ljharb/qs/issues?milestone=24&state=closed)
|
||||
- [**#89**](https://github.com/ljharb/qs/issues/89) Add option to disable "Transform dot notation to bracket notation"
|
||||
|
||||
## [**3.0.0**](https://github.com/ljharb/qs/issues?milestone=23&state=closed)
|
||||
- [**#80**](https://github.com/ljharb/qs/issues/80) qs.parse silently drops properties
|
||||
- [**#77**](https://github.com/ljharb/qs/issues/77) Perf boost
|
||||
- [**#60**](https://github.com/ljharb/qs/issues/60) Add explicit option to disable array parsing
|
||||
- [**#74**](https://github.com/ljharb/qs/issues/74) Bad parse when turning array into object
|
||||
- [**#81**](https://github.com/ljharb/qs/issues/81) Add a `filter` option
|
||||
- [**#68**](https://github.com/ljharb/qs/issues/68) Fixed issue with recursion and passing strings into objects.
|
||||
- [**#66**](https://github.com/ljharb/qs/issues/66) Add mixed array and object dot notation support Closes: #47
|
||||
- [**#76**](https://github.com/ljharb/qs/issues/76) RFC 3986
|
||||
- [**#85**](https://github.com/ljharb/qs/issues/85) No equal sign
|
||||
- [**#84**](https://github.com/ljharb/qs/issues/84) update license attribute
|
||||
|
||||
## [**2.4.1**](https://github.com/ljharb/qs/issues?milestone=20&state=closed)
|
||||
- [**#73**](https://github.com/ljharb/qs/issues/73) Property 'hasOwnProperty' of object #<Object> is not a function
|
||||
|
||||
## [**2.4.0**](https://github.com/ljharb/qs/issues?milestone=19&state=closed)
|
||||
- [**#70**](https://github.com/ljharb/qs/issues/70) Add arrayFormat option
|
||||
|
||||
## [**2.3.3**](https://github.com/ljharb/qs/issues?milestone=18&state=closed)
|
||||
- [**#59**](https://github.com/ljharb/qs/issues/59) make sure array indexes are >= 0, closes #57
|
||||
- [**#58**](https://github.com/ljharb/qs/issues/58) make qs usable for browser loader
|
||||
|
||||
## [**2.3.2**](https://github.com/ljharb/qs/issues?milestone=17&state=closed)
|
||||
- [**#55**](https://github.com/ljharb/qs/issues/55) allow merging a string into an object
|
||||
|
||||
## [**2.3.1**](https://github.com/ljharb/qs/issues?milestone=16&state=closed)
|
||||
- [**#52**](https://github.com/ljharb/qs/issues/52) Return "undefined" and "false" instead of throwing "TypeError".
|
||||
|
||||
## [**2.3.0**](https://github.com/ljharb/qs/issues?milestone=15&state=closed)
|
||||
- [**#50**](https://github.com/ljharb/qs/issues/50) add option to omit array indices, closes #46
|
||||
|
||||
## [**2.2.5**](https://github.com/ljharb/qs/issues?milestone=14&state=closed)
|
||||
- [**#39**](https://github.com/ljharb/qs/issues/39) Is there an alternative to Buffer.isBuffer?
|
||||
- [**#49**](https://github.com/ljharb/qs/issues/49) refactor utils.merge, fixes #45
|
||||
- [**#41**](https://github.com/ljharb/qs/issues/41) avoid browserifying Buffer, for #39
|
||||
|
||||
## [**2.2.4**](https://github.com/ljharb/qs/issues?milestone=13&state=closed)
|
||||
- [**#38**](https://github.com/ljharb/qs/issues/38) how to handle object keys beginning with a number
|
||||
|
||||
## [**2.2.3**](https://github.com/ljharb/qs/issues?milestone=12&state=closed)
|
||||
- [**#37**](https://github.com/ljharb/qs/issues/37) parser discards first empty value in array
|
||||
- [**#36**](https://github.com/ljharb/qs/issues/36) Update to lab 4.x
|
||||
|
||||
## [**2.2.2**](https://github.com/ljharb/qs/issues?milestone=11&state=closed)
|
||||
- [**#33**](https://github.com/ljharb/qs/issues/33) Error when plain object in a value
|
||||
- [**#34**](https://github.com/ljharb/qs/issues/34) use Object.prototype.hasOwnProperty.call instead of obj.hasOwnProperty
|
||||
- [**#24**](https://github.com/ljharb/qs/issues/24) Changelog? Semver?
|
||||
|
||||
## [**2.2.1**](https://github.com/ljharb/qs/issues?milestone=10&state=closed)
|
||||
- [**#32**](https://github.com/ljharb/qs/issues/32) account for circular references properly, closes #31
|
||||
- [**#31**](https://github.com/ljharb/qs/issues/31) qs.parse stackoverflow on circular objects
|
||||
|
||||
## [**2.2.0**](https://github.com/ljharb/qs/issues?milestone=9&state=closed)
|
||||
- [**#26**](https://github.com/ljharb/qs/issues/26) Don't use Buffer global if it's not present
|
||||
- [**#30**](https://github.com/ljharb/qs/issues/30) Bug when merging non-object values into arrays
|
||||
- [**#29**](https://github.com/ljharb/qs/issues/29) Don't call Utils.clone at the top of Utils.merge
|
||||
- [**#23**](https://github.com/ljharb/qs/issues/23) Ability to not limit parameters?
|
||||
|
||||
## [**2.1.0**](https://github.com/ljharb/qs/issues?milestone=8&state=closed)
|
||||
- [**#22**](https://github.com/ljharb/qs/issues/22) Enable using a RegExp as delimiter
|
||||
|
||||
## [**2.0.0**](https://github.com/ljharb/qs/issues?milestone=7&state=closed)
|
||||
- [**#18**](https://github.com/ljharb/qs/issues/18) Why is there arrayLimit?
|
||||
- [**#20**](https://github.com/ljharb/qs/issues/20) Configurable parametersLimit
|
||||
- [**#21**](https://github.com/ljharb/qs/issues/21) make all limits optional, for #18, for #20
|
||||
|
||||
## [**1.2.2**](https://github.com/ljharb/qs/issues?milestone=6&state=closed)
|
||||
- [**#19**](https://github.com/ljharb/qs/issues/19) Don't overwrite null values
|
||||
|
||||
## [**1.2.1**](https://github.com/ljharb/qs/issues?milestone=5&state=closed)
|
||||
- [**#16**](https://github.com/ljharb/qs/issues/16) ignore non-string delimiters
|
||||
- [**#15**](https://github.com/ljharb/qs/issues/15) Close code block
|
||||
|
||||
## [**1.2.0**](https://github.com/ljharb/qs/issues?milestone=4&state=closed)
|
||||
- [**#12**](https://github.com/ljharb/qs/issues/12) Add optional delim argument
|
||||
- [**#13**](https://github.com/ljharb/qs/issues/13) fix #11: flattened keys in array are now correctly parsed
|
||||
|
||||
## [**1.1.0**](https://github.com/ljharb/qs/issues?milestone=3&state=closed)
|
||||
- [**#7**](https://github.com/ljharb/qs/issues/7) Empty values of a POST array disappear after being submitted
|
||||
- [**#9**](https://github.com/ljharb/qs/issues/9) Should not omit equals signs (=) when value is null
|
||||
- [**#6**](https://github.com/ljharb/qs/issues/6) Minor grammar fix in README
|
||||
|
||||
## [**1.0.2**](https://github.com/ljharb/qs/issues?milestone=2&state=closed)
|
||||
- [**#5**](https://github.com/ljharb/qs/issues/5) array holes incorrectly copied into object on large index
|
||||
+29
@@ -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.
|
||||
+758
@@ -0,0 +1,758 @@
|
||||
<p align="center">
|
||||
<img alt="qs" src="./logos/banner_default.png" width="800" />
|
||||
</p>
|
||||
|
||||
# qs <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||
|
||||
[![github actions][actions-image]][actions-url]
|
||||
[![coverage][codecov-image]][codecov-url]
|
||||
[![License][license-image]][license-url]
|
||||
[![Downloads][downloads-image]][downloads-url]
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/9058)
|
||||
|
||||
[![npm badge][npm-badge-png]][package-url]
|
||||
|
||||
A querystring parsing and stringifying library with some added security.
|
||||
|
||||
Lead Maintainer: [Jordan Harband](https://github.com/ljharb)
|
||||
|
||||
The **qs** module was originally created and maintained by [TJ Holowaychuk](https://github.com/visionmedia/node-querystring).
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
var qs = require('qs');
|
||||
var assert = require('assert');
|
||||
|
||||
var obj = qs.parse('a=c');
|
||||
assert.deepEqual(obj, { a: 'c' });
|
||||
|
||||
var str = qs.stringify(obj);
|
||||
assert.equal(str, 'a=c');
|
||||
```
|
||||
|
||||
### Parsing Objects
|
||||
|
||||
[](#preventEval)
|
||||
```javascript
|
||||
qs.parse(string, [options]);
|
||||
```
|
||||
|
||||
**qs** allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets `[]`.
|
||||
For example, the string `'foo[bar]=baz'` converts to:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('foo[bar]=baz'), {
|
||||
foo: {
|
||||
bar: 'baz'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When using the `plainObjects` option the parsed value is returned as a null object, created via `{ __proto__: null }` and as such you should be aware that prototype methods will not exist on it and a user may set those names to whatever value they like:
|
||||
|
||||
```javascript
|
||||
var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
|
||||
assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });
|
||||
```
|
||||
|
||||
By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties.
|
||||
*WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten.
|
||||
Always be careful with this option.
|
||||
|
||||
```javascript
|
||||
var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true });
|
||||
assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } });
|
||||
```
|
||||
|
||||
URI encoded strings work too:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
|
||||
a: { b: 'c' }
|
||||
});
|
||||
```
|
||||
|
||||
You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'foobarbaz'
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
By default, when nesting objects **qs** will only parse up to 5 children deep.
|
||||
This means if you attempt to parse a string like `'a[b][c][d][e][f][g][h][i]=j'` your resulting object will be:
|
||||
|
||||
```javascript
|
||||
var expected = {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
'[g][h][i]': 'j'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var string = 'a[b][c][d][e][f][g][h][i]=j';
|
||||
assert.deepEqual(qs.parse(string), expected);
|
||||
```
|
||||
|
||||
This depth can be overridden by passing a `depth` option to `qs.parse(string, [options])`:
|
||||
|
||||
```javascript
|
||||
var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
|
||||
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
|
||||
```
|
||||
|
||||
You can configure **qs** to throw an error when parsing nested input beyond this depth using the `strictDepth` option (defaulted to false):
|
||||
|
||||
```javascript
|
||||
try {
|
||||
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
|
||||
} catch (err) {
|
||||
assert(err instanceof RangeError);
|
||||
assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true');
|
||||
}
|
||||
```
|
||||
|
||||
The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. The strictDepth option adds a layer of protection by throwing an error when the limit is exceeded, allowing you to catch and handle such cases.
|
||||
|
||||
For similar reasons, by default **qs** will only parse up to 1000 parameters. This can be overridden by passing a `parameterLimit` option:
|
||||
|
||||
```javascript
|
||||
var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
|
||||
assert.deepEqual(limited, { a: 'b' });
|
||||
```
|
||||
|
||||
If you want an error to be thrown whenever the a limit is exceeded (eg, `parameterLimit`, `arrayLimit`), set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
|
||||
```javascript
|
||||
try {
|
||||
qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true });
|
||||
} catch (err) {
|
||||
assert(err instanceof Error);
|
||||
assert.strictEqual(err.message, 'Parameter limit exceeded. Only 3 parameters allowed.');
|
||||
}
|
||||
```
|
||||
|
||||
When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `parameterLimit` and ignore the rest without throwing an error.
|
||||
|
||||
To bypass the leading question mark, use `ignoreQueryPrefix`:
|
||||
|
||||
```javascript
|
||||
var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
|
||||
assert.deepEqual(prefixed, { a: 'b', c: 'd' });
|
||||
```
|
||||
|
||||
An optional delimiter can also be passed:
|
||||
|
||||
```javascript
|
||||
var delimited = qs.parse('a=b;c=d', { delimiter: ';' });
|
||||
assert.deepEqual(delimited, { a: 'b', c: 'd' });
|
||||
```
|
||||
|
||||
Delimiters can be a regular expression too:
|
||||
|
||||
```javascript
|
||||
var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
|
||||
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
|
||||
```
|
||||
|
||||
Option `allowDots` can be used to enable dot notation:
|
||||
|
||||
```javascript
|
||||
var withDots = qs.parse('a.b=c', { allowDots: true });
|
||||
assert.deepEqual(withDots, { a: { b: 'c' } });
|
||||
```
|
||||
|
||||
Option `decodeDotInKeys` can be used to decode dots in keys
|
||||
Note: it implies `allowDots`, so `parse` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`.
|
||||
|
||||
```javascript
|
||||
var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true });
|
||||
assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }});
|
||||
```
|
||||
|
||||
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' });
|
||||
```
|
||||
|
||||
Option `duplicates` can be used to change the behavior when duplicate keys are encountered
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] });
|
||||
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] });
|
||||
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'first' }), { foo: 'bar' });
|
||||
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'last' }), { foo: 'baz' });
|
||||
```
|
||||
|
||||
Note that keys with bracket notation (`[]`) always combine into arrays, regardless of the `duplicates` setting:
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'last' }), { a: '2', b: ['1', '2'] });
|
||||
```
|
||||
|
||||
If you have to deal with legacy browsers or services, there's also support for decoding percent-encoded octets as iso-8859-1:
|
||||
|
||||
```javascript
|
||||
var oldCharset = qs.parse('a=%A7', { charset: 'iso-8859-1' });
|
||||
assert.deepEqual(oldCharset, { a: '§' });
|
||||
```
|
||||
|
||||
Some services add an initial `utf8=✓` value to forms so that old Internet Explorer versions are more likely to submit the form as utf-8.
|
||||
Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was *not* sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set.
|
||||
|
||||
**qs** supports this mechanism via the `charsetSentinel` option.
|
||||
If specified, the `utf8` parameter will be omitted from the returned object.
|
||||
It will be used to switch to `iso-8859-1`/`utf-8` mode depending on how the checkmark is encoded.
|
||||
|
||||
**Important**: When you specify both the `charset` option and the `charsetSentinel` option, the `charset` will be overridden when the request contains a `utf8` parameter from which the actual charset can be deduced.
|
||||
In that sense the `charset` will behave as the default charset rather than the authoritative charset.
|
||||
|
||||
```javascript
|
||||
var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', {
|
||||
charset: 'iso-8859-1',
|
||||
charsetSentinel: true
|
||||
});
|
||||
assert.deepEqual(detectedAsUtf8, { a: 'ø' });
|
||||
|
||||
// Browsers encode the checkmark as ✓ when submitting as iso-8859-1:
|
||||
var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', {
|
||||
charset: 'utf-8',
|
||||
charsetSentinel: true
|
||||
});
|
||||
assert.deepEqual(detectedAsIso8859_1, { a: 'ø' });
|
||||
```
|
||||
|
||||
If you want to decode the `&#...;` syntax to the actual character, you can specify the `interpretNumericEntities` option as well:
|
||||
|
||||
```javascript
|
||||
var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', {
|
||||
charset: 'iso-8859-1',
|
||||
interpretNumericEntities: true
|
||||
});
|
||||
assert.deepEqual(detectedAsIso8859_1, { a: '☺' });
|
||||
```
|
||||
|
||||
It also works when the charset has been detected in `charsetSentinel` mode.
|
||||
|
||||
### Parsing Arrays
|
||||
|
||||
**qs** can also parse arrays using a similar `[]` notation:
|
||||
|
||||
```javascript
|
||||
var withArray = qs.parse('a[]=b&a[]=c');
|
||||
assert.deepEqual(withArray, { a: ['b', 'c'] });
|
||||
```
|
||||
|
||||
You may specify an index as well:
|
||||
|
||||
```javascript
|
||||
var withIndexes = qs.parse('a[1]=c&a[0]=b');
|
||||
assert.deepEqual(withIndexes, { a: ['b', 'c'] });
|
||||
```
|
||||
|
||||
Note that the only difference between an index in an array and a key in an object is that the value between the brackets must be a number to create an array.
|
||||
When creating arrays with specific indices, **qs** will compact a sparse array to only the existing values preserving their order:
|
||||
|
||||
```javascript
|
||||
var noSparse = qs.parse('a[1]=b&a[15]=c');
|
||||
assert.deepEqual(noSparse, { a: ['b', 'c'] });
|
||||
```
|
||||
|
||||
You may also use `allowSparse` option to parse sparse arrays:
|
||||
|
||||
```javascript
|
||||
var sparseArray = qs.parse('a[1]=2&a[3]=5', { allowSparse: true });
|
||||
assert.deepEqual(sparseArray, { a: [, '2', , '5'] });
|
||||
```
|
||||
|
||||
Note that an empty string is also a value, and will be preserved:
|
||||
|
||||
```javascript
|
||||
var withEmptyString = qs.parse('a[]=&a[]=b');
|
||||
assert.deepEqual(withEmptyString, { a: ['', 'b'] });
|
||||
|
||||
var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
|
||||
assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
|
||||
```
|
||||
|
||||
**qs** will also limit arrays to a maximum of `20` elements.
|
||||
Any array members with an index of `20` or greater will 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');
|
||||
assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });
|
||||
```
|
||||
|
||||
This limit can be overridden by passing an `arrayLimit` option:
|
||||
|
||||
```javascript
|
||||
var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
|
||||
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });
|
||||
```
|
||||
|
||||
If you want to throw an error whenever the array limit is exceeded, set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
|
||||
```javascript
|
||||
try {
|
||||
qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true });
|
||||
} catch (err) {
|
||||
assert(err instanceof Error);
|
||||
assert.strictEqual(err.message, 'Array limit exceeded. Only 0 elements allowed in an array.');
|
||||
}
|
||||
```
|
||||
|
||||
When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `arrayLimit` and if the limit is exceeded, the array will instead be converted to an object with the index as the key
|
||||
|
||||
To prevent array syntax (`a[]`, `a[0]`) from being parsed as arrays, set `parseArrays` to `false`.
|
||||
Note that duplicate keys (e.g. `a=b&a=c`) may still produce arrays when `duplicates` is `'combine'` (the default).
|
||||
|
||||
```javascript
|
||||
var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
|
||||
assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });
|
||||
```
|
||||
|
||||
If you mix notations, **qs** will merge the two items into an object:
|
||||
|
||||
```javascript
|
||||
var mixedNotation = qs.parse('a[0]=b&a[b]=c');
|
||||
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
|
||||
```
|
||||
|
||||
When a key appears as both a plain value and an object, **qs** will by default wrap the conflicting values in an array (`strictMerge` defaults to `true`):
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('a[b]=c&a=d'), { a: [{ b: 'c' }, 'd'] });
|
||||
assert.deepEqual(qs.parse('a=d&a[b]=c'), { a: ['d', { b: 'c' }] });
|
||||
```
|
||||
|
||||
To restore the legacy behavior (where the primitive is used as a key with value `true`), set `strictMerge` to `false`:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(qs.parse('a[b]=c&a=d', { strictMerge: false }), { a: { b: 'c', d: true } });
|
||||
```
|
||||
|
||||
You can also create arrays of objects:
|
||||
|
||||
```javascript
|
||||
var arraysOfObjects = qs.parse('a[][b]=c');
|
||||
assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });
|
||||
```
|
||||
|
||||
Some people use comma to join array, **qs** can parse it:
|
||||
```javascript
|
||||
var arraysOfObjects = qs.parse('a=b,c', { comma: true })
|
||||
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })
|
||||
```
|
||||
(_this cannot convert nested objects, such as `a={b:1},{c:d}`_)
|
||||
|
||||
### Parsing primitive/scalar values (numbers, booleans, null, etc)
|
||||
|
||||
By default, all values are parsed as strings.
|
||||
This behavior will not change and is explained in [issue #91](https://github.com/ljharb/qs/issues/91).
|
||||
|
||||
```javascript
|
||||
var primitiveValues = qs.parse('a=15&b=true&c=null');
|
||||
assert.deepEqual(primitiveValues, { a: '15', b: 'true', c: 'null' });
|
||||
```
|
||||
|
||||
If you wish to auto-convert values which look like numbers, booleans, and other values into their primitive counterparts, you can use the [query-types Express JS middleware](https://github.com/xpepermint/query-types) which will auto-convert all request query parameters.
|
||||
|
||||
### Stringifying
|
||||
|
||||
[](#preventEval)
|
||||
```javascript
|
||||
qs.stringify(object, [options]);
|
||||
```
|
||||
|
||||
When stringifying, **qs** by default URI encodes output. Objects are stringified as you would expect:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: 'b' }), 'a=b');
|
||||
assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
|
||||
```
|
||||
|
||||
This encoding can be disabled by setting the `encode` option to `false`:
|
||||
|
||||
```javascript
|
||||
var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
|
||||
assert.equal(unencoded, 'a[b]=c');
|
||||
```
|
||||
|
||||
Encoding can be disabled for keys by setting the `encodeValuesOnly` option to `true`:
|
||||
```javascript
|
||||
var encodedValues = qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
|
||||
{ encodeValuesOnly: true }
|
||||
);
|
||||
assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');
|
||||
```
|
||||
|
||||
This encoding can also be replaced by a custom encoding method set as `encoder` option:
|
||||
|
||||
```javascript
|
||||
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
|
||||
// Passed in values `a`, `b`, `c`
|
||||
return // Return encoded string
|
||||
}})
|
||||
```
|
||||
|
||||
_(Note: the `encoder` option does not apply if `encode` is `false`)_
|
||||
|
||||
Analogue to the `encoder` there is a `decoder` option for `parse` to override decoding of properties and values:
|
||||
|
||||
```javascript
|
||||
var decoded = qs.parse('x=z', { decoder: function (str) {
|
||||
// Passed in values `x`, `z`
|
||||
return // Return decoded string
|
||||
}})
|
||||
```
|
||||
|
||||
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, they follow the `arrayFormat` option, which defaults to `indices`:
|
||||
|
||||
```javascript
|
||||
qs.stringify({ a: ['b', 'c', 'd'] });
|
||||
// 'a[0]=b&a[1]=c&a[2]=d'
|
||||
```
|
||||
|
||||
You may override this by setting the `indices` option to `false`, or to be more explicit, the `arrayFormat` option to `repeat`:
|
||||
|
||||
```javascript
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
|
||||
// 'a=b&a=c&a=d'
|
||||
```
|
||||
|
||||
You may use the `arrayFormat` option to specify the format of the output array:
|
||||
|
||||
```javascript
|
||||
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
|
||||
// 'a[0]=b&a[1]=c'
|
||||
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
|
||||
// 'a[]=b&a[]=c'
|
||||
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
|
||||
// 'a=b&a=c'
|
||||
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
|
||||
// 'a=b,c'
|
||||
```
|
||||
|
||||
Note: when using `arrayFormat` set to `'comma'`, you can also pass the `commaRoundTrip` option set to `true` or `false`, to append `[]` on single-item arrays, so that they can round trip through a parse.
|
||||
|
||||
When objects are stringified, by default they use bracket notation:
|
||||
|
||||
```javascript
|
||||
qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
|
||||
// 'a[b][c]=d&a[b][e]=f'
|
||||
```
|
||||
|
||||
You may override this to use dot notation by setting the `allowDots` option to `true`:
|
||||
|
||||
```javascript
|
||||
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
|
||||
// 'a.b.c=d&a.b.e=f'
|
||||
```
|
||||
|
||||
You may encode the dot notation in the keys of object with option `encodeDotInKeys` by setting it to `true`:
|
||||
Note: it implies `allowDots`, so `stringify` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`.
|
||||
Caveat: when `encodeValuesOnly` is `true` as well as `encodeDotInKeys`, only dots in keys and nothing else will be encoded.
|
||||
```javascript
|
||||
qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true })
|
||||
// 'name%252Eobj.first=John&name%252Eobj.last=Doe'
|
||||
```
|
||||
|
||||
You may allow empty array values by setting the `allowEmptyArrays` option to `true`:
|
||||
```javascript
|
||||
qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true });
|
||||
// 'foo[]&bar=baz'
|
||||
```
|
||||
|
||||
Empty strings and null values will omit the value, but the equals sign (=) remains in place:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: '' }), 'a=');
|
||||
```
|
||||
|
||||
Key with no values (such as an empty object or array) will return nothing:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: [] }), '');
|
||||
assert.equal(qs.stringify({ a: {} }), '');
|
||||
assert.equal(qs.stringify({ a: [{}] }), '');
|
||||
assert.equal(qs.stringify({ a: { b: []} }), '');
|
||||
assert.equal(qs.stringify({ a: { b: {}} }), '');
|
||||
```
|
||||
|
||||
Properties that are set to `undefined` will be omitted entirely:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
|
||||
```
|
||||
|
||||
The query string may optionally be prepended with a question mark:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
|
||||
```
|
||||
|
||||
Note that when the output is an empty string, the prefix will not be added:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({}, { addQueryPrefix: true }), '');
|
||||
```
|
||||
|
||||
The delimiter may be overridden with stringify as well:
|
||||
|
||||
```javascript
|
||||
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
|
||||
```
|
||||
|
||||
If you only want to override the serialization of `Date` objects, you can provide a `serializeDate` option:
|
||||
|
||||
```javascript
|
||||
var date = new Date(7);
|
||||
assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A'));
|
||||
assert.equal(
|
||||
qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
|
||||
'a=7'
|
||||
);
|
||||
```
|
||||
|
||||
You may use the `sort` option to affect the order of parameter keys:
|
||||
|
||||
```javascript
|
||||
function alphabeticalSort(a, b) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
|
||||
```
|
||||
|
||||
Finally, you can use the `filter` option to restrict which keys will be included in the stringified output.
|
||||
If you pass a function, it will be called for each key to obtain the replacement value.
|
||||
Otherwise, if you pass an array, it will be used to select properties and array indices for stringification:
|
||||
|
||||
```javascript
|
||||
function filterFunc(prefix, value) {
|
||||
if (prefix == 'b') {
|
||||
// Return an `undefined` value to omit a property.
|
||||
return;
|
||||
}
|
||||
if (prefix == 'e[f]') {
|
||||
return value.getTime();
|
||||
}
|
||||
if (prefix == 'e[g][0]') {
|
||||
return value * 2;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
|
||||
// 'a=b&c=d&e[f]=123&e[g][0]=4'
|
||||
qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] });
|
||||
// 'a=b&e=f'
|
||||
qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] });
|
||||
// 'a[0]=b&a[2]=d'
|
||||
```
|
||||
|
||||
You could also use `filter` to inject custom serialization for user defined types.
|
||||
Consider you're working with some api that expects query strings of the format for ranges:
|
||||
|
||||
```
|
||||
https://domain.com/endpoint?range=30...70
|
||||
```
|
||||
|
||||
For which you model as:
|
||||
|
||||
```javascript
|
||||
class Range {
|
||||
constructor(from, to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You could _inject_ a custom serializer to handle values of this type:
|
||||
|
||||
```javascript
|
||||
qs.stringify(
|
||||
{
|
||||
range: new Range(30, 70),
|
||||
},
|
||||
{
|
||||
filter: (prefix, value) => {
|
||||
if (value instanceof Range) {
|
||||
return `${value.from}...${value.to}`;
|
||||
}
|
||||
// serialize the usual way
|
||||
return value;
|
||||
},
|
||||
}
|
||||
);
|
||||
// range=30...70
|
||||
```
|
||||
|
||||
### Handling of `null` values
|
||||
|
||||
By default, `null` values are treated like empty strings:
|
||||
|
||||
```javascript
|
||||
var withNull = qs.stringify({ a: null, b: '' });
|
||||
assert.equal(withNull, 'a=&b=');
|
||||
```
|
||||
|
||||
Parsing does not distinguish between parameters with and without equal signs.
|
||||
Both are converted to empty strings.
|
||||
|
||||
```javascript
|
||||
var equalsInsensitive = qs.parse('a&b=');
|
||||
assert.deepEqual(equalsInsensitive, { a: '', b: '' });
|
||||
```
|
||||
|
||||
To distinguish between `null` values and empty strings use the `strictNullHandling` flag. In the result string the `null`
|
||||
values have no `=` sign:
|
||||
|
||||
```javascript
|
||||
var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
|
||||
assert.equal(strictNull, 'a&b=');
|
||||
```
|
||||
|
||||
To parse values without `=` back to `null` use the `strictNullHandling` flag:
|
||||
|
||||
```javascript
|
||||
var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true });
|
||||
assert.deepEqual(parsedStrictNull, { a: null, b: '' });
|
||||
```
|
||||
|
||||
To completely skip rendering keys with `null` values, use the `skipNulls` flag:
|
||||
|
||||
```javascript
|
||||
var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
|
||||
assert.equal(nullsSkipped, 'a=b');
|
||||
```
|
||||
|
||||
If you're communicating with legacy systems, you can switch to `iso-8859-1` using the `charset` option:
|
||||
|
||||
```javascript
|
||||
var iso = qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' });
|
||||
assert.equal(iso, '%E6=%E6');
|
||||
```
|
||||
|
||||
Characters that don't exist in `iso-8859-1` will be converted to numeric entities, similar to what browsers do:
|
||||
|
||||
```javascript
|
||||
var numeric = qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' });
|
||||
assert.equal(numeric, 'a=%26%239786%3B');
|
||||
```
|
||||
|
||||
You can use the `charsetSentinel` option to announce the character by including an `utf8=✓` parameter with the proper encoding if the checkmark, similar to what Ruby on Rails and others do when submitting forms.
|
||||
|
||||
```javascript
|
||||
var sentinel = qs.stringify({ a: '☺' }, { charsetSentinel: true });
|
||||
assert.equal(sentinel, 'utf8=%E2%9C%93&a=%E2%98%BA');
|
||||
|
||||
var isoSentinel = qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' });
|
||||
assert.equal(isoSentinel, 'utf8=%26%2310003%3B&a=%E6');
|
||||
```
|
||||
|
||||
### Dealing with special character sets
|
||||
|
||||
By default the encoding and decoding of characters is done in `utf-8`, and `iso-8859-1` support is also built in via the `charset` parameter.
|
||||
|
||||
If you wish to encode querystrings to a different character set (i.e.
|
||||
[Shift JIS](https://en.wikipedia.org/wiki/Shift_JIS)) you can use the
|
||||
[`qs-iconv`](https://github.com/martinheidegger/qs-iconv) library:
|
||||
|
||||
```javascript
|
||||
var encoder = require('qs-iconv/encoder')('shift_jis');
|
||||
var shiftJISEncoded = qs.stringify({ a: 'こんにちは!' }, { encoder: encoder });
|
||||
assert.equal(shiftJISEncoded, 'a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I');
|
||||
```
|
||||
|
||||
This also works for decoding of query strings:
|
||||
|
||||
```javascript
|
||||
var decoder = require('qs-iconv/decoder')('shift_jis');
|
||||
var obj = qs.parse('a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I', { decoder: decoder });
|
||||
assert.deepEqual(obj, { a: 'こんにちは!' });
|
||||
```
|
||||
|
||||
### RFC 3986 and RFC 1738 space encoding
|
||||
|
||||
RFC3986 used as default option and encodes ' ' to *%20* which is backward compatible.
|
||||
In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'.
|
||||
|
||||
```
|
||||
assert.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
|
||||
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC3986' }), 'a=b%20c');
|
||||
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC1738' }), 'a=b+c');
|
||||
```
|
||||
|
||||
## 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]: 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/github/check-runs/ljharb/qs/main
|
||||
[actions-url]: https://github.com/ljharb/qs/actions
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
qs logo by [NUMI](https://github.com/numi-hq/open-design):
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/numi-hq/open-design/main/assets/numi-lockup.png" alt="NUMI Logo" style="width: 200px;"/>](https://numi.tech/?ref=qs)
|
||||
+141
File diff suppressed because one or more lines are too long
+57
@@ -0,0 +1,57 @@
|
||||
import ljharbConfig from '@ljharb/eslint-config/flat';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['dist/'],
|
||||
},
|
||||
|
||||
...ljharbConfig,
|
||||
|
||||
{
|
||||
rules: {
|
||||
complexity: 'off',
|
||||
'consistent-return': 'warn',
|
||||
eqeqeq: ['error', 'allow-null'],
|
||||
'func-name-matching': 'off',
|
||||
'id-length': [
|
||||
'error',
|
||||
{
|
||||
max: 25,
|
||||
min: 1,
|
||||
properties: 'never',
|
||||
},
|
||||
],
|
||||
indent: ['error', 4],
|
||||
'max-lines': 'off',
|
||||
'max-lines-per-function': [
|
||||
'error',
|
||||
{ max: 150 },
|
||||
],
|
||||
'max-params': ['error', 18],
|
||||
'max-statements': ['error', 100],
|
||||
'multiline-comment-style': 'off',
|
||||
'no-continue': 'warn',
|
||||
'no-magic-numbers': 'off',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
'BreakStatement',
|
||||
'DebuggerStatement',
|
||||
'ForInStatement',
|
||||
'LabeledStatement',
|
||||
'WithStatement',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['test/**'],
|
||||
rules: {
|
||||
'function-paren-newline': 'off',
|
||||
'max-lines-per-function': 'off',
|
||||
'max-statements': 'off',
|
||||
'no-buffer-constructor': 'off',
|
||||
'no-extend-native': 'off',
|
||||
'no-throw-literal': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
var replace = String.prototype.replace;
|
||||
var percentTwenties = /%20/g;
|
||||
|
||||
var Format = {
|
||||
RFC1738: 'RFC1738',
|
||||
RFC3986: 'RFC3986'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
'default': Format.RFC3986,
|
||||
formatters: {
|
||||
RFC1738: function (value) {
|
||||
return replace.call(value, percentTwenties, '+');
|
||||
},
|
||||
RFC3986: function (value) {
|
||||
return String(value);
|
||||
}
|
||||
},
|
||||
RFC1738: Format.RFC1738,
|
||||
RFC3986: Format.RFC3986
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
var stringify = require('./stringify');
|
||||
var parse = require('./parse');
|
||||
var formats = require('./formats');
|
||||
|
||||
module.exports = {
|
||||
formats: formats,
|
||||
parse: parse,
|
||||
stringify: stringify
|
||||
};
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
'use strict';
|
||||
|
||||
var utils = require('./utils');
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
var isArray = Array.isArray;
|
||||
|
||||
var defaults = {
|
||||
allowDots: false,
|
||||
allowEmptyArrays: false,
|
||||
allowPrototypes: false,
|
||||
allowSparse: false,
|
||||
arrayLimit: 20,
|
||||
charset: 'utf-8',
|
||||
charsetSentinel: false,
|
||||
comma: false,
|
||||
decodeDotInKeys: false,
|
||||
decoder: utils.decode,
|
||||
delimiter: '&',
|
||||
depth: 5,
|
||||
duplicates: 'combine',
|
||||
ignoreQueryPrefix: false,
|
||||
interpretNumericEntities: false,
|
||||
parameterLimit: 1000,
|
||||
parseArrays: true,
|
||||
plainObjects: false,
|
||||
strictDepth: false,
|
||||
strictMerge: true,
|
||||
strictNullHandling: false,
|
||||
throwOnLimitExceeded: false
|
||||
};
|
||||
|
||||
var interpretNumericEntities = function (str) {
|
||||
return str.replace(/&#(\d+);/g, function ($0, numberStr) {
|
||||
return String.fromCharCode(parseInt(numberStr, 10));
|
||||
});
|
||||
};
|
||||
|
||||
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(',');
|
||||
}
|
||||
|
||||
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
|
||||
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
// This is what browsers will submit when the ✓ character occurs in an
|
||||
// application/x-www-form-urlencoded body and the encoding of the page containing
|
||||
// the form is iso-8859-1, or when the submitted form has an accept-charset
|
||||
// attribute of iso-8859-1. Presumably also with other charsets that do not contain
|
||||
// the ✓ character, such as us-ascii.
|
||||
var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('✓')
|
||||
|
||||
// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded.
|
||||
var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓')
|
||||
|
||||
var parseValues = function parseQueryStringValues(str, options) {
|
||||
var obj = { __proto__: null };
|
||||
|
||||
var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
|
||||
cleanStr = cleanStr.replace(/%5B/gi, '[').replace(/%5D/gi, ']');
|
||||
|
||||
var limit = options.parameterLimit === Infinity ? void undefined : options.parameterLimit;
|
||||
var parts = cleanStr.split(
|
||||
options.delimiter,
|
||||
options.throwOnLimitExceeded && typeof limit !== 'undefined' ? limit + 1 : limit
|
||||
);
|
||||
|
||||
if (options.throwOnLimitExceeded && typeof limit !== 'undefined' && parts.length > limit) {
|
||||
throw new RangeError('Parameter limit exceeded. Only ' + limit + ' parameter' + (limit === 1 ? '' : 's') + ' allowed.');
|
||||
}
|
||||
|
||||
var skipIndex = -1; // Keep track of where the utf8 sentinel was found
|
||||
var i;
|
||||
|
||||
var charset = options.charset;
|
||||
if (options.charsetSentinel) {
|
||||
for (i = 0; i < parts.length; ++i) {
|
||||
if (parts[i].indexOf('utf8=') === 0) {
|
||||
if (parts[i] === charsetSentinel) {
|
||||
charset = 'utf-8';
|
||||
} else if (parts[i] === isoSentinel) {
|
||||
charset = 'iso-8859-1';
|
||||
}
|
||||
skipIndex = i;
|
||||
i = parts.length; // The eslint settings do not allow break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < parts.length; ++i) {
|
||||
if (i === skipIndex) {
|
||||
continue;
|
||||
}
|
||||
var part = parts[i];
|
||||
|
||||
var bracketEqualsPos = part.indexOf(']=');
|
||||
var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
|
||||
|
||||
var key;
|
||||
var val;
|
||||
if (pos === -1) {
|
||||
key = options.decoder(part, defaults.decoder, charset, 'key');
|
||||
val = options.strictNullHandling ? null : '';
|
||||
} else {
|
||||
key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
|
||||
|
||||
if (key !== null) {
|
||||
val = utils.maybeMap(
|
||||
parseArrayValue(
|
||||
part.slice(pos + 1),
|
||||
options,
|
||||
isArray(obj[key]) ? obj[key].length : 0,
|
||||
part.indexOf('[]=') === -1
|
||||
),
|
||||
function (encodedVal) {
|
||||
return options.decoder(encodedVal, defaults.decoder, charset, 'value');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (val && options.interpretNumericEntities && charset === 'iso-8859-1') {
|
||||
val = interpretNumericEntities(String(val));
|
||||
}
|
||||
|
||||
if (part.indexOf('[]=') > -1) {
|
||||
val = isArray(val) ? [val] : val;
|
||||
}
|
||||
|
||||
if (options.comma && isArray(val) && val.length > options.arrayLimit) {
|
||||
val = utils.combine([], val, options.arrayLimit, options.plainObjects, options.throwOnLimitExceeded);
|
||||
}
|
||||
|
||||
if (key !== null) {
|
||||
var existing = has.call(obj, key);
|
||||
if (existing && (options.duplicates === 'combine' || part.indexOf('[]=') > -1)) {
|
||||
obj[key] = utils.combine(
|
||||
obj[key],
|
||||
val,
|
||||
options.arrayLimit,
|
||||
options.plainObjects,
|
||||
options.throwOnLimitExceeded
|
||||
);
|
||||
} else if (!existing || options.duplicates === 'last') {
|
||||
obj[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
var parseObject = function (chain, val, options, valuesParsed) {
|
||||
var currentArrayLength = 0;
|
||||
if (chain.length > 0 && chain[chain.length - 1] === '[]') {
|
||||
var parentKey = chain.slice(0, -1).join('');
|
||||
currentArrayLength = Array.isArray(val) && val[parentKey] ? val[parentKey].length : 0;
|
||||
}
|
||||
|
||||
var leaf = valuesParsed ? val : parseArrayValue(val, options, currentArrayLength);
|
||||
|
||||
for (var i = chain.length - 1; i >= 0; --i) {
|
||||
var obj;
|
||||
var root = chain[i];
|
||||
|
||||
if (root === '[]' && options.parseArrays) {
|
||||
if (utils.isOverflow(leaf)) {
|
||||
// leaf is already an overflow object, preserve it
|
||||
obj = leaf;
|
||||
} else {
|
||||
obj = options.allowEmptyArrays && (leaf === '' || (options.strictNullHandling && leaf === null))
|
||||
? []
|
||||
: utils.combine(
|
||||
[],
|
||||
leaf,
|
||||
options.arrayLimit,
|
||||
options.plainObjects,
|
||||
options.throwOnLimitExceeded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
obj = options.plainObjects ? { __proto__: null } : {};
|
||||
var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
|
||||
var decodedRoot = options.decodeDotInKeys ? cleanRoot.replace(/%2E/g, '.') : cleanRoot;
|
||||
var index = parseInt(decodedRoot, 10);
|
||||
var isValidArrayIndex = !isNaN(index)
|
||||
&& root !== decodedRoot
|
||||
&& String(index) === decodedRoot
|
||||
&& index >= 0
|
||||
&& options.parseArrays;
|
||||
if (!options.parseArrays && decodedRoot === '') {
|
||||
obj = { 0: leaf };
|
||||
} else if (isValidArrayIndex && index < options.arrayLimit) {
|
||||
obj = [];
|
||||
obj[index] = leaf;
|
||||
} else if (isValidArrayIndex && options.throwOnLimitExceeded) {
|
||||
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
|
||||
} else if (isValidArrayIndex) {
|
||||
obj[index] = leaf;
|
||||
utils.markOverflow(obj, index);
|
||||
} else if (decodedRoot !== '__proto__') {
|
||||
obj[decodedRoot] = leaf;
|
||||
}
|
||||
}
|
||||
|
||||
leaf = obj;
|
||||
}
|
||||
|
||||
return leaf;
|
||||
};
|
||||
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return [key];
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
segments[segments.length] = parent;
|
||||
}
|
||||
|
||||
var n = key.length;
|
||||
var open = first;
|
||||
var collected = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 (open >= 0) {
|
||||
if (options.strictDepth === true) {
|
||||
throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
|
||||
}
|
||||
|
||||
segments[segments.length] = '[' + key.slice(open) + ']';
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
|
||||
if (!givenKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var keys = splitKeyIntoSegments(givenKey, options);
|
||||
|
||||
if (!keys) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parseObject(keys, val, options, valuesParsed);
|
||||
};
|
||||
|
||||
var normalizeParseOptions = function normalizeParseOptions(opts) {
|
||||
if (!opts) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
|
||||
throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
|
||||
}
|
||||
|
||||
if (typeof opts.decodeDotInKeys !== 'undefined' && typeof opts.decodeDotInKeys !== 'boolean') {
|
||||
throw new TypeError('`decodeDotInKeys` option can only be `true` or `false`, when provided');
|
||||
}
|
||||
|
||||
if (opts.decoder !== null && typeof opts.decoder !== 'undefined' && typeof opts.decoder !== 'function') {
|
||||
throw new TypeError('Decoder has to be a function.');
|
||||
}
|
||||
|
||||
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
|
||||
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
|
||||
}
|
||||
|
||||
if (typeof opts.throwOnLimitExceeded !== 'undefined' && typeof opts.throwOnLimitExceeded !== 'boolean') {
|
||||
throw new TypeError('`throwOnLimitExceeded` option must be a boolean');
|
||||
}
|
||||
|
||||
var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;
|
||||
|
||||
var duplicates = typeof opts.duplicates === 'undefined' ? defaults.duplicates : opts.duplicates;
|
||||
|
||||
if (duplicates !== 'combine' && duplicates !== 'first' && duplicates !== 'last') {
|
||||
throw new TypeError('The duplicates option must be either combine, first, or last');
|
||||
}
|
||||
|
||||
var allowDots = typeof opts.allowDots === 'undefined' ? opts.decodeDotInKeys === true ? true : defaults.allowDots : !!opts.allowDots;
|
||||
|
||||
return {
|
||||
allowDots: allowDots,
|
||||
allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
|
||||
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,
|
||||
charset: charset,
|
||||
charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
|
||||
comma: typeof opts.comma === 'boolean' ? opts.comma : defaults.comma,
|
||||
decodeDotInKeys: typeof opts.decodeDotInKeys === 'boolean' ? opts.decodeDotInKeys : defaults.decodeDotInKeys,
|
||||
decoder: typeof opts.decoder === 'function' ? opts.decoder : defaults.decoder,
|
||||
delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter,
|
||||
// eslint-disable-next-line no-implicit-coercion, no-extra-parens
|
||||
depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth,
|
||||
duplicates: duplicates,
|
||||
ignoreQueryPrefix: opts.ignoreQueryPrefix === true,
|
||||
interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities,
|
||||
parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
|
||||
parseArrays: opts.parseArrays !== false,
|
||||
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
|
||||
strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
|
||||
strictMerge: typeof opts.strictMerge === 'boolean' ? !!opts.strictMerge : defaults.strictMerge,
|
||||
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
|
||||
throwOnLimitExceeded: typeof opts.throwOnLimitExceeded === 'boolean' ? opts.throwOnLimitExceeded : false
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = function (str, opts) {
|
||||
var options = normalizeParseOptions(opts);
|
||||
|
||||
if (str === '' || str === null || typeof str === 'undefined') {
|
||||
return options.plainObjects ? { __proto__: null } : {};
|
||||
}
|
||||
|
||||
var tempObj = typeof str === 'string' ? parseValues(str, options) : str;
|
||||
var obj = options.plainObjects ? { __proto__: null } : {};
|
||||
|
||||
// Iterate over the keys and setup the new object
|
||||
|
||||
var keys = Object.keys(tempObj);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var key = keys[i];
|
||||
var newObj = parseKeys(key, tempObj[key], options, typeof str === 'string');
|
||||
obj = utils.merge(obj, newObj, options);
|
||||
}
|
||||
|
||||
if (options.allowSparse === true) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
return utils.compact(obj);
|
||||
};
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
'use strict';
|
||||
|
||||
var getSideChannel = require('side-channel');
|
||||
var utils = require('./utils');
|
||||
var formats = require('./formats');
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
|
||||
var arrayPrefixGenerators = {
|
||||
brackets: function brackets(prefix) {
|
||||
return prefix + '[]';
|
||||
},
|
||||
comma: 'comma',
|
||||
indices: function indices(prefix, key) {
|
||||
return prefix + '[' + key + ']';
|
||||
},
|
||||
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 defaultFormat = formats['default'];
|
||||
var defaults = {
|
||||
addQueryPrefix: false,
|
||||
allowDots: false,
|
||||
allowEmptyArrays: false,
|
||||
arrayFormat: 'indices',
|
||||
charset: 'utf-8',
|
||||
charsetSentinel: false,
|
||||
commaRoundTrip: false,
|
||||
delimiter: '&',
|
||||
encode: true,
|
||||
encodeDotInKeys: false,
|
||||
encoder: utils.encode,
|
||||
encodeValuesOnly: false,
|
||||
filter: void undefined,
|
||||
format: defaultFormat,
|
||||
formatter: formats.formatters[defaultFormat],
|
||||
// deprecated
|
||||
indices: false,
|
||||
serializeDate: function serializeDate(date) {
|
||||
return toISO.call(date);
|
||||
},
|
||||
skipNulls: false,
|
||||
strictNullHandling: false
|
||||
};
|
||||
|
||||
var isNonNullishPrimitive = function isNonNullishPrimitive(v) {
|
||||
return typeof v === 'string'
|
||||
|| typeof v === 'number'
|
||||
|| typeof v === 'boolean'
|
||||
|| typeof v === 'symbol'
|
||||
|| typeof v === 'bigint';
|
||||
};
|
||||
|
||||
var sentinel = {};
|
||||
|
||||
var stringify = function stringify(
|
||||
object,
|
||||
prefix,
|
||||
generateArrayPrefix,
|
||||
commaRoundTrip,
|
||||
allowEmptyArrays,
|
||||
strictNullHandling,
|
||||
skipNulls,
|
||||
encodeDotInKeys,
|
||||
encoder,
|
||||
filter,
|
||||
sort,
|
||||
allowDots,
|
||||
serializeDate,
|
||||
format,
|
||||
formatter,
|
||||
encodeValuesOnly,
|
||||
charset,
|
||||
sideChannel
|
||||
) {
|
||||
var obj = object;
|
||||
|
||||
var tmpSc = sideChannel;
|
||||
var step = 0;
|
||||
var findFlag = false;
|
||||
while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) {
|
||||
// Where object last appeared in the ref tree
|
||||
var pos = tmpSc.get(object);
|
||||
step += 1;
|
||||
if (typeof pos !== 'undefined') {
|
||||
if (pos === step) {
|
||||
throw new RangeError('Cyclic object value');
|
||||
} else {
|
||||
findFlag = true; // Break while
|
||||
}
|
||||
}
|
||||
if (typeof tmpSc.get(sentinel) === 'undefined') {
|
||||
step = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof filter === 'function') {
|
||||
obj = filter(prefix, obj);
|
||||
} else if (obj instanceof Date) {
|
||||
obj = serializeDate(obj);
|
||||
} else if (generateArrayPrefix === 'comma' && isArray(obj)) {
|
||||
obj = utils.maybeMap(obj, function (value) {
|
||||
if (value instanceof Date) {
|
||||
return serializeDate(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (obj === null) {
|
||||
if (strictNullHandling) {
|
||||
return formatter(encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix);
|
||||
}
|
||||
|
||||
obj = '';
|
||||
}
|
||||
|
||||
if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) {
|
||||
if (encoder) {
|
||||
var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format);
|
||||
return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))];
|
||||
}
|
||||
return [formatter(prefix) + '=' + formatter(String(obj))];
|
||||
}
|
||||
|
||||
var values = [];
|
||||
|
||||
if (typeof obj === 'undefined') {
|
||||
return values;
|
||||
}
|
||||
|
||||
var objKeys;
|
||||
if (generateArrayPrefix === 'comma' && isArray(obj)) {
|
||||
// we need to join elements in
|
||||
if (encodeValuesOnly && 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)) {
|
||||
objKeys = filter;
|
||||
} else {
|
||||
var keys = Object.keys(obj);
|
||||
objKeys = sort ? keys.sort(sort) : keys;
|
||||
}
|
||||
|
||||
var encodedPrefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix);
|
||||
|
||||
var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? encodedPrefix + '[]' : encodedPrefix;
|
||||
|
||||
if (allowEmptyArrays && isArray(obj) && obj.length === 0) {
|
||||
return adjustedPrefix + '[]';
|
||||
}
|
||||
|
||||
for (var j = 0; j < objKeys.length; ++j) {
|
||||
var key = objKeys[j];
|
||||
var value = typeof key === 'object' && key && typeof key.value !== 'undefined'
|
||||
? key.value
|
||||
: obj[key];
|
||||
|
||||
if (skipNulls && value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var encodedKey = allowDots && encodeDotInKeys ? String(key).replace(/\./g, '%2E') : String(key);
|
||||
var keyPrefix = isArray(obj)
|
||||
? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(adjustedPrefix, encodedKey) : adjustedPrefix
|
||||
: adjustedPrefix + (allowDots ? '.' + encodedKey : '[' + encodedKey + ']');
|
||||
|
||||
sideChannel.set(object, step);
|
||||
var valueSideChannel = getSideChannel();
|
||||
valueSideChannel.set(sentinel, sideChannel);
|
||||
pushToArray(values, stringify(
|
||||
value,
|
||||
keyPrefix,
|
||||
generateArrayPrefix,
|
||||
commaRoundTrip,
|
||||
allowEmptyArrays,
|
||||
strictNullHandling,
|
||||
skipNulls,
|
||||
encodeDotInKeys,
|
||||
generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder,
|
||||
filter,
|
||||
sort,
|
||||
allowDots,
|
||||
serializeDate,
|
||||
format,
|
||||
formatter,
|
||||
encodeValuesOnly,
|
||||
charset,
|
||||
valueSideChannel
|
||||
));
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
|
||||
if (!opts) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
|
||||
throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
|
||||
}
|
||||
|
||||
if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') {
|
||||
throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided');
|
||||
}
|
||||
|
||||
if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
|
||||
throw new TypeError('Encoder has to be a function.');
|
||||
}
|
||||
|
||||
var charset = opts.charset || defaults.charset;
|
||||
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
|
||||
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
|
||||
}
|
||||
|
||||
var format = formats['default'];
|
||||
if (typeof opts.format !== 'undefined') {
|
||||
if (!has.call(formats.formatters, opts.format)) {
|
||||
throw new TypeError('Unknown format option provided.');
|
||||
}
|
||||
format = opts.format;
|
||||
}
|
||||
var formatter = formats.formatters[format];
|
||||
|
||||
var filter = defaults.filter;
|
||||
if (typeof opts.filter === 'function' || isArray(opts.filter)) {
|
||||
filter = opts.filter;
|
||||
}
|
||||
|
||||
var arrayFormat;
|
||||
if (opts.arrayFormat in arrayPrefixGenerators) {
|
||||
arrayFormat = opts.arrayFormat;
|
||||
} else if ('indices' in opts) {
|
||||
arrayFormat = opts.indices ? 'indices' : 'repeat';
|
||||
} else {
|
||||
arrayFormat = defaults.arrayFormat;
|
||||
}
|
||||
|
||||
if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
|
||||
throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
|
||||
}
|
||||
|
||||
var allowDots = typeof opts.allowDots === 'undefined' ? opts.encodeDotInKeys === true ? true : defaults.allowDots : !!opts.allowDots;
|
||||
|
||||
return {
|
||||
addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
|
||||
allowDots: allowDots,
|
||||
allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
|
||||
arrayFormat: arrayFormat,
|
||||
charset: charset,
|
||||
charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
|
||||
commaRoundTrip: !!opts.commaRoundTrip,
|
||||
delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
|
||||
encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
|
||||
encodeDotInKeys: typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys,
|
||||
encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
|
||||
encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
|
||||
filter: filter,
|
||||
format: format,
|
||||
formatter: formatter,
|
||||
serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
|
||||
skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
|
||||
sort: typeof opts.sort === 'function' ? opts.sort : null,
|
||||
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = function (object, opts) {
|
||||
var obj = object;
|
||||
var options = normalizeStringifyOptions(opts);
|
||||
|
||||
var objKeys;
|
||||
var filter;
|
||||
|
||||
if (typeof options.filter === 'function') {
|
||||
filter = options.filter;
|
||||
obj = filter('', obj);
|
||||
} else if (isArray(options.filter)) {
|
||||
filter = options.filter;
|
||||
objKeys = filter;
|
||||
}
|
||||
|
||||
var keys = [];
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var generateArrayPrefix = arrayPrefixGenerators[options.arrayFormat];
|
||||
var commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip;
|
||||
|
||||
if (!objKeys) {
|
||||
objKeys = Object.keys(obj);
|
||||
}
|
||||
|
||||
if (options.sort) {
|
||||
objKeys.sort(options.sort);
|
||||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
pushToArray(keys, stringify(
|
||||
value,
|
||||
key,
|
||||
generateArrayPrefix,
|
||||
commaRoundTrip,
|
||||
options.allowEmptyArrays,
|
||||
options.strictNullHandling,
|
||||
options.skipNulls,
|
||||
options.encodeDotInKeys,
|
||||
options.encode ? options.encoder : null,
|
||||
options.filter,
|
||||
options.sort,
|
||||
options.allowDots,
|
||||
options.serializeDate,
|
||||
options.format,
|
||||
options.formatter,
|
||||
options.encodeValuesOnly,
|
||||
options.charset,
|
||||
sideChannel
|
||||
));
|
||||
}
|
||||
|
||||
var joined = keys.join(options.delimiter);
|
||||
var prefix = options.addQueryPrefix === true ? '?' : '';
|
||||
|
||||
if (options.charsetSentinel) {
|
||||
if (options.charset === 'iso-8859-1') {
|
||||
// encodeURIComponent('✓'), the "numeric entity" representation of a checkmark
|
||||
prefix += 'utf8=%26%2310003%3B' + options.delimiter;
|
||||
} else {
|
||||
// encodeURIComponent('✓')
|
||||
prefix += 'utf8=%E2%9C%93' + options.delimiter;
|
||||
}
|
||||
}
|
||||
|
||||
return joined.length > 0 ? prefix + joined : '';
|
||||
};
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
'use strict';
|
||||
|
||||
var formats = require('./formats');
|
||||
var getSideChannel = require('side-channel');
|
||||
var defineProperty = require('es-define-property');
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
var isArray = Array.isArray;
|
||||
|
||||
// Track objects created from arrayLimit overflow using side-channel
|
||||
// Stores the current max numeric index for O(1) lookup
|
||||
var overflowChannel = getSideChannel();
|
||||
|
||||
var markOverflow = function markOverflow(obj, maxIndex) {
|
||||
overflowChannel.set(obj, maxIndex);
|
||||
return obj;
|
||||
};
|
||||
|
||||
var isOverflow = function isOverflow(obj) {
|
||||
return overflowChannel.has(obj);
|
||||
};
|
||||
|
||||
var getMaxIndex = function getMaxIndex(obj) {
|
||||
return overflowChannel.get(obj);
|
||||
};
|
||||
|
||||
var setMaxIndex = function setMaxIndex(obj, maxIndex) {
|
||||
overflowChannel.set(obj, maxIndex);
|
||||
};
|
||||
|
||||
var hexTable = (function () {
|
||||
var array = [];
|
||||
for (var i = 0; i < 256; ++i) {
|
||||
array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
|
||||
}
|
||||
|
||||
return array;
|
||||
}());
|
||||
|
||||
var compactQueue = function compactQueue(queue) {
|
||||
while (queue.length > 1) {
|
||||
var item = queue.pop();
|
||||
var obj = item.obj[item.prop];
|
||||
|
||||
if (isArray(obj)) {
|
||||
var compacted = [];
|
||||
|
||||
for (var j = 0; j < obj.length; ++j) {
|
||||
if (typeof obj[j] !== 'undefined') {
|
||||
compacted[compacted.length] = obj[j];
|
||||
}
|
||||
}
|
||||
|
||||
item.obj[item.prop] = compacted;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var arrayToObject = function arrayToObject(source, options) {
|
||||
var obj = options && options.plainObjects ? { __proto__: null } : {};
|
||||
for (var i = 0; i < source.length; ++i) {
|
||||
if (typeof source[i] !== 'undefined') {
|
||||
obj[i] = source[i];
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return target;
|
||||
}
|
||||
|
||||
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.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;
|
||||
} else if (target && typeof target === 'object') {
|
||||
if (isOverflow(target)) {
|
||||
// Add at next numeric index for overflow objects
|
||||
var newIndex = getMaxIndex(target) + 1;
|
||||
target[newIndex] = source;
|
||||
setMaxIndex(target, newIndex);
|
||||
} else if (options && options.strictMerge) {
|
||||
return [target, source];
|
||||
} else if (
|
||||
(options && (options.plainObjects || options.allowPrototypes))
|
||||
|| !has.call(Object.prototype, source)
|
||||
) {
|
||||
target[source] = true;
|
||||
}
|
||||
} else {
|
||||
return [target, source];
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
if (!target || typeof target !== 'object') {
|
||||
if (isOverflow(source)) {
|
||||
// Create new object with target at 0, source values shifted by 1
|
||||
var sourceKeys = Object.keys(source);
|
||||
var result = options && options.plainObjects
|
||||
? { __proto__: null, 0: target }
|
||||
: { 0: target };
|
||||
for (var m = 0; m < sourceKeys.length; m++) {
|
||||
var oldKey = parseInt(sourceKeys[m], 10);
|
||||
result[oldKey + 1] = source[sourceKeys[m]];
|
||||
}
|
||||
return markOverflow(result, getMaxIndex(source) + 1);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
var mergeTarget = target;
|
||||
if (isArray(target) && !isArray(source)) {
|
||||
mergeTarget = arrayToObject(target, options);
|
||||
}
|
||||
|
||||
if (isArray(target) && isArray(source)) {
|
||||
source.forEach(function (item, i) {
|
||||
if (has.call(target, i)) {
|
||||
var targetItem = target[i];
|
||||
if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
|
||||
target[i] = merge(targetItem, item, options);
|
||||
} else {
|
||||
target[target.length] = item;
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
return Object.keys(source).reduce(function (acc, key) {
|
||||
var value = source[key];
|
||||
|
||||
if (has.call(acc, key)) {
|
||||
setProperty(acc, key, merge(acc[key], value, options));
|
||||
} else {
|
||||
setProperty(acc, key, value);
|
||||
}
|
||||
|
||||
if (isOverflow(source) && !isOverflow(acc)) {
|
||||
markOverflow(acc, getMaxIndex(source));
|
||||
}
|
||||
if (isOverflow(acc)) {
|
||||
var keyNum = parseInt(key, 10);
|
||||
if (String(keyNum) === key && keyNum >= 0 && keyNum > getMaxIndex(acc)) {
|
||||
setMaxIndex(acc, keyNum);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, mergeTarget);
|
||||
};
|
||||
|
||||
var assign = function assignSingleSource(target, source) {
|
||||
return Object.keys(source).reduce(function (acc, key) {
|
||||
setProperty(acc, key, source[key]);
|
||||
return acc;
|
||||
}, target);
|
||||
};
|
||||
|
||||
var decode = function (str, defaultDecoder, charset) {
|
||||
var strWithoutPlus = str.replace(/\+/g, ' ');
|
||||
if (charset === 'iso-8859-1') {
|
||||
// unescape never throws, no try...catch needed:
|
||||
return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape);
|
||||
}
|
||||
// utf-8
|
||||
try {
|
||||
return decodeURIComponent(strWithoutPlus);
|
||||
} catch (e) {
|
||||
return strWithoutPlus;
|
||||
}
|
||||
};
|
||||
|
||||
var limit = 1024;
|
||||
|
||||
/* eslint operator-linebreak: [2, "before"] */
|
||||
|
||||
var encode = function encode(str, defaultEncoder, charset, kind, format) {
|
||||
// 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) {
|
||||
return str;
|
||||
}
|
||||
|
||||
var string = str;
|
||||
if (typeof str === 'symbol') {
|
||||
string = Symbol.prototype.toString.call(str);
|
||||
} else if (typeof str !== 'string') {
|
||||
string = String(str);
|
||||
}
|
||||
|
||||
if (charset === 'iso-8859-1') {
|
||||
return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) {
|
||||
return '%26%23' + parseInt($0.slice(2), 16) + '%3B';
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
var c = segment.charCodeAt(i);
|
||||
if (
|
||||
c === 0x2D // -
|
||||
|| c === 0x2E // .
|
||||
|| c === 0x5F // _
|
||||
|| c === 0x7E // ~
|
||||
|| (c >= 0x30 && c <= 0x39) // 0-9
|
||||
|| (c >= 0x41 && c <= 0x5A) // a-z
|
||||
|| (c >= 0x61 && c <= 0x7A) // A-Z
|
||||
|| (format === formats.RFC1738 && (c === 0x28 || c === 0x29)) // ( )
|
||||
) {
|
||||
arr[arr.length] = segment.charAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c < 0x80) {
|
||||
arr[arr.length] = hexTable[c];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c < 0x800) {
|
||||
arr[arr.length] = hexTable[0xC0 | (c >> 6)]
|
||||
+ hexTable[0x80 | (c & 0x3F)];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c < 0xD800 || c >= 0xE000) {
|
||||
arr[arr.length] = hexTable[0xE0 | (c >> 12)]
|
||||
+ hexTable[0x80 | ((c >> 6) & 0x3F)]
|
||||
+ hexTable[0x80 | (c & 0x3F)];
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
c = 0x10000 + (((c & 0x3FF) << 10) | (segment.charCodeAt(i) & 0x3FF));
|
||||
|
||||
arr[arr.length] = hexTable[0xF0 | (c >> 18)]
|
||||
+ hexTable[0x80 | ((c >> 12) & 0x3F)]
|
||||
+ hexTable[0x80 | ((c >> 6) & 0x3F)]
|
||||
+ hexTable[0x80 | (c & 0x3F)];
|
||||
}
|
||||
|
||||
out += arr.join('');
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
var compact = function compact(value) {
|
||||
var queue = [{ obj: { o: value }, prop: 'o' }];
|
||||
var refs = getSideChannel();
|
||||
|
||||
for (var i = 0; i < queue.length; ++i) {
|
||||
var item = queue[i];
|
||||
var obj = item.obj[item.prop];
|
||||
|
||||
var keys = Object.keys(obj);
|
||||
for (var j = 0; j < keys.length; ++j) {
|
||||
var key = keys[j];
|
||||
var val = obj[key];
|
||||
if (typeof val === 'object' && val !== null && !refs.has(val)) {
|
||||
queue[queue.length] = { obj: obj, prop: key };
|
||||
refs.set(val, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compactQueue(queue);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
var isRegExp = function isRegExp(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object RegExp]';
|
||||
};
|
||||
|
||||
var isBuffer = function isBuffer(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
|
||||
};
|
||||
|
||||
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);
|
||||
return a;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
var maybeMap = function maybeMap(val, fn) {
|
||||
if (isArray(val)) {
|
||||
var mapped = [];
|
||||
for (var i = 0; i < val.length; i += 1) {
|
||||
mapped[mapped.length] = fn(val[i]);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
return fn(val);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
arrayToObject: arrayToObject,
|
||||
assign: assign,
|
||||
combine: combine,
|
||||
compact: compact,
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
isBuffer: isBuffer,
|
||||
isOverflow: isOverflow,
|
||||
isRegExp: isRegExp,
|
||||
markOverflow: markOverflow,
|
||||
maybeMap: maybeMap,
|
||||
merge: merge
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "qs",
|
||||
"description": "A querystring parser that supports nesting and arrays, with a depth limit",
|
||||
"homepage": "https://github.com/ljharb/qs",
|
||||
"version": "6.15.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ljharb/qs.git"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"sideEffects": false,
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Jordan Harband",
|
||||
"email": "ljharb@gmail.com",
|
||||
"url": "http://ljharb.codes"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"querystring",
|
||||
"qs",
|
||||
"query",
|
||||
"url",
|
||||
"parse",
|
||||
"stringify"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"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.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": "^10.5.0",
|
||||
"evalmd": "^0.0.19",
|
||||
"for-each": "^0.3.5",
|
||||
"glob": "=10.3.7",
|
||||
"has-bigints": "^1.1.0",
|
||||
"has-override-mistake": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2",
|
||||
"has-proto": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"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.2",
|
||||
"module-deps": "^6.2.3",
|
||||
"npmignore": "^0.3.5",
|
||||
"nyc": "^10.3.2",
|
||||
"object-inspect": "^1.13.4",
|
||||
"qs-iconv": "^1.0.4",
|
||||
"safe-publish-latest": "^2.0.0",
|
||||
"safer-buffer": "^2.1.2",
|
||||
"tape": "^5.10.2",
|
||||
"unassertify": "^3.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npmignore --auto --commentLines=autogenerated && npm run dist",
|
||||
"prepublishOnly": "safe-publish-latest",
|
||||
"prepublish": "not-in-publish || npm run prepublishOnly",
|
||||
"pretest": "npm run --silent readme && npm run --silent lint",
|
||||
"test": "npm run tests-only",
|
||||
"tests-only": "nyc tape 'test/**/*.js'",
|
||||
"posttest": "npx npm@'>=10.2' audit --production",
|
||||
"readme": "evalmd README.md",
|
||||
"postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)",
|
||||
"lint": "eslint .",
|
||||
"dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"publishConfig": {
|
||||
"ignore": [
|
||||
"!dist/*",
|
||||
"bower.json",
|
||||
"component.json",
|
||||
".github/workflows",
|
||||
"logos",
|
||||
"tea.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
emptyTestCases: [
|
||||
{
|
||||
input: '&',
|
||||
withEmptyKeys: {},
|
||||
stringifyOutput: {
|
||||
brackets: '',
|
||||
indices: '',
|
||||
repeat: ''
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '&&',
|
||||
withEmptyKeys: {},
|
||||
stringifyOutput: {
|
||||
brackets: '',
|
||||
indices: '',
|
||||
repeat: ''
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '&=',
|
||||
withEmptyKeys: { '': '' },
|
||||
stringifyOutput: {
|
||||
brackets: '=',
|
||||
indices: '=',
|
||||
repeat: '='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '&=&',
|
||||
withEmptyKeys: { '': '' },
|
||||
stringifyOutput: {
|
||||
brackets: '=',
|
||||
indices: '=',
|
||||
repeat: '='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '&=&=',
|
||||
withEmptyKeys: { '': ['', ''] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=&[]=',
|
||||
indices: '[0]=&[1]=',
|
||||
repeat: '=&='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '&=&=&',
|
||||
withEmptyKeys: { '': ['', ''] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=&[]=',
|
||||
indices: '[0]=&[1]=',
|
||||
repeat: '=&='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '=',
|
||||
withEmptyKeys: { '': '' },
|
||||
noEmptyKeys: {},
|
||||
stringifyOutput: {
|
||||
brackets: '=',
|
||||
indices: '=',
|
||||
repeat: '='
|
||||
}
|
||||
},
|
||||
{
|
||||
input: '=&',
|
||||
withEmptyKeys: { '': '' },
|
||||
stringifyOutput: {
|
||||
brackets: '=',
|
||||
indices: '=',
|
||||
repeat: '='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '=&&&',
|
||||
withEmptyKeys: { '': '' },
|
||||
stringifyOutput: {
|
||||
brackets: '=',
|
||||
indices: '=',
|
||||
repeat: '='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '=&=&=&',
|
||||
withEmptyKeys: { '': ['', '', ''] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=&[]=&[]=',
|
||||
indices: '[0]=&[1]=&[2]=',
|
||||
repeat: '=&=&='
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '=&a[]=b&a[1]=c',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: '=a',
|
||||
withEmptyKeys: { '': 'a' },
|
||||
noEmptyKeys: {},
|
||||
stringifyOutput: {
|
||||
brackets: '=a',
|
||||
indices: '=a',
|
||||
repeat: '=a'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: 'a==a',
|
||||
withEmptyKeys: { a: '=a' },
|
||||
noEmptyKeys: { a: '=a' },
|
||||
stringifyOutput: {
|
||||
brackets: 'a==a',
|
||||
indices: 'a==a',
|
||||
repeat: 'a==a'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: '=&a[]=b',
|
||||
withEmptyKeys: { '': '', a: ['b'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b',
|
||||
indices: '=&a[0]=b',
|
||||
repeat: '=&a=b'
|
||||
},
|
||||
noEmptyKeys: { a: ['b'] }
|
||||
},
|
||||
{
|
||||
input: '=&a[]=b&a[]=c&a[2]=d',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c', 'd'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c&a[]=d',
|
||||
indices: '=&a[0]=b&a[1]=c&a[2]=d',
|
||||
repeat: '=&a=b&a=c&a=d'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c', 'd'] }
|
||||
},
|
||||
{
|
||||
input: '=a&=b',
|
||||
withEmptyKeys: { '': ['a', 'b'] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=a&[]=b',
|
||||
indices: '[0]=a&[1]=b',
|
||||
repeat: '=a&=b'
|
||||
},
|
||||
noEmptyKeys: {}
|
||||
},
|
||||
{
|
||||
input: '=a&foo=b',
|
||||
withEmptyKeys: { '': 'a', foo: 'b' },
|
||||
noEmptyKeys: { foo: 'b' },
|
||||
stringifyOutput: {
|
||||
brackets: '=a&foo=b',
|
||||
indices: '=a&foo=b',
|
||||
repeat: '=a&foo=b'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: 'a[]=b&a=c&=',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: 'a[]=b&a=c&=',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: 'a[0]=b&a=c&=',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: 'a=b&a[]=c&=',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: 'a=b&a[0]=c&=',
|
||||
withEmptyKeys: { '': '', a: ['b', 'c'] },
|
||||
stringifyOutput: {
|
||||
brackets: '=&a[]=b&a[]=c',
|
||||
indices: '=&a[0]=b&a[1]=c',
|
||||
repeat: '=&a=b&a=c'
|
||||
},
|
||||
noEmptyKeys: { a: ['b', 'c'] }
|
||||
},
|
||||
{
|
||||
input: '[]=a&[]=b& []=1',
|
||||
withEmptyKeys: { '': ['a', 'b'], ' ': ['1'] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=a&[]=b& []=1',
|
||||
indices: '[0]=a&[1]=b& [0]=1',
|
||||
repeat: '=a&=b& =1'
|
||||
},
|
||||
noEmptyKeys: { 0: 'a', 1: 'b', ' ': ['1'] }
|
||||
},
|
||||
{
|
||||
input: '[0]=a&[1]=b&a[0]=1&a[1]=2',
|
||||
withEmptyKeys: { '': ['a', 'b'], a: ['1', '2'] },
|
||||
noEmptyKeys: { 0: 'a', 1: 'b', a: ['1', '2'] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=a&[]=b&a[]=1&a[]=2',
|
||||
indices: '[0]=a&[1]=b&a[0]=1&a[1]=2',
|
||||
repeat: '=a&=b&a=1&a=2'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: '[deep]=a&[deep]=2',
|
||||
withEmptyKeys: { '': { deep: ['a', '2'] }
|
||||
},
|
||||
stringifyOutput: {
|
||||
brackets: '[deep][]=a&[deep][]=2',
|
||||
indices: '[deep][0]=a&[deep][1]=2',
|
||||
repeat: '[deep]=a&[deep]=2'
|
||||
},
|
||||
noEmptyKeys: { deep: ['a', '2'] }
|
||||
},
|
||||
{
|
||||
input: '%5B0%5D=a&%5B1%5D=b',
|
||||
withEmptyKeys: { '': ['a', 'b'] },
|
||||
stringifyOutput: {
|
||||
brackets: '[]=a&[]=b',
|
||||
indices: '[0]=a&[1]=b',
|
||||
repeat: '=a&=b'
|
||||
},
|
||||
noEmptyKeys: { 0: 'a', 1: 'b' }
|
||||
}
|
||||
]
|
||||
};
|
||||
+1933
@@ -0,0 +1,1933 @@
|
||||
'use strict';
|
||||
|
||||
var test = require('tape');
|
||||
var hasPropertyDescriptors = require('has-property-descriptors')();
|
||||
var iconv = require('iconv-lite');
|
||||
var mockProperty = require('mock-property');
|
||||
var hasOverrideMistake = require('has-override-mistake')();
|
||||
var SaferBuffer = require('safer-buffer').Buffer;
|
||||
var v = require('es-value-fixtures');
|
||||
var inspect = require('object-inspect');
|
||||
var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
|
||||
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' });
|
||||
st.deepEqual(qs.parse('foo=c++'), { foo: 'c ' });
|
||||
st.deepEqual(qs.parse('a[>=]=23'), { a: { '>=': '23' } });
|
||||
st.deepEqual(qs.parse('a[<=>]==23'), { a: { '<=>': '=23' } });
|
||||
st.deepEqual(qs.parse('a[==]=23'), { a: { '==': '23' } });
|
||||
st.deepEqual(qs.parse('foo', { strictNullHandling: true }), { foo: null });
|
||||
st.deepEqual(qs.parse('foo'), { foo: '' });
|
||||
st.deepEqual(qs.parse('foo='), { foo: '' });
|
||||
st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' });
|
||||
st.deepEqual(qs.parse(' foo = bar = baz '), { ' foo ': ' bar = baz ' });
|
||||
st.deepEqual(qs.parse('foo=bar=baz'), { foo: 'bar=baz' });
|
||||
st.deepEqual(qs.parse('foo=bar&bar=baz'), { foo: 'bar', bar: 'baz' });
|
||||
st.deepEqual(qs.parse('foo2=bar2&baz2='), { foo2: 'bar2', baz2: '' });
|
||||
st.deepEqual(qs.parse('foo=bar&baz', { strictNullHandling: true }), { foo: 'bar', baz: null });
|
||||
st.deepEqual(qs.parse('foo=bar&baz'), { foo: 'bar', baz: '' });
|
||||
st.deepEqual(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'), {
|
||||
cht: 'p3',
|
||||
chd: 't:60,40',
|
||||
chs: '250x100',
|
||||
chl: 'Hello|World'
|
||||
});
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('comma: false', function (st) {
|
||||
st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a=b,c'), { a: 'b,c' });
|
||||
st.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('comma: true', function (st) {
|
||||
st.deepEqual(qs.parse('a[]=b&a[]=c', { comma: true }), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { comma: true }), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a=b,c', { comma: true }), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a=b&a=c', { comma: true }), { a: ['b', 'c'] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows enabling dot notation', function (st) {
|
||||
st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' });
|
||||
st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('decode dot keys correctly', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: false, decodeDotInKeys: false }),
|
||||
{ 'name%2Eobj.first': 'John', 'name%2Eobj.last': 'Doe' },
|
||||
'with allowDots false and decodeDotInKeys false'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('name.obj.first=John&name.obj.last=Doe', { allowDots: true, decodeDotInKeys: false }),
|
||||
{ name: { obj: { first: 'John', last: 'Doe' } } },
|
||||
'with allowDots false and decodeDotInKeys false'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: true, decodeDotInKeys: false }),
|
||||
{ 'name%2Eobj': { first: 'John', last: 'Doe' } },
|
||||
'with allowDots true and decodeDotInKeys false'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: true, decodeDotInKeys: true }),
|
||||
{ 'name.obj': { first: 'John', last: 'Doe' } },
|
||||
'with allowDots true and decodeDotInKeys true'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
|
||||
{ allowDots: false, decodeDotInKeys: false }
|
||||
),
|
||||
{ 'name%2Eobj%2Esubobject.first%2Egodly%2Ename': 'John', 'name%2Eobj%2Esubobject.last': 'Doe' },
|
||||
'with allowDots false and decodeDotInKeys false'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
|
||||
{ allowDots: true, decodeDotInKeys: false }
|
||||
),
|
||||
{ name: { obj: { subobject: { first: { godly: { name: 'John' } }, last: 'Doe' } } } },
|
||||
'with allowDots true and decodeDotInKeys false'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
|
||||
{ allowDots: true, decodeDotInKeys: true }
|
||||
),
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
'with allowDots true and decodeDotInKeys true'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe'),
|
||||
{ 'name%2Eobj.first': 'John', 'name%2Eobj.last': 'Doe' },
|
||||
'with allowDots and decodeDotInKeys undefined'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('decodes dot in key of object, and allow enabling dot notation when decodeDotInKeys is set to true and allowDots is undefined', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
|
||||
{ decodeDotInKeys: true }
|
||||
),
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
'with allowDots undefined and decodeDotInKeys true'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws when decodeDotInKeys is not of type boolean', function (st) {
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: 'foobar' }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: 0 }); },
|
||||
TypeError
|
||||
);
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: NaN }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: null }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows empty arrays in obj values', function (st) {
|
||||
st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }), { foo: [], bar: 'baz' });
|
||||
st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: false }), { foo: [''], bar: 'baz' });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws when allowEmptyArrays is not of type boolean', function (st) {
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 'foobar' }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 0 }); },
|
||||
TypeError
|
||||
);
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: NaN }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: null }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allowEmptyArrays + strictNullHandling', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('testEmptyArray[]', { strictNullHandling: true, allowEmptyArrays: true }),
|
||||
{ testEmptyArray: [] }
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.deepEqual(qs.parse('a[b]=c'), { a: { b: 'c' } }, 'parses a single nested string');
|
||||
t.deepEqual(qs.parse('a[b][c]=d'), { a: { b: { c: 'd' } } }, 'parses a double nested string');
|
||||
t.deepEqual(
|
||||
qs.parse('a[b][c][d][e][f][g][h]=i'),
|
||||
{ a: { b: { c: { d: { e: { f: { '[g][h]': 'i' } } } } } } },
|
||||
'defaults to a depth of 5'
|
||||
);
|
||||
|
||||
t.test('only parses one level when depth = 1', function (st) {
|
||||
st.deepEqual(qs.parse('a[b][c]=d', { depth: 1 }), { a: { b: { '[c]': 'd' } } });
|
||||
st.deepEqual(qs.parse('a[b][c][d]=e', { depth: 1 }), { a: { b: { '[c][d]': 'e' } } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('uses original key when depth = false', function (st) {
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: false }), { 'a[0]': 'b', 'a[1]': 'c' });
|
||||
st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: false }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] }, 'parses a simple array');
|
||||
|
||||
t.test('parses an explicit array', function (st) {
|
||||
st.deepEqual(qs.parse('a[]=b'), { a: ['b'] });
|
||||
st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[]=b&a[]=c&a[]=d'), { a: ['b', 'c', 'd'] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses a mix of simple and explicit arrays', function (st) {
|
||||
st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[0]=b&a=c'), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a=b&a[0]=c'), { a: ['b', 'c'] });
|
||||
|
||||
st.deepEqual(qs.parse('a[1]=b&a=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a[]=b&a=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
|
||||
st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
|
||||
|
||||
st.deepEqual(qs.parse('a=b&a[1]=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
|
||||
st.deepEqual(qs.parse('a=b&a[]=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
|
||||
st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses a nested array', function (st) {
|
||||
st.deepEqual(qs.parse('a[b][]=c&a[b][]=d'), { a: { b: ['c', 'd'] } });
|
||||
st.deepEqual(qs.parse('a[>=]=25'), { a: { '>=': '25' } });
|
||||
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'] });
|
||||
st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 20 }), { a: ['c'] });
|
||||
st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 0 }), { a: { 1: 'c' } });
|
||||
st.deepEqual(qs.parse('a[1]=c'), { a: ['c'] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('limits specific array indices to arrayLimit', function (st) {
|
||||
st.deepEqual(qs.parse('a[19]=a', { arrayLimit: 20 }), { a: ['a'] });
|
||||
st.deepEqual(qs.parse('a[20]=a', { arrayLimit: 20 }), { a: { 20: 'a' } });
|
||||
|
||||
st.deepEqual(qs.parse('a[19]=a'), { a: ['a'] });
|
||||
st.deepEqual(qs.parse('a[20]=a'), { a: { 20: 'a' } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.deepEqual(qs.parse('a[12b]=c'), { a: { '12b': 'c' } }, 'supports keys that begin with a number');
|
||||
|
||||
t.test('supports encoded = signs', function (st) {
|
||||
st.deepEqual(qs.parse('he%3Dllo=th%3Dere'), { 'he=llo': 'th=ere' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('is ok with url encoded strings', function (st) {
|
||||
st.deepEqual(qs.parse('a[b%20c]=d'), { a: { 'b c': 'd' } });
|
||||
st.deepEqual(qs.parse('a[b]=c%20d'), { a: { b: 'c d' } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows brackets in the value', function (st) {
|
||||
st.deepEqual(qs.parse('pets=["tobi"]'), { pets: '["tobi"]' });
|
||||
st.deepEqual(qs.parse('operators=[">=", "<="]'), { operators: '[">=", "<="]' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows empty values', function (st) {
|
||||
st.deepEqual(qs.parse(''), {});
|
||||
st.deepEqual(qs.parse(null), {});
|
||||
st.deepEqual(qs.parse(undefined), {});
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('transforms arrays to objects', function (st) {
|
||||
st.deepEqual(qs.parse('foo[0]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
|
||||
st.deepEqual(qs.parse('foo[bad]=baz&foo[0]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
|
||||
st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
|
||||
st.deepEqual(qs.parse('foo[]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
|
||||
st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar&foo[]=foo'), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
|
||||
st.deepEqual(qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb'), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
|
||||
|
||||
st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: false }), { a: { 0: 'b', t: 'u' } });
|
||||
st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: true }), { a: { 0: 'b', t: 'u', hasOwnProperty: 'c' } });
|
||||
st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: false }), { a: { 0: 'b', x: 'y' } });
|
||||
st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: true }), { a: { 0: 'b', hasOwnProperty: 'c', x: 'y' } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('transforms arrays to objects (dot notation)', function (st) {
|
||||
st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: 'baz' } });
|
||||
st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad.boo=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: { boo: 'baz' } } });
|
||||
st.deepEqual(qs.parse('foo[0][0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [[{ baz: 'bar' }]], fool: { bad: 'baz' } });
|
||||
st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15'], bar: '2' }] });
|
||||
st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].baz[1]=16&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15', '16'], bar: '2' }] });
|
||||
st.deepEqual(qs.parse('foo.bad=baz&foo[0]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
|
||||
st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
|
||||
st.deepEqual(qs.parse('foo[]=bar&foo.bad=baz', { allowDots: true }), { foo: { 0: 'bar', bad: 'baz' } });
|
||||
st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar&foo[]=foo', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
|
||||
st.deepEqual(qs.parse('foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb', { allowDots: true }), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('correctly prunes undefined values when converting an array to an object', function (st) {
|
||||
st.deepEqual(qs.parse('a[2]=b&a[99999999]=c'), { a: { 2: 'b', 99999999: 'c' } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('supports malformed uri characters', function (st) {
|
||||
st.deepEqual(qs.parse('{%:%}', { strictNullHandling: true }), { '{%:%}': null });
|
||||
st.deepEqual(qs.parse('{%:%}='), { '{%:%}': '' });
|
||||
st.deepEqual(qs.parse('foo=%:%}'), { foo: '%:%}' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('doesn\'t produce empty keys', function (st) {
|
||||
st.deepEqual(qs.parse('_r=1&'), { _r: '1' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('cannot access Object prototype', function (st) {
|
||||
qs.parse('constructor[prototype][bad]=bad');
|
||||
qs.parse('bad[constructor][prototype][bad]=bad');
|
||||
st.equal(typeof Object.prototype.bad, 'undefined');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses arrays of objects', function (st) {
|
||||
st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
|
||||
st.deepEqual(qs.parse('a[0][b]=c'), { a: [{ b: 'c' }] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows for empty strings in arrays', function (st) {
|
||||
st.deepEqual(qs.parse('a[]=b&a[]=&a[]=c'), { a: ['b', '', 'c'] });
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { strictNullHandling: true, arrayLimit: 20 }),
|
||||
{ a: ['b', null, 'c', ''] },
|
||||
'with arrayLimit 20 + array indices: null then empty string works'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[]&a[]=c&a[]=', { strictNullHandling: true, arrayLimit: 0 }),
|
||||
{ a: { 0: 'b', 1: null, 2: 'c', 3: '' } },
|
||||
'with arrayLimit 0 + array brackets: null then empty string works'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true, arrayLimit: 20 }),
|
||||
{ a: ['b', '', 'c', null] },
|
||||
'with arrayLimit 20 + array indices: empty string then null works'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[]=&a[]=c&a[]', { strictNullHandling: true, arrayLimit: 0 }),
|
||||
{ a: { 0: 'b', 1: '', 2: 'c', 3: null } },
|
||||
'with arrayLimit 0 + array brackets: empty string then null works'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=&a[]=b&a[]=c'),
|
||||
{ a: ['', 'b', 'c'] },
|
||||
'array brackets: empty strings work'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('compacts sparse arrays', function (st) {
|
||||
st.deepEqual(qs.parse('a[10]=1&a[2]=2', { arrayLimit: 20 }), { a: ['2', '1'] });
|
||||
st.deepEqual(qs.parse('a[1][b][2][c]=1', { arrayLimit: 20 }), { a: [{ b: [{ c: '1' }] }] });
|
||||
st.deepEqual(qs.parse('a[1][2][3][c]=1', { arrayLimit: 20 }), { a: [[[{ c: '1' }]]] });
|
||||
st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { arrayLimit: 20 }), { a: [[[{ c: ['1'] }]]] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses sparse arrays', function (st) {
|
||||
/* eslint no-sparse-arrays: 0 */
|
||||
st.deepEqual(qs.parse('a[4]=1&a[1]=2', { allowSparse: true }), { a: [, '2', , , '1'] });
|
||||
st.deepEqual(qs.parse('a[1][b][2][c]=1', { allowSparse: true }), { a: [, { b: [, , { c: '1' }] }] });
|
||||
st.deepEqual(qs.parse('a[1][2][3][c]=1', { allowSparse: true }), { a: [, [, , [, , , { c: '1' }]]] });
|
||||
st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { allowSparse: true }), { a: [, [, , [, , , { c: [, '1'] }]]] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses semi-parsed strings', function (st) {
|
||||
st.deepEqual(qs.parse({ 'a[b]': 'c' }), { a: { b: 'c' } });
|
||||
st.deepEqual(qs.parse({ 'a[b]': 'c', 'a[d]': 'e' }), { a: { b: 'c', d: 'e' } });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses buffers correctly', function (st) {
|
||||
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' });
|
||||
st.deepEqual(qs.parse('[foo]=bar'), { foo: 'bar' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not error when parsing a very long array', function (st) {
|
||||
var str = 'a[]=a';
|
||||
while (Buffer.byteLength(str) < 128 * 1024) {
|
||||
str = str + '&' + str;
|
||||
}
|
||||
|
||||
st.doesNotThrow(function () {
|
||||
qs.parse(str);
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not throw when a native prototype has an enumerable property', function (st) {
|
||||
st.intercept(Object.prototype, 'crash', { value: '' });
|
||||
st.intercept(Array.prototype, 'crash', { value: '' });
|
||||
|
||||
st.doesNotThrow(qs.parse.bind(null, 'a=b'));
|
||||
st.deepEqual(qs.parse('a=b'), { a: 'b' });
|
||||
st.doesNotThrow(qs.parse.bind(null, 'a[][b]=c'));
|
||||
st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses a string with an alternative string delimiter', function (st) {
|
||||
st.deepEqual(qs.parse('a=b;c=d', { delimiter: ';' }), { a: 'b', c: 'd' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses a string with an alternative RegExp delimiter', function (st) {
|
||||
st.deepEqual(qs.parse('a=b; c=d', { delimiter: /[;,] */ }), { a: 'b', c: 'd' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not use non-splittable objects as delimiters', function (st) {
|
||||
st.deepEqual(qs.parse('a=b&c=d', { delimiter: true }), { a: 'b', c: 'd' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows overriding parameter limit', function (st) {
|
||||
st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: 1 }), { a: 'b' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows setting the parameter limit to Infinity', function (st) {
|
||||
st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: Infinity }), { a: 'b', c: 'd' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows overriding array limit', function (st) {
|
||||
st.deepEqual(qs.parse('a[0]=b', { arrayLimit: -1 }), { a: { 0: 'b' } });
|
||||
st.deepEqual(qs.parse('a[0]=b', { arrayLimit: 0 }), { a: { 0: 'b' } });
|
||||
|
||||
st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: -1 }), { a: { '-1': 'b' } });
|
||||
st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: 0 }), { a: { '-1': 'b' } });
|
||||
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: -1 }), { a: { 0: 'b', 1: 'c' } });
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows disabling array parsing', function (st) {
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('allows for query string prefix', function (st) {
|
||||
st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
|
||||
st.deepEqual(qs.parse('foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
|
||||
st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: false }), { '?foo': 'bar' });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses an object', function (st) {
|
||||
var input = {
|
||||
'user[name]': { 'pop[bob]': 3 },
|
||||
'user[email]': null
|
||||
};
|
||||
|
||||
var expected = {
|
||||
user: {
|
||||
name: { 'pop[bob]': 3 },
|
||||
email: null
|
||||
}
|
||||
};
|
||||
|
||||
var result = qs.parse(input);
|
||||
|
||||
st.deepEqual(result, expected);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses string with comma as array divider', function (st) {
|
||||
st.deepEqual(qs.parse('foo=bar,tee', { comma: true }), { foo: ['bar', 'tee'] });
|
||||
st.deepEqual(qs.parse('foo[bar]=coffee,tee', { comma: true }), { foo: { bar: ['coffee', 'tee'] } });
|
||||
st.deepEqual(qs.parse('foo=', { comma: true }), { foo: '' });
|
||||
st.deepEqual(qs.parse('foo', { comma: true }), { foo: '' });
|
||||
st.deepEqual(qs.parse('foo', { comma: true, strictNullHandling: true }), { foo: null });
|
||||
|
||||
// test cases inversed from from stringify tests
|
||||
st.deepEqual(qs.parse('a[0]=c'), { a: ['c'] });
|
||||
st.deepEqual(qs.parse('a[]=c'), { a: ['c'] });
|
||||
st.deepEqual(qs.parse('a[]=c', { comma: true }), { a: ['c'] });
|
||||
|
||||
st.deepEqual(qs.parse('a[0]=c&a[1]=d'), { a: ['c', 'd'] });
|
||||
st.deepEqual(qs.parse('a[]=c&a[]=d'), { a: ['c', 'd'] });
|
||||
st.deepEqual(qs.parse('a=c,d', { comma: true }), { a: ['c', 'd'] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses values with comma as array divider', function (st) {
|
||||
st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: false }), { foo: 'bar,tee' });
|
||||
st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: true }), { foo: ['bar', 'tee'] });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('use number decoder, parses string that has one number with comma option enabled', function (st) {
|
||||
var decoder = function (str, defaultDecoder, charset, type) {
|
||||
if (!isNaN(Number(str))) {
|
||||
return parseFloat(str);
|
||||
}
|
||||
return defaultDecoder(str, defaultDecoder, charset, type);
|
||||
};
|
||||
|
||||
st.deepEqual(qs.parse('foo=1', { comma: true, decoder: decoder }), { foo: 1 });
|
||||
st.deepEqual(qs.parse('foo=0', { comma: true, decoder: decoder }), { foo: 0 });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses brackets holds array of arrays when having two parts of strings with comma as array divider', function (st) {
|
||||
st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=4,5,6', { comma: true }), { foo: [['1', '2', '3'], ['4', '5', '6']] });
|
||||
st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=', { comma: true }), { foo: [['1', '2', '3'], ''] });
|
||||
st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=,', { comma: true }), { foo: [['1', '2', '3'], ['', '']] });
|
||||
st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=a', { comma: true }), { foo: [['1', '2', '3'], 'a'] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses url-encoded brackets holds array of arrays when having two parts of strings with comma as array divider', function (st) {
|
||||
st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=4,5,6', { comma: true }), { foo: [['1', '2', '3'], ['4', '5', '6']] });
|
||||
st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=', { comma: true }), { foo: [['1', '2', '3'], ''] });
|
||||
st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=,', { comma: true }), { foo: [['1', '2', '3'], ['', '']] });
|
||||
st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=a', { comma: true }), { foo: [['1', '2', '3'], 'a'] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses comma delimited array while having percent-encoded comma treated as normal text', function (st) {
|
||||
st.deepEqual(qs.parse('foo=a%2Cb', { comma: true }), { foo: 'a,b' });
|
||||
st.deepEqual(qs.parse('foo=a%2C%20b,d', { comma: true }), { foo: ['a, b', 'd'] });
|
||||
st.deepEqual(qs.parse('foo=a%2C%20b,c%2C%20d', { comma: true }), { foo: ['a, b', 'c, d'] });
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses an object in dot notation', function (st) {
|
||||
var input = {
|
||||
'user.name': { 'pop[bob]': 3 },
|
||||
'user.email.': null
|
||||
};
|
||||
|
||||
var expected = {
|
||||
user: {
|
||||
name: { 'pop[bob]': 3 },
|
||||
email: null
|
||||
}
|
||||
};
|
||||
|
||||
var result = qs.parse(input, { allowDots: true });
|
||||
|
||||
st.deepEqual(result, expected);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses an object and not child values', function (st) {
|
||||
var input = {
|
||||
'user[name]': { 'pop[bob]': { test: 3 } },
|
||||
'user[email]': null
|
||||
};
|
||||
|
||||
var expected = {
|
||||
user: {
|
||||
name: { 'pop[bob]': { test: 3 } },
|
||||
email: null
|
||||
}
|
||||
};
|
||||
|
||||
var result = qs.parse(input);
|
||||
|
||||
st.deepEqual(result, expected);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not blow up when Buffer global is missing', function (st) {
|
||||
var restore = mockProperty(global, 'Buffer', { 'delete': true });
|
||||
|
||||
var result = qs.parse('a=b&c=d');
|
||||
|
||||
restore();
|
||||
|
||||
st.deepEqual(result, { a: 'b', c: 'd' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash when parsing circular references', function (st) {
|
||||
var a = {};
|
||||
a.b = a;
|
||||
|
||||
var parsed;
|
||||
|
||||
st.doesNotThrow(function () {
|
||||
parsed = qs.parse({ 'foo[bar]': 'baz', 'foo[baz]': a });
|
||||
});
|
||||
|
||||
st.equal('foo' in parsed, true, 'parsed has "foo" property');
|
||||
st.equal('bar' in parsed.foo, true);
|
||||
st.equal('baz' in parsed.foo, true);
|
||||
st.equal(parsed.foo.bar, 'baz');
|
||||
st.deepEqual(parsed.foo.baz, a);
|
||||
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';
|
||||
|
||||
for (var i = 0; i < 5000; i++) {
|
||||
str += '[p]';
|
||||
}
|
||||
|
||||
str += '=bar';
|
||||
|
||||
st.doesNotThrow(function () {
|
||||
parsed = qs.parse(str, { depth: 5000 });
|
||||
});
|
||||
|
||||
st.equal('foo' in parsed, true, 'parsed has "foo" property');
|
||||
|
||||
var depth = 0;
|
||||
var ref = parsed.foo;
|
||||
while ((ref = ref.p)) {
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
st.equal(depth, 5000, 'parsed is 5000 properties deep');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses null objects correctly', { skip: !hasProto }, function (st) {
|
||||
var a = { __proto__: null, b: 'c' };
|
||||
|
||||
st.deepEqual(qs.parse(a), { b: 'c' });
|
||||
var result = qs.parse({ a: a });
|
||||
st.equal('a' in result, true, 'result has "a" property');
|
||||
st.deepEqual(result.a, a);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses dates correctly', function (st) {
|
||||
var now = new Date();
|
||||
st.deepEqual(qs.parse({ a: now }), { a: now });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses regular expressions correctly', function (st) {
|
||||
var re = /^test$/;
|
||||
st.deepEqual(qs.parse({ a: re }), { a: re });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not allow overwriting prototype properties', function (st) {
|
||||
st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: false }), {});
|
||||
st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: false }), {});
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('toString', { allowPrototypes: false }),
|
||||
{},
|
||||
'bare "toString" results in {}'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can allow overwriting prototype properties', function (st) {
|
||||
st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true }), { a: { hasOwnProperty: 'b' } });
|
||||
st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: true }), { hasOwnProperty: 'b' });
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('toString', { allowPrototypes: true }),
|
||||
{ toString: '' },
|
||||
'bare "toString" results in { toString: "" }'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash when the global Object prototype is frozen', { skip: !hasPropertyDescriptors || !hasOverrideMistake }, function (st) {
|
||||
// We can't actually freeze the global Object prototype as that will interfere with other tests, and once an object is frozen, it
|
||||
// can't be unfrozen. Instead, we add a new non-writable property to simulate this.
|
||||
st.teardown(mockProperty(Object.prototype, 'frozenProp', { value: 'foo', nonWritable: true, nonEnumerable: true }));
|
||||
|
||||
st['throws'](
|
||||
function () {
|
||||
var obj = {};
|
||||
obj.frozenProp = 'bar';
|
||||
},
|
||||
// node < 6 has a different error message
|
||||
/^TypeError: Cannot assign to read only property 'frozenProp' of (?:object '#<Object>'|#<Object>)/,
|
||||
'regular assignment of an inherited non-writable property throws'
|
||||
);
|
||||
|
||||
var parsed;
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
parsed = qs.parse('frozenProp', { allowPrototypes: false });
|
||||
},
|
||||
'parsing a nonwritable Object.prototype property does not throw'
|
||||
);
|
||||
|
||||
st.deepEqual(parsed, {}, 'bare "frozenProp" results in {}');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('params starting with a closing bracket', function (st) {
|
||||
st.deepEqual(qs.parse(']=toString'), { ']': 'toString' });
|
||||
st.deepEqual(qs.parse(']]=toString'), { ']]': 'toString' });
|
||||
st.deepEqual(qs.parse(']hello]=toString'), { ']hello]': 'toString' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('params starting with a starting bracket', function (st) {
|
||||
st.deepEqual(qs.parse('[=toString'), { '[': 'toString' });
|
||||
st.deepEqual(qs.parse('[[=toString'), { '[[': 'toString' });
|
||||
st.deepEqual(qs.parse('[hello[=toString'), { '[hello[': 'toString' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('add keys to objects', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=d', { strictMerge: false }),
|
||||
{ a: { b: 'c', d: true } },
|
||||
'can add keys to objects'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=toString', { strictMerge: false }),
|
||||
{ a: { b: 'c' } },
|
||||
'can not overwrite prototype'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=toString', { strictMerge: false, allowPrototypes: true }),
|
||||
{ a: { b: 'c', toString: true } },
|
||||
'can overwrite prototype with allowPrototypes true'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=toString', { strictMerge: false, plainObjects: true }),
|
||||
{ __proto__: null, a: { __proto__: null, b: 'c', toString: true } },
|
||||
'can overwrite prototype with plainObjects true'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('strictMerge wraps object and primitive into an array', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=d'),
|
||||
{ a: [{ b: 'c' }, 'd'] },
|
||||
'object then primitive produces array'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a=d&a[b]=c'),
|
||||
{ a: ['d', { b: 'c' }] },
|
||||
'primitive then object produces array'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=toString'),
|
||||
{ a: [{ b: 'c' }, 'toString'] },
|
||||
'prototype-colliding value is preserved in array'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[b]=c&a=toString', { plainObjects: true }),
|
||||
{ __proto__: null, a: [{ __proto__: null, b: 'c' }, 'toString'] },
|
||||
'plainObjects preserved in array wrapping'
|
||||
);
|
||||
|
||||
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('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,
|
||||
a: {
|
||||
__proto__: null,
|
||||
b: 'c',
|
||||
hasOwnProperty: 'd'
|
||||
}
|
||||
};
|
||||
st.deepEqual(qs.parse('a[b]=c&a[hasOwnProperty]=d', { plainObjects: true }), expected);
|
||||
st.deepEqual(qs.parse(null, { plainObjects: true }), { __proto__: null });
|
||||
var expectedArray = {
|
||||
__proto__: null,
|
||||
a: {
|
||||
__proto__: null,
|
||||
0: 'b',
|
||||
c: 'd'
|
||||
}
|
||||
};
|
||||
st.deepEqual(qs.parse('a[]=b&a[c]=d', { plainObjects: true }), expectedArray);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can parse with custom encoding', function (st) {
|
||||
st.deepEqual(qs.parse('%8c%a7=%91%e5%8d%e3%95%7b', {
|
||||
decoder: function (str) {
|
||||
var reg = /%([0-9A-F]{2})/ig;
|
||||
var result = [];
|
||||
var parts = reg.exec(str);
|
||||
while (parts) {
|
||||
result.push(parseInt(parts[1], 16));
|
||||
parts = reg.exec(str);
|
||||
}
|
||||
return String(iconv.decode(SaferBuffer.from(result), 'shift_jis'));
|
||||
}
|
||||
}), { 県: '大阪府' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('receives the default decoder as a second argument', function (st) {
|
||||
st.plan(1);
|
||||
qs.parse('a', {
|
||||
decoder: function (str, defaultDecoder) {
|
||||
st.equal(defaultDecoder, utils.decode);
|
||||
}
|
||||
});
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws error with wrong decoder', function (st) {
|
||||
st['throws'](function () {
|
||||
qs.parse({}, { decoder: 'string' });
|
||||
}, new TypeError('Decoder has to be a function.'));
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not mutate the options argument', function (st) {
|
||||
var options = {};
|
||||
qs.parse('a[b]=true', options);
|
||||
st.deepEqual(options, {});
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws if an invalid charset is specified', function (st) {
|
||||
st['throws'](function () {
|
||||
qs.parse('a=b', { charset: 'foobar' });
|
||||
}, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses an iso-8859-1 string if asked to', function (st) {
|
||||
st.deepEqual(qs.parse('%A2=%BD', { charset: 'iso-8859-1' }), { '¢': '½' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
var urlEncodedCheckmarkInUtf8 = '%E2%9C%93';
|
||||
var urlEncodedOSlashInUtf8 = '%C3%B8';
|
||||
var urlEncodedNumCheckmark = '%26%2310003%3B';
|
||||
var urlEncodedNumSmiley = '%26%239786%3B';
|
||||
|
||||
t.test('prefers an utf-8 charset specified by the utf8 sentinel to a default charset of iso-8859-1', function (st) {
|
||||
st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'iso-8859-1' }), { ø: 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('prefers an iso-8859-1 charset specified by the utf8 sentinel to a default charset of utf-8', function (st) {
|
||||
st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { 'ø': 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not require the utf8 sentinel to be defined before the parameters whose decoding it affects', function (st) {
|
||||
st.deepEqual(qs.parse('a=' + urlEncodedOSlashInUtf8 + '&utf8=' + urlEncodedNumCheckmark, { charsetSentinel: true, charset: 'utf-8' }), { a: 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('ignores an utf8 sentinel with an unknown value', function (st) {
|
||||
st.deepEqual(qs.parse('utf8=foo&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { ø: 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses the utf8 sentinel to switch to utf-8 when no default charset is given', function (st) {
|
||||
st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { ø: 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses the utf8 sentinel to switch to iso-8859-1 when no default charset is given', function (st) {
|
||||
st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { 'ø': 'ø' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('interprets numeric entities in iso-8859-1 when `interpretNumericEntities`', function (st) {
|
||||
st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1', interpretNumericEntities: true }), { foo: '☺' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('handles a custom decoder returning `null`, in the `iso-8859-1` charset, when `interpretNumericEntities`', function (st) {
|
||||
st.deepEqual(qs.parse('foo=&bar=' + urlEncodedNumSmiley, {
|
||||
charset: 'iso-8859-1',
|
||||
decoder: function (str, defaultDecoder, charset) {
|
||||
return str ? defaultDecoder(str, defaultDecoder, charset) : null;
|
||||
},
|
||||
interpretNumericEntities: true
|
||||
}), { foo: null, bar: '☺' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('handles a custom decoder returning `null`, with a string key of `null`', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('null=1&ToNull=2', {
|
||||
decoder: function (str, defaultDecoder, charset) {
|
||||
return str === 'ToNull' ? null : defaultDecoder(str, defaultDecoder, charset);
|
||||
}
|
||||
}),
|
||||
{ 'null': '1' },
|
||||
'"null" key is not overridden by `null` decoder result'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not interpret numeric entities in iso-8859-1 when `interpretNumericEntities` is absent', function (st) {
|
||||
st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1' }), { foo: '☺' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not interpret numeric entities when the charset is utf-8, even when `interpretNumericEntities`', function (st) {
|
||||
st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'utf-8', interpretNumericEntities: true }), { foo: '☺' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('interpretNumericEntities with comma:true and iso charset does not crash', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('b&a[]=1,' + urlEncodedNumSmiley, { comma: true, charset: 'iso-8859-1', interpretNumericEntities: true }),
|
||||
{ b: '', a: ['1,☺'] }
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not interpret %uXXXX syntax in iso-8859-1 mode', function (st) {
|
||||
st.deepEqual(qs.parse('%u263A=%u263A', { charset: 'iso-8859-1' }), { '%u263A': '%u263A' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows for decoding keys and values differently', function (st) {
|
||||
var decoder = function (str, defaultDecoder, charset, type) {
|
||||
if (type === 'key') {
|
||||
return defaultDecoder(str, defaultDecoder, charset, type).toLowerCase();
|
||||
}
|
||||
if (type === 'value') {
|
||||
return defaultDecoder(str, defaultDecoder, charset, type).toUpperCase();
|
||||
}
|
||||
throw 'this should never happen! type: ' + type;
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('parameter limit tests', function (st) {
|
||||
st.test('does not throw error when within parameter limit', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3', { parameterLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses without errors');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws error when throwOnLimitExceeded is present but not boolean', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: 'true' });
|
||||
},
|
||||
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
|
||||
'throws error when throwOnLimitExceeded is present and not boolean'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws error when parameter limit exceeded', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: true });
|
||||
},
|
||||
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();
|
||||
});
|
||||
|
||||
st.test('silently truncates when throwOnLimitExceeded is not given', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3 });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('silently truncates when parameter limit exceeded without error', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3, throwOnLimitExceeded: false });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('allows unlimited parameters when parameterLimit set to Infinity', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation');
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('array limit tests', function (st) {
|
||||
st.test('does not throw error when array is within limit', function (sst) {
|
||||
var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: ['1', '2', '3'] }, 'parses array without errors');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws error when throwOnLimitExceeded is present but not boolean for array limit', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: 'true' });
|
||||
},
|
||||
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
|
||||
'throws error when throwOnLimitExceeded is present and not boolean for array limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws error when array limit exceeded', function (sst) {
|
||||
// 4 elements exceeds limit of 3
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
|
||||
'throws error when array limit is exceeded'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw when at limit', function (sst) {
|
||||
// 3 elements = limit of 3, should not throw
|
||||
var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 3, throwOnLimitExceeded: true });
|
||||
sst.ok(Array.isArray(result.a), 'result is an array');
|
||||
sst.deepEqual(result.a, ['1', '2', '3'], 'all values present');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('converts array to object if length is greater than limit', function (sst) {
|
||||
var result = qs.parse('a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6', { arrayLimit: 5 });
|
||||
|
||||
sst.deepEqual(result, { a: { 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6' } }, 'parses into object if array length is greater than limit');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws error when indexed notation exceeds arrayLimit with throwOnLimitExceeded', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[1001]=b', { arrayLimit: 1000, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1000 elements allowed in an array.'),
|
||||
'throws error for a single index exceeding arrayLimit'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[0]=1&a[1]=2&a[2]=3&a[10]=4', { arrayLimit: 6, throwOnLimitExceeded: true, allowSparse: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 6 elements allowed in an array.'),
|
||||
'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();
|
||||
});
|
||||
|
||||
st.test('does not throw for indexed notation within arrayLimit with throwOnLimitExceeded', function (sst) {
|
||||
var result = qs.parse('a[4]=b', { arrayLimit: 5, throwOnLimitExceeded: true, allowSparse: true });
|
||||
sst.ok(Array.isArray(result.a), 'result is an array');
|
||||
sst.equal(result.a.length, 5, 'array has correct length');
|
||||
sst.equal(result.a[4], 'b', 'value at index 4 is correct');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('silently converts to object for indexed notation exceeding arrayLimit without throwOnLimitExceeded', function (sst) {
|
||||
var result = qs.parse('a[1001]=b', { arrayLimit: 1000 });
|
||||
sst.deepEqual(result, { a: { 1001: 'b' } }, 'converts to object without throwing');
|
||||
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();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('parses empty keys', function (t) {
|
||||
emptyTestCases.forEach(function (testCase) {
|
||||
t.test('skips empty string key with ' + testCase.input, function (st) {
|
||||
st.deepEqual(qs.parse(testCase.input), testCase.noEmptyKeys);
|
||||
|
||||
st.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('`duplicates` option', function (t) {
|
||||
v.nonStrings.concat('not a valid option').forEach(function (invalidOption) {
|
||||
if (typeof invalidOption !== 'undefined') {
|
||||
t['throws'](
|
||||
function () { qs.parse('', { duplicates: invalidOption }); },
|
||||
TypeError,
|
||||
'throws on invalid option: ' + inspect(invalidOption)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
qs.parse('foo=bar&foo=baz'),
|
||||
{ foo: ['bar', 'baz'] },
|
||||
'duplicates: default, combine'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }),
|
||||
{ foo: ['bar', 'baz'] },
|
||||
'duplicates: combine'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
qs.parse('foo=bar&foo=baz', { duplicates: 'first' }),
|
||||
{ foo: 'bar' },
|
||||
'duplicates: first'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
qs.parse('foo=bar&foo=baz', { duplicates: 'last' }),
|
||||
{ foo: 'baz' },
|
||||
'duplicates: last'
|
||||
);
|
||||
|
||||
t.test('bracket notation always combines regardless of duplicates', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'last' }),
|
||||
{ a: '2', b: ['1', '2'] },
|
||||
'duplicates last: unbracketed takes last, bracketed combines'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('b[]=1&b[]=2', { duplicates: 'last' }),
|
||||
{ b: ['1', '2'] },
|
||||
'duplicates last: bracketed always combines'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('b[]=1&b[]=2', { duplicates: 'first' }),
|
||||
{ b: ['1', '2'] },
|
||||
'duplicates first: bracketed always combines'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'first' }),
|
||||
{ a: '1', b: ['1', '2'] },
|
||||
'duplicates first: unbracketed takes first, bracketed combines'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('qs strictDepth option - throw cases', function (t) {
|
||||
t.test('throws an exception when depth exceeds the limit with strictDepth: true', function (st) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
|
||||
},
|
||||
RangeError,
|
||||
'throws RangeError'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws an exception for multiple nested arrays with strictDepth: true', function (st) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true });
|
||||
},
|
||||
RangeError,
|
||||
'throws RangeError'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws an exception for nested objects and arrays with strictDepth: true', function (st) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true });
|
||||
},
|
||||
RangeError,
|
||||
'throws RangeError'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws an exception for different types of values with strictDepth: true', function (st) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true });
|
||||
},
|
||||
RangeError,
|
||||
'throws RangeError'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('qs strictDepth option - non-throw cases', function (t) {
|
||||
t.test('when depth is 0 and strictDepth true, do not throw', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true });
|
||||
},
|
||||
RangeError,
|
||||
'does not throw RangeError'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses successfully when depth is within the limit with strictDepth: true', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true });
|
||||
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
|
||||
}
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not throw an exception when depth exceeds the limit with strictDepth: false', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
|
||||
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'parses with depth limit');
|
||||
}
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses successfully when depth is within the limit with strictDepth: false', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
var result = qs.parse('a[b]=c', { depth: 1 });
|
||||
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
|
||||
}
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not throw when depth is exactly at the limit with strictDepth: true', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () {
|
||||
var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true });
|
||||
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'parses correctly');
|
||||
}
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('DOS', function (t) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < 105; i++) {
|
||||
arr[arr.length] = 'x';
|
||||
}
|
||||
var attack = 'a[]=' + arr.join('&a[]=');
|
||||
var result = qs.parse(attack, { arrayLimit: 100 });
|
||||
|
||||
t.notOk(Array.isArray(result.a), 'arrayLimit is respected: result is an object, not an array');
|
||||
t.equal(Object.keys(result.a).length, 105, 'all values are preserved');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('arrayLimit boundary conditions', function (t) {
|
||||
// arrayLimit is the max number of elements allowed in an array
|
||||
t.test('exactly at the limit stays as array', function (st) {
|
||||
// 3 elements = limit of 3
|
||||
var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 3 });
|
||||
st.ok(Array.isArray(result.a), 'result is an array when count equals limit');
|
||||
st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('one over the limit converts to object', function (st) {
|
||||
// 4 elements exceeds limit of 3
|
||||
var result = qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3 });
|
||||
st.notOk(Array.isArray(result.a), 'result is not an array when over limit');
|
||||
st.deepEqual(result.a, { 0: '1', 1: '2', 2: '3', 3: '4' }, 'all values preserved as object');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('arrayLimit 1 with one value', function (st) {
|
||||
// 1 element = limit of 1
|
||||
var result = qs.parse('a[]=1', { arrayLimit: 1 });
|
||||
st.ok(Array.isArray(result.a), 'result is an array when count equals limit');
|
||||
st.deepEqual(result.a, ['1'], 'value preserved as array');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('arrayLimit 1 with two values converts to object', function (st) {
|
||||
// 2 elements exceeds limit of 1
|
||||
var result = qs.parse('a[]=1&a[]=2', { arrayLimit: 1 });
|
||||
st.notOk(Array.isArray(result.a), 'result is not an array');
|
||||
st.deepEqual(result.a, { 0: '1', 1: '2' }, 'all values preserved as object');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('comma + arrayLimit', function (t) {
|
||||
t.test('comma-separated values within arrayLimit stay as array', function (st) {
|
||||
var result = qs.parse('a=1,2,3', { comma: true, arrayLimit: 5 });
|
||||
st.ok(Array.isArray(result.a), 'result is an array');
|
||||
st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('comma-separated values exceeding arrayLimit convert to object', function (st) {
|
||||
var result = qs.parse('a=1,2,3,4', { comma: true, arrayLimit: 3 });
|
||||
st.notOk(Array.isArray(result.a), 'result is not an array when over limit');
|
||||
st.deepEqual(result.a, { 0: '1', 1: '2', 2: '3', 3: '4' }, 'all values preserved as object');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('comma-separated values exceeding arrayLimit with throwOnLimitExceeded throws', function (st) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
|
||||
},
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('comma-separated values at exactly arrayLimit stay as array', function (st) {
|
||||
var result = qs.parse('a=1,2,3', { comma: true, arrayLimit: 3 });
|
||||
st.ok(Array.isArray(result.a), 'result is an array when exactly at limit');
|
||||
st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mixed array and object notation', function (t) {
|
||||
t.test('array brackets with object key - under limit', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[c]=d'),
|
||||
{ a: { 0: 'b', c: 'd' } },
|
||||
'mixing [] and [key] converts to object'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('array index with object key - under limit', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a[0]=b&a[c]=d'),
|
||||
{ a: { 0: 'b', c: 'd' } },
|
||||
'mixing [0] and [key] produces object'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('plain value with array brackets - under limit', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a=b&a[]=c', { arrayLimit: 20 }),
|
||||
{ a: ['b', 'c'] },
|
||||
'plain value combined with [] stays as array under limit'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('array brackets with plain value - under limit', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a=c', { arrayLimit: 20 }),
|
||||
{ a: ['b', 'c'] },
|
||||
'[] combined with plain value stays as array under limit'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('plain value with array index - under limit', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a=b&a[0]=c', { arrayLimit: 20 }),
|
||||
{ a: ['b', 'c'] },
|
||||
'plain value combined with [0] stays as array under limit'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('multiple plain values with duplicates combine', function (st) {
|
||||
st.deepEqual(
|
||||
qs.parse('a=b&a=c&a=d', { arrayLimit: 20 }),
|
||||
{ a: ['b', 'c', 'd'] },
|
||||
'duplicate plain keys combine into array'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('multiple plain values exceeding limit', function (st) {
|
||||
// 3 elements (indices 0-2), max index 2 > limit 1
|
||||
st.deepEqual(
|
||||
qs.parse('a=b&a=c&a=d', { arrayLimit: 1 }),
|
||||
{ a: { 0: 'b', 1: 'c', 2: 'd' } },
|
||||
'duplicate plain keys convert to object when exceeding limit'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('mixed notation produces consistent results when arrayLimit is exceeded', function (st) {
|
||||
var expected = { a: { 0: 'b', 1: 'c', 2: 'd' } };
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: -1 }),
|
||||
expected,
|
||||
'arrayLimit -1'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: 0 }),
|
||||
expected,
|
||||
'arrayLimit 0'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: 1 }),
|
||||
expected,
|
||||
'arrayLimit 1'
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
+1448
@@ -0,0 +1,1448 @@
|
||||
'use strict';
|
||||
|
||||
var test = require('tape');
|
||||
var qs = require('../');
|
||||
var utils = require('../lib/utils');
|
||||
var iconv = require('iconv-lite');
|
||||
var SaferBuffer = require('safer-buffer').Buffer;
|
||||
var hasSymbols = require('has-symbols');
|
||||
var mockProperty = require('mock-property');
|
||||
var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
|
||||
var hasProto = require('has-proto')();
|
||||
var hasBigInt = require('has-bigints')();
|
||||
|
||||
test('stringify()', function (t) {
|
||||
t.test('stringifies a querystring object', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b' }), 'a=b');
|
||||
st.equal(qs.stringify({ a: 1 }), 'a=1');
|
||||
st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2');
|
||||
st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z');
|
||||
st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC');
|
||||
st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80');
|
||||
st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90');
|
||||
st.equal(qs.stringify({ a: '𐐷' }), 'a=%F0%90%90%B7');
|
||||
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('stringifies symbols', { skip: !hasSymbols() }, function (st) {
|
||||
st.equal(qs.stringify(Symbol.iterator), '');
|
||||
st.equal(qs.stringify([Symbol.iterator]), '0=Symbol%28Symbol.iterator%29');
|
||||
st.equal(qs.stringify({ a: Symbol.iterator }), 'a=Symbol%28Symbol.iterator%29');
|
||||
st.equal(
|
||||
qs.stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
|
||||
'a[]=Symbol%28Symbol.iterator%29'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies bigints', { skip: !hasBigInt }, function (st) {
|
||||
var three = BigInt(3);
|
||||
var encodeWithN = function (value, defaultEncoder, charset) {
|
||||
var result = defaultEncoder(value, defaultEncoder, charset);
|
||||
return typeof value === 'bigint' ? result + 'n' : result;
|
||||
};
|
||||
st.equal(qs.stringify(three), '');
|
||||
st.equal(qs.stringify([three]), '0=3');
|
||||
st.equal(qs.stringify([three], { encoder: encodeWithN }), '0=3n');
|
||||
st.equal(qs.stringify({ a: three }), 'a=3');
|
||||
st.equal(qs.stringify({ a: three }, { encoder: encodeWithN }), 'a=3n');
|
||||
st.equal(
|
||||
qs.stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
|
||||
'a[]=3'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }),
|
||||
'a[]=3n'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': { first: 'John', last: 'Doe' } },
|
||||
{ allowDots: false, encodeDotInKeys: false }
|
||||
),
|
||||
'name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe',
|
||||
'with allowDots false and encodeDotInKeys false'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': { first: 'John', last: 'Doe' } },
|
||||
{ allowDots: true, encodeDotInKeys: false }
|
||||
),
|
||||
'name.obj.first=John&name.obj.last=Doe',
|
||||
'with allowDots true and encodeDotInKeys false'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': { first: 'John', last: 'Doe' } },
|
||||
{ allowDots: false, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe',
|
||||
'with allowDots false and encodeDotInKeys true'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': { first: 'John', last: 'Doe' } },
|
||||
{ allowDots: true, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj.first=John&name%252Eobj.last=Doe',
|
||||
'with allowDots true and encodeDotInKeys true'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
{ allowDots: false, encodeDotInKeys: false }
|
||||
),
|
||||
'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe',
|
||||
'with allowDots false and encodeDotInKeys false'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
{ allowDots: true, encodeDotInKeys: false }
|
||||
),
|
||||
'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
|
||||
'with allowDots false and encodeDotInKeys false'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
{ allowDots: false, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe',
|
||||
'with allowDots false and encodeDotInKeys true'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
{ allowDots: true, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
|
||||
'with allowDots true and encodeDotInKeys true'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
|
||||
{ encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
|
||||
'with allowDots undefined and encodeDotInKeys true'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, {
|
||||
encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true
|
||||
}),
|
||||
'name%2Eobj.first=John&name%2Eobj.last=Doe'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }),
|
||||
'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws when `commaRoundTrip` is not a boolean', function (st) {
|
||||
st['throws'](
|
||||
function () { qs.stringify({}, { commaRoundTrip: 'not a boolean' }); },
|
||||
TypeError,
|
||||
'throws when `commaRoundTrip` is not a boolean'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws when `encodeDotInKeys` is not a boolean', function (st) {
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('adds query prefix', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('with query prefix, outputs blank string given an empty object', function (st) {
|
||||
st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
|
||||
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');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('`allowDots` option: stringifies a nested object with dots notation', function (st) {
|
||||
st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
|
||||
st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an array value', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }),
|
||||
'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }),
|
||||
'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }),
|
||||
'a=b%2Cc%2Cd',
|
||||
'comma => comma'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }),
|
||||
'a=b%2Cc%2Cd',
|
||||
'comma round trip => comma'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: ['b', 'c', 'd'] }),
|
||||
'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
|
||||
'default => indices'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('`skipNulls` option', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
|
||||
'a=b',
|
||||
'omits nulls when asked'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }),
|
||||
'a%5Bb%5D=c',
|
||||
'omits nested nulls when asked'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('omits array indices when asked', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('omits object key/value pair when value is empty array', function (st) {
|
||||
st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('should not omit object key/value pair when value is empty array and when asked', function (st) {
|
||||
st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
|
||||
st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz');
|
||||
st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('should throw when allowEmptyArrays is not of type boolean', function (st) {
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); },
|
||||
TypeError
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allowEmptyArrays + strictNullHandling', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ testEmptyArray: [] },
|
||||
{ strictNullHandling: true, allowEmptyArrays: true }
|
||||
),
|
||||
'testEmptyArray[]'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an array value with one item vs multiple items', function (st) {
|
||||
st.test('non-array item', function (s2t) {
|
||||
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
|
||||
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=c');
|
||||
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
|
||||
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('array with a single item', function (s2t) {
|
||||
s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c');
|
||||
s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c');
|
||||
s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
|
||||
s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a[]=c'); // so it parses back as an array
|
||||
s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('array with multiple items', function (s2t) {
|
||||
s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c&a[1]=d');
|
||||
s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c&a[]=d');
|
||||
s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c,d');
|
||||
s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c,d');
|
||||
s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('array with multiple items with a comma inside', function (s2t) {
|
||||
s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c%2Cd,e');
|
||||
s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce');
|
||||
|
||||
s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd,e');
|
||||
s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd%2Ce');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies a nested array value', function (st) {
|
||||
st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[b][0]=c&a[b][1]=d');
|
||||
st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[b][]=c&a[b][]=d');
|
||||
st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a[b]=c,d');
|
||||
st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true }), 'a[b][0]=c&a[b][1]=d');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies comma and empty array values', function (st) {
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), 'a[0]=,&a[1]=&a[2]=c,d%');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), 'a[]=,&a[]=&a[]=c,d%');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), 'a=,,,c,d%');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), 'a=,&a=&a=c,d%');
|
||||
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=%2C&a[]=&a[]=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C,,c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
|
||||
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C%2C%2Cc%2Cd%25');
|
||||
st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies comma and empty non-array values', function (st) {
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), 'a=,&b=&c=c,d%');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), 'a=,&b=&c=c,d%');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), 'a=,&b=&c=c,d%');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), 'a=,&b=&c=c,d%');
|
||||
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies a nested array value with dots notation', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: ['c', 'd'] } },
|
||||
{ allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }
|
||||
),
|
||||
'a.b[0]=c&a.b[1]=d',
|
||||
'indices: stringifies with dots + indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: ['c', 'd'] } },
|
||||
{ allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }
|
||||
),
|
||||
'a.b[]=c&a.b[]=d',
|
||||
'brackets: stringifies with dots + brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: ['c', 'd'] } },
|
||||
{ allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }
|
||||
),
|
||||
'a.b=c,d',
|
||||
'comma: stringifies with dots + comma'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: ['c', 'd'] } },
|
||||
{ allowDots: true, encodeValuesOnly: true }
|
||||
),
|
||||
'a.b[0]=c&a.b[1]=d',
|
||||
'default: stringifies with dots + indices'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an object inside an array', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
|
||||
'a[0][b]=c',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
|
||||
'a[b]=c',
|
||||
'repeat => repeat'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
|
||||
'a[][b]=c',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }),
|
||||
'a[0][b]=c',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
|
||||
'a[0][b][c][0]=1',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
|
||||
'a[b][c]=1',
|
||||
'repeat => repeat'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
|
||||
'a[][b][c][]=1',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }),
|
||||
'a[0][b][c][0]=1',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an array with mixed objects and primitives', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
|
||||
'a[0][b]=1&a[1]=2&a[2]=3',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
|
||||
'a[][b]=1&a[]=2&a[]=3',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }),
|
||||
'???',
|
||||
'brackets => brackets',
|
||||
{ skip: 'TODO: figure out what this should do' }
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }),
|
||||
'a[0][b]=1&a[1]=2&a[2]=3',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an object inside an array with dots notation', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: 'c' }] },
|
||||
{ allowDots: true, encode: false, arrayFormat: 'indices' }
|
||||
),
|
||||
'a[0].b=c',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: 'c' }] },
|
||||
{ allowDots: true, encode: false, arrayFormat: 'brackets' }
|
||||
),
|
||||
'a[].b=c',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: 'c' }] },
|
||||
{ allowDots: true, encode: false }
|
||||
),
|
||||
'a[0].b=c',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: { c: [1] } }] },
|
||||
{ allowDots: true, encode: false, arrayFormat: 'indices' }
|
||||
),
|
||||
'a[0].b.c[0]=1',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: { c: [1] } }] },
|
||||
{ allowDots: true, encode: false, arrayFormat: 'brackets' }
|
||||
),
|
||||
'a[].b.c[]=1',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [{ b: { c: [1] } }] },
|
||||
{ allowDots: true, encode: false }
|
||||
),
|
||||
'a[0].b.c[0]=1',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not omit object keys when indices = false', function (st) {
|
||||
st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses indices notation for arrays when indices=true', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses indices notation for arrays when arrayFormat=indices', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses repeat notation for arrays when arrayFormat=repeat', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses brackets notation for arrays when arrayFormat=brackets', function (st) {
|
||||
st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies a complicated object', function (st) {
|
||||
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an empty value', function (st) {
|
||||
st.equal(qs.stringify({ a: '' }), 'a=');
|
||||
st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
|
||||
|
||||
st.equal(qs.stringify({ a: '', b: '' }), 'a=&b=');
|
||||
st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b=');
|
||||
|
||||
st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D=');
|
||||
st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D');
|
||||
st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D=');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an empty array in different arrayFormat', function (st) {
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c');
|
||||
// arrayFormat default
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), 'b[0]=&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), 'b[]=&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), 'b=&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), 'b=&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), 'b[]=&c=c');
|
||||
// with strictNullHandling
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', strictNullHandling: true }), 'b[0]&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', strictNullHandling: true }), 'b[]&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', strictNullHandling: true }), 'b&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true }), 'b&c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }), 'b[]&c=c');
|
||||
// with skipNulls
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), 'c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), 'c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), 'c=c');
|
||||
st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), 'c=c');
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('returns an empty string for invalid input', function (st) {
|
||||
st.equal(qs.stringify(undefined), '');
|
||||
st.equal(qs.stringify(false), '');
|
||||
st.equal(qs.stringify(null), '');
|
||||
st.equal(qs.stringify(''), '');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an object with a null object as a child', { skip: !hasProto }, function (st) {
|
||||
st.equal(qs.stringify({ a: { __proto__: null, b: 'c' } }), 'a%5Bb%5D=c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('drops keys with a value of undefined', function (st) {
|
||||
st.equal(qs.stringify({ a: undefined }), '');
|
||||
|
||||
st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D');
|
||||
st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D=');
|
||||
st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D=');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('url encodes values', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies a date', function (st) {
|
||||
var now = new Date();
|
||||
var str = 'a=' + encodeURIComponent(now.toISOString());
|
||||
st.equal(qs.stringify({ a: now }), str);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies the weird object from qs', function (st) {
|
||||
st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('skips properties that are part of the object prototype', function (st) {
|
||||
st.intercept(Object.prototype, 'crash', { value: 'test' });
|
||||
|
||||
st.equal(qs.stringify({ a: 'b' }), 'a=b');
|
||||
st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies boolean values', function (st) {
|
||||
st.equal(qs.stringify({ a: true }), 'a=true');
|
||||
st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true');
|
||||
st.equal(qs.stringify({ b: false }), 'b=false');
|
||||
st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies buffer values', function (st) {
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('stringifies an object using an alternative delimiter', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not blow up when Buffer global is missing', function (st) {
|
||||
var restore = mockProperty(global, 'Buffer', { 'delete': true });
|
||||
|
||||
var result = qs.stringify({ a: 'b', c: 'd' });
|
||||
|
||||
restore();
|
||||
|
||||
st.equal(result, 'a=b&c=d');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash when parsing circular references', function (st) {
|
||||
var a = {};
|
||||
a.b = a;
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); },
|
||||
/RangeError: Cyclic object value/,
|
||||
'cyclic values throw'
|
||||
);
|
||||
|
||||
var circular = {
|
||||
a: 'value'
|
||||
};
|
||||
circular.a = circular;
|
||||
st['throws'](
|
||||
function () { qs.stringify(circular); },
|
||||
/RangeError: Cyclic object value/,
|
||||
'cyclic values throw'
|
||||
);
|
||||
|
||||
var arr = ['a'];
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ x: arr, y: arr }); },
|
||||
'non-cyclic values do not throw'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('non-circular duplicated references can still work', function (st) {
|
||||
var hourOfDay = {
|
||||
'function': 'hour_of_day'
|
||||
};
|
||||
|
||||
var p1 = {
|
||||
'function': 'gte',
|
||||
arguments: [hourOfDay, 0]
|
||||
};
|
||||
var p2 = {
|
||||
'function': 'lte',
|
||||
arguments: [hourOfDay, 23]
|
||||
};
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
|
||||
'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
|
||||
'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }),
|
||||
'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('selects properties when filter=array', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b');
|
||||
st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
|
||||
{ filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }
|
||||
),
|
||||
'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
|
||||
'indices => indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
|
||||
{ filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }
|
||||
),
|
||||
'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3',
|
||||
'brackets => brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
|
||||
{ filter: ['a', 'b', 0, 2] }
|
||||
),
|
||||
'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
|
||||
'default => indices'
|
||||
);
|
||||
|
||||
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) } };
|
||||
var filterFunc = function (prefix, value) {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
st.equal(prefix, '', 'prefix is empty');
|
||||
st.equal(value, obj);
|
||||
} else if (prefix === 'c') {
|
||||
return void 0;
|
||||
} else if (value instanceof Date) {
|
||||
st.equal(prefix, 'e[f]');
|
||||
return value.getTime();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000');
|
||||
st.equal(calls, 5);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can disable uri encoding', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
|
||||
st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
|
||||
st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can sort the keys', function (st) {
|
||||
var sort = function (a, b) {
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y');
|
||||
st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can sort the keys at depth 3 or more too', function (st) {
|
||||
var sort = function (a, b) {
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
|
||||
{ sort: sort, encode: false }
|
||||
),
|
||||
'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
|
||||
{ sort: null, encode: false }
|
||||
),
|
||||
'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can stringify with custom encoding', function (st) {
|
||||
st.equal(qs.stringify({ 県: '大阪府', '': '' }, {
|
||||
encoder: function (str) {
|
||||
if (str.length === 0) {
|
||||
return '';
|
||||
}
|
||||
var buf = iconv.encode(str, 'shiftjis');
|
||||
var result = [];
|
||||
for (var i = 0; i < buf.length; ++i) {
|
||||
result.push(buf.readUInt8(i).toString(16));
|
||||
}
|
||||
return '%' + result.join('%');
|
||||
}
|
||||
}), '%8c%a7=%91%e5%8d%e3%95%7b&=');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('receives the default encoder as a second argument', function (st) {
|
||||
st.plan(8);
|
||||
|
||||
qs.stringify({ a: 1, b: new Date(), c: true, d: [1] }, {
|
||||
encoder: function (str) {
|
||||
st.match(typeof str, /^(?:string|number|boolean)$/);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('receives the default encoder as a second argument', function (st) {
|
||||
st.plan(2);
|
||||
|
||||
qs.stringify({ a: 1 }, {
|
||||
encoder: function (str, defaultEncoder) {
|
||||
st.equal(defaultEncoder, utils.encode);
|
||||
}
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws error with wrong encoder', function (st) {
|
||||
st['throws'](function () {
|
||||
qs.stringify({}, { encoder: 'string' });
|
||||
}, new TypeError('Encoder has to be a function.'));
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('serializeDate option', function (st) {
|
||||
var date = new Date();
|
||||
st.equal(
|
||||
qs.stringify({ a: date }),
|
||||
'a=' + date.toISOString().replace(/:/g, '%3A'),
|
||||
'default is toISOString'
|
||||
);
|
||||
|
||||
var mutatedDate = new Date();
|
||||
mutatedDate.toISOString = function () {
|
||||
throw new SyntaxError();
|
||||
};
|
||||
st['throws'](function () {
|
||||
mutatedDate.toISOString();
|
||||
}, SyntaxError);
|
||||
st.equal(
|
||||
qs.stringify({ a: mutatedDate }),
|
||||
'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'),
|
||||
'toISOString works even when method is not locally present'
|
||||
);
|
||||
|
||||
var specificDate = new Date(6);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: specificDate },
|
||||
{ serializeDate: function (d) { return d.getTime() * 7; } }
|
||||
),
|
||||
'a=42',
|
||||
'custom serializeDate function called'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [date] },
|
||||
{
|
||||
serializeDate: function (d) { return d.getTime(); },
|
||||
arrayFormat: 'comma'
|
||||
}
|
||||
),
|
||||
'a=' + date.getTime(),
|
||||
'works with arrayFormat comma'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: [date] },
|
||||
{
|
||||
serializeDate: function (d) { return d.getTime(); },
|
||||
arrayFormat: 'comma',
|
||||
commaRoundTrip: true
|
||||
}
|
||||
),
|
||||
'a%5B%5D=' + date.getTime(),
|
||||
'works with arrayFormat comma'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('RFC 1738 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.equal(qs.stringify({ 'foo(ref)': 'bar' }, { format: qs.formats.RFC1738 }), 'foo(ref)=bar');
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('Edge cases and unknown formats', function (st) {
|
||||
['UFO1234', false, 1234, null, {}, []].forEach(function (format) {
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.stringify({ a: 'b c' }, { format: format });
|
||||
},
|
||||
new TypeError('Unknown format option provided.')
|
||||
);
|
||||
});
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodeValuesOnly', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
|
||||
{ encodeValuesOnly: true, arrayFormat: 'indices' }
|
||||
),
|
||||
'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h',
|
||||
'encodeValuesOnly + indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
|
||||
{ encodeValuesOnly: true, arrayFormat: 'brackets' }
|
||||
),
|
||||
'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h',
|
||||
'encodeValuesOnly + brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
|
||||
{ encodeValuesOnly: true, arrayFormat: 'repeat' }
|
||||
),
|
||||
'a=b&c=d&c=e%3Df&f=g&f=h',
|
||||
'encodeValuesOnly + repeat'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
|
||||
{ arrayFormat: 'indices' }
|
||||
),
|
||||
'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h',
|
||||
'no encodeValuesOnly + indices'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
|
||||
{ arrayFormat: 'brackets' }
|
||||
),
|
||||
'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h',
|
||||
'no encodeValuesOnly + brackets'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
|
||||
{ arrayFormat: 'repeat' }
|
||||
),
|
||||
'a=b&c=d&c=e&f=g&f=h',
|
||||
'no encodeValuesOnly + repeat'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodeValuesOnly - strictNullHandling', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: { b: null } },
|
||||
{ encodeValuesOnly: true, strictNullHandling: true }
|
||||
),
|
||||
'a[b]'
|
||||
);
|
||||
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' });
|
||||
}, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('respects a charset of iso-8859-1', function (st) {
|
||||
st.equal(qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function (st) {
|
||||
st.equal(qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('respects an explicit charset of utf-8 (the default)', function (st) {
|
||||
st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('`charsetSentinel` option', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }),
|
||||
'utf8=%E2%9C%93&a=%C3%A6',
|
||||
'adds the right sentinel when instructed to and the charset is utf-8'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }),
|
||||
'utf8=%26%2310003%3B&a=%E6',
|
||||
'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();
|
||||
});
|
||||
|
||||
t.test('does not mutate the options argument', function (st) {
|
||||
var options = {};
|
||||
qs.stringify({}, options);
|
||||
st.deepEqual(options, {});
|
||||
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.test('allows for encoding keys and values differently', function (st) {
|
||||
var encoder = function (str, defaultEncoder, charset, type) {
|
||||
if (type === 'key') {
|
||||
return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase();
|
||||
}
|
||||
if (type === 'value') {
|
||||
return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase();
|
||||
}
|
||||
throw 'this should never happen! type: ' + type;
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
t.test('objects inside arrays', function (st) {
|
||||
var obj = { a: { b: { c: 'd', e: 'f' } } };
|
||||
var withArray = { a: { b: [{ c: 'd', e: 'f' }] } };
|
||||
|
||||
st.equal(qs.stringify(obj, { encode: false }), 'a[b][c]=d&a[b][e]=f', 'no array, no arrayFormat');
|
||||
st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'brackets' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
|
||||
st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'indices' }), 'a[b][c]=d&a[b][e]=f', 'no array, indices');
|
||||
st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'no array, repeat');
|
||||
st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'comma' }), 'a[b][c]=d&a[b][e]=f', 'no array, comma');
|
||||
|
||||
st.equal(qs.stringify(withArray, { encode: false }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, no arrayFormat');
|
||||
st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'brackets' }), 'a[b][][c]=d&a[b][][e]=f', 'array, bracket');
|
||||
st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'indices' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, indices');
|
||||
st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'array, repeat');
|
||||
st.equal(
|
||||
qs.stringify(withArray, { encode: false, arrayFormat: 'comma' }),
|
||||
'???',
|
||||
'array, comma',
|
||||
{ skip: 'TODO: figure out what this should do' }
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies sparse arrays', function (st) {
|
||||
/* eslint no-sparse-arrays: 0 */
|
||||
st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1]=2&a[4]=1');
|
||||
st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=2&a[]=1');
|
||||
st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=2&a=1');
|
||||
|
||||
st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][b][2][c]=1');
|
||||
st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][b][][c]=1');
|
||||
st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[b][c]=1');
|
||||
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c]=1');
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c]=1');
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
|
||||
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c][1]=1');
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c][]=1');
|
||||
st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodes a very long string', function (st) {
|
||||
var chars = [];
|
||||
var expected = [];
|
||||
for (var i = 0; i < 5e3; i++) {
|
||||
chars.push(' ' + i);
|
||||
|
||||
expected.push('%20' + i);
|
||||
}
|
||||
|
||||
var obj = {
|
||||
foo: chars.join('')
|
||||
};
|
||||
|
||||
st.equal(
|
||||
qs.stringify(obj, { arrayFormat: 'brackets', charset: 'utf-8' }),
|
||||
'foo=' + expected.join('')
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stringifies empty keys', function (t) {
|
||||
emptyTestCases.forEach(function (testCase) {
|
||||
t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
|
||||
st.deepEqual(
|
||||
qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }),
|
||||
testCase.stringifyOutput.indices,
|
||||
'test case: ' + testCase.input + ', indices'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }),
|
||||
testCase.stringifyOutput.brackets,
|
||||
'test case: ' + testCase.input + ', brackets'
|
||||
);
|
||||
st.deepEqual(
|
||||
qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }),
|
||||
testCase.stringifyOutput.repeat,
|
||||
'test case: ' + testCase.input + ', repeat'
|
||||
);
|
||||
|
||||
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.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
|
||||
st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3');
|
||||
st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3&[a]=2');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies non-string keys', function (st) {
|
||||
var S = Object('abc');
|
||||
S.toString = function () {
|
||||
return 'd';
|
||||
};
|
||||
var actual = qs.stringify({ a: 'b', 'false': {}, 1e+22: 'c', d: 'e' }, {
|
||||
filter: ['a', false, null, 10000000000000000000000, S],
|
||||
allowDots: true,
|
||||
encodeDotInKeys: true
|
||||
});
|
||||
|
||||
st.equal(actual, 'a=b&1e%2B22=c&d=e', 'stringifies correctly');
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
'use strict';
|
||||
|
||||
var test = require('tape');
|
||||
var inspect = require('object-inspect');
|
||||
var SaferBuffer = require('safer-buffer').Buffer;
|
||||
var forEach = require('for-each');
|
||||
var v = require('es-value-fixtures');
|
||||
|
||||
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' } });
|
||||
t.deepEqual(oneMerged, { foo: ['bar', { first: '123' }] }, 'merges a standalone and an object into an array');
|
||||
|
||||
var twoMerged = utils.merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } });
|
||||
t.deepEqual(twoMerged, { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, 'merges a standalone and two objects into an array');
|
||||
|
||||
var sandwiched = utils.merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' });
|
||||
t.deepEqual(sandwiched, { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, 'merges an object sandwiched by two standalones into an array');
|
||||
|
||||
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 });
|
||||
|
||||
var func = function f() {};
|
||||
func();
|
||||
t.deepEqual(
|
||||
utils.merge(func, { foo: 'bar' }),
|
||||
[func, { foo: 'bar' }],
|
||||
'functions can not be merged into'
|
||||
);
|
||||
|
||||
func.bar = 'baz';
|
||||
t.deepEqual(
|
||||
utils.merge({ foo: 'bar' }, func),
|
||||
{ foo: 'bar', bar: 'baz' },
|
||||
'functions can be merge sources'
|
||||
);
|
||||
|
||||
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.test('with overflow objects (from arrayLimit)', function (st) {
|
||||
// arrayLimit is max index, so with limit 0, max index 0 is allowed (1 element)
|
||||
// To create overflow, need 2+ elements with limit 0, or 3+ with limit 1, etc.
|
||||
st.test('merges primitive into overflow object at next index', function (s2t) {
|
||||
// Create an overflow object via combine: 3 elements (indices 0-2) with limit 0
|
||||
var overflow = utils.combine(['a', 'b'], 'c', 0, false);
|
||||
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();
|
||||
});
|
||||
|
||||
st.test('merges primitive into regular object with numeric keys normally', function (s2t) {
|
||||
var obj = { 0: 'a', 1: 'b' };
|
||||
s2t.notOk(utils.isOverflow(obj), 'plain object is not marked as overflow');
|
||||
var merged = utils.merge(obj, 'c');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b', c: true }, 'adds primitive as key (not at next index)');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges primitive into object with non-numeric keys normally', function (s2t) {
|
||||
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();
|
||||
});
|
||||
|
||||
st.test('with strictMerge, wraps object and primitive in array', function (s2t) {
|
||||
var obj = { foo: 'bar' };
|
||||
var merged = utils.merge(obj, 'baz', { strictMerge: true });
|
||||
s2t.deepEqual(merged, [{ foo: 'bar' }, 'baz'], 'wraps in array with strictMerge');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges overflow object into primitive', function (s2t) {
|
||||
// Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
|
||||
var overflow = utils.combine(['a'], 'b', 0, false);
|
||||
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
|
||||
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();
|
||||
});
|
||||
|
||||
st.test('merges overflow object with multiple values into primitive', function (s2t) {
|
||||
// Create an overflow object via combine: 3 elements (indices 0-2) with limit 0
|
||||
var overflow = utils.combine(['b', 'c'], 'd', 0, false);
|
||||
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();
|
||||
});
|
||||
|
||||
st.test('merges regular object into primitive as array', function (s2t) {
|
||||
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();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('assign()', function (t) {
|
||||
var target = { a: 1, b: 2 };
|
||||
var source = { b: 3, c: 4 };
|
||||
var result = utils.assign(target, source);
|
||||
|
||||
t.equal(result, target, 'returns the target');
|
||||
t.deepEqual(target, { a: 1, b: 3, c: 4 }, 'target and source are merged');
|
||||
t.deepEqual(source, { b: 3, c: 4 }, 'source is untouched');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('combine()', function (t) {
|
||||
t.test('both arrays', function (st) {
|
||||
var a = [1];
|
||||
var b = [2];
|
||||
var combined = utils.combine(a, b);
|
||||
|
||||
st.deepEqual(a, [1], 'a is not mutated');
|
||||
st.deepEqual(b, [2], 'b is not mutated');
|
||||
st.notEqual(a, combined, 'a !== combined');
|
||||
st.notEqual(b, combined, 'b !== combined');
|
||||
st.deepEqual(combined, [1, 2], 'combined is a + b');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('one array, one non-array', function (st) {
|
||||
var aN = 1;
|
||||
var a = [aN];
|
||||
var bN = 2;
|
||||
var b = [bN];
|
||||
|
||||
var combinedAnB = utils.combine(aN, b);
|
||||
st.deepEqual(b, [bN], 'b is not mutated');
|
||||
st.notEqual(aN, combinedAnB, 'aN + b !== aN');
|
||||
st.notEqual(a, combinedAnB, 'aN + b !== a');
|
||||
st.notEqual(bN, combinedAnB, 'aN + b !== bN');
|
||||
st.notEqual(b, combinedAnB, 'aN + b !== b');
|
||||
st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array');
|
||||
|
||||
var combinedABn = utils.combine(a, bN);
|
||||
st.deepEqual(a, [aN], 'a is not mutated');
|
||||
st.notEqual(aN, combinedABn, 'a + bN !== aN');
|
||||
st.notEqual(a, combinedABn, 'a + bN !== a');
|
||||
st.notEqual(bN, combinedABn, 'a + bN !== bN');
|
||||
st.notEqual(b, combinedABn, 'a + bN !== b');
|
||||
st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('neither is an array', function (st) {
|
||||
var combined = utils.combine(1, 2);
|
||||
st.notEqual(1, combined, '1 + 2 !== 1');
|
||||
st.notEqual(2, combined, '1 + 2 !== 2');
|
||||
st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('with arrayLimit', function (st) {
|
||||
st.test('under the limit', function (s2t) {
|
||||
var combined = utils.combine(['a', 'b'], 'c', 10, false);
|
||||
s2t.deepEqual(combined, ['a', 'b', 'c'], 'returns array when under limit');
|
||||
s2t.ok(Array.isArray(combined), 'result is an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('exactly at the limit stays as array', function (s2t) {
|
||||
var combined = utils.combine(['a', 'b'], 'c', 3, false);
|
||||
s2t.deepEqual(combined, ['a', 'b', 'c'], 'stays as array when count equals limit');
|
||||
s2t.ok(Array.isArray(combined), 'result is an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('over the limit', function (s2t) {
|
||||
var combined = utils.combine(['a', 'b', 'c'], 'd', 3, false);
|
||||
s2t.deepEqual(combined, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'converts to object when over limit');
|
||||
s2t.notOk(Array.isArray(combined), 'result is not an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('with arrayLimit 1', function (s2t) {
|
||||
var combined = utils.combine([], 'a', 1, false);
|
||||
s2t.deepEqual(combined, ['a'], 'stays as array when count equals limit');
|
||||
s2t.ok(Array.isArray(combined), 'result is an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('with arrayLimit 0 converts single element to object', function (s2t) {
|
||||
var combined = utils.combine([], 'a', 0, false);
|
||||
s2t.deepEqual(combined, { 0: 'a' }, 'converts to object when count exceeds limit');
|
||||
s2t.notOk(Array.isArray(combined), 'result is not an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('with arrayLimit 0 and two elements converts to object', function (s2t) {
|
||||
var combined = utils.combine(['a'], 'b', 0, false);
|
||||
s2t.deepEqual(combined, { 0: 'a', 1: 'b' }, 'converts to object when count exceeds limit');
|
||||
s2t.notOk(Array.isArray(combined), 'result is not an array');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('with plainObjects option', function (s2t) {
|
||||
var combined = utils.combine(['a', 'b'], 'c', 1, true);
|
||||
var expected = { __proto__: null, 0: 'a', 1: 'b', 2: 'c' };
|
||||
s2t.deepEqual(combined, expected, 'converts to object with null prototype');
|
||||
s2t.equal(Object.getPrototypeOf(combined), null, 'result has null prototype when plainObjects is true');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
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
|
||||
var overflow = utils.combine(['a', 'b'], 'c', 0, false);
|
||||
s2t.ok(utils.isOverflow(overflow), 'initial object is marked as overflow');
|
||||
|
||||
var combined = utils.combine(overflow, 'd', 10, false);
|
||||
s2t.equal(combined, overflow, 'returns the same object (mutated)');
|
||||
s2t.deepEqual(combined, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'adds value at next numeric index');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('does not treat plain object with numeric keys as overflow', function (s2t) {
|
||||
var plainObj = { 0: 'a', 1: 'b' };
|
||||
s2t.notOk(utils.isOverflow(plainObj), 'plain object is not marked as overflow');
|
||||
|
||||
// combine treats this as a regular value, not an overflow object to append to
|
||||
var combined = utils.combine(plainObj, 'c', 10, false);
|
||||
s2t.deepEqual(combined, [{ 0: 'a', 1: 'b' }, 'c'], 'concatenates as regular values');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('decode', function (t) {
|
||||
t.equal(
|
||||
utils.decode('a+b'),
|
||||
'a b',
|
||||
'decodes + to space'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
utils.decode('name%2Eobj'),
|
||||
'name.obj',
|
||||
'decodes a string'
|
||||
);
|
||||
t.equal(
|
||||
utils.decode('name%2Eobj%2Efoo', null, 'iso-8859-1'),
|
||||
'name.obj.foo',
|
||||
'decodes a string in iso-8859-1'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('encode', function (t) {
|
||||
forEach(v.nullPrimitives, function (nullish) {
|
||||
t['throws'](
|
||||
function () { utils.encode(nullish); },
|
||||
TypeError,
|
||||
inspect(nullish) + ' is not a string'
|
||||
);
|
||||
});
|
||||
|
||||
t.equal(utils.encode(''), '', 'empty string returns itself');
|
||||
t.deepEqual(utils.encode([]), [], 'empty array returns itself');
|
||||
t.deepEqual(utils.encode({ length: 0 }), { length: 0 }, 'empty arraylike returns itself');
|
||||
|
||||
t.test('symbols', { skip: !v.hasSymbols }, function (st) {
|
||||
st.equal(utils.encode(Symbol('x')), 'Symbol%28x%29', 'symbol is encoded');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
utils.encode('(abc)'),
|
||||
'%28abc%29',
|
||||
'encodes parentheses'
|
||||
);
|
||||
t.equal(
|
||||
utils.encode({ toString: function () { return '(abc)'; } }),
|
||||
'%28abc%29',
|
||||
'toStrings and encodes parentheses'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
utils.encode('abc 123 💩', null, 'iso-8859-1'),
|
||||
'abc%20123%20%26%2355357%3B%26%2356489%3B',
|
||||
'encodes in iso-8859-1'
|
||||
);
|
||||
|
||||
var longString = '';
|
||||
var expectedString = '';
|
||||
for (var i = 0; i < 1500; i++) {
|
||||
longString += ' ';
|
||||
expectedString += '%20';
|
||||
}
|
||||
|
||||
t.equal(
|
||||
utils.encode(longString),
|
||||
expectedString,
|
||||
'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',
|
||||
'encodes parens normally'
|
||||
);
|
||||
t.equal(
|
||||
utils.encode('\x28\x29', null, null, null, 'RFC1738'),
|
||||
'()',
|
||||
'does not encode parens in RFC1738'
|
||||
);
|
||||
|
||||
// todo RFC1738 format
|
||||
|
||||
t.equal(
|
||||
utils.encode('Āက豈'),
|
||||
'%C4%80%E1%80%80%EF%A4%80',
|
||||
'encodes multibyte chars'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
utils.encode('\uD83D \uDCA9'),
|
||||
'%F0%9F%90%A0%F0%BA%90%80',
|
||||
'encodes lone surrogates'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isBuffer()', function (t) {
|
||||
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');
|
||||
});
|
||||
|
||||
var fakeBuffer = { constructor: Buffer };
|
||||
t.equal(utils.isBuffer(fakeBuffer), false, 'fake buffer is not a buffer');
|
||||
|
||||
var saferBuffer = SaferBuffer.from('abc');
|
||||
t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer 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();
|
||||
});
|
||||
|
||||
test('isRegExp()', function (t) {
|
||||
t.equal(utils.isRegExp(/a/g), true, 'RegExp is a RegExp');
|
||||
t.equal(utils.isRegExp(new RegExp('a', 'g')), true, 'new RegExp is a RegExp');
|
||||
t.equal(utils.isRegExp(new Date()), false, 'Date is not a RegExp');
|
||||
|
||||
forEach(v.primitives, function (primitive) {
|
||||
t.equal(utils.isRegExp(primitive), false, inspect(primitive) + ' is not a RegExp');
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
Reference in New Issue
Block a user