diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..f96d4f1 --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "es3", + "es2015", + "es2016", + "es2017" + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..925fbb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ + +sauce_connect.log +.zuulrc diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e69de29 diff --git a/.travis.yml b/.travis.yml index 895dbd3..ffb5649 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: node_js node_js: - - 0.6 - - 0.8 + - "6" +addons: + sauce_connect: true diff --git a/.zuul.yml b/.zuul.yml new file mode 100644 index 0000000..21043dd --- /dev/null +++ b/.zuul.yml @@ -0,0 +1,30 @@ +ui: mocha-bdd + +browsers: + - name: chrome + version: [26, latest] + - name: android + version: [4.4, latest] + + - name: firefox + version: [4, latest] + + - name: safari + version: [5, 6, latest] + - name: iphone + version: [8.1, latest] + - name: ipad + version: [8.1, latest] + + - name: ie + version: 8..11 + - name: microsoftedge + version: -1..latest + +browserify: + - transform: babelify + +concurrency: 1 +# tunnel: +# type: ngrok +# bind_tls: true diff --git a/License.md b/License.md index fc80e85..c9fa9a7 100644 --- a/License.md +++ b/License.md @@ -1,5 +1,9 @@ +# Implementation (./src/index.js) + +## License + +Copyright Node.js contributors. All rights reserved. -Copyright 2012 Irakli Gozalishvili. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the @@ -17,3 +21,15 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## See also + + + +# Rest of the repository + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Readme.md b/Readme.md index 344fdb5..885e347 100644 --- a/Readme.md +++ b/Readme.md @@ -1,15 +1,16 @@ # querystring -[![Build Status](https://secure.travis-ci.org/mike-spainhower/querystring.png)](http://travis-ci.org/mike-spainhower/querystring) +[![Build Status](https://travis-ci.org/SpainTrain/querystring-es3.svg?branch=master)](https://travis-ci.org/SpainTrain/querystring-es3) +[![Build Status](https://saucelabs.com/buildstatus/querystring-es3)](https://saucelabs.com/u/querystring-es3) +[![Build Status](https://saucelabs.com/browser-matrix/querystring-es3.svg)](https://saucelabs.com/u/querystring-es3) -[![Browser support](http://ci.testling.com/mike-spainhower/querystring.png)](http://ci.testling.com/mike-spainhower/querystring) +Node API compliant querystring module for all browsers. (ES3 compatible) - - -Node's querystring module for all engines. - -## Install ## +## Install npm install querystring-es3 +## Usage + + diff --git a/decode.js b/decode.js deleted file mode 100644 index b5825c0..0000000 --- a/decode.js +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; - -// If obj.hasOwnProperty has been overridden, then calling -// obj.hasOwnProperty(prop) will break. -// See: https://github.com/joyent/node/issues/1707 -function hasOwnProperty(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop); -} - -module.exports = function(qs, sep, eq, options) { - sep = sep || '&'; - eq = eq || '='; - var obj = {}; - - if (typeof qs !== 'string' || qs.length === 0) { - return obj; - } - - var regexp = /\+/g; - qs = qs.split(sep); - - var maxKeys = 1000; - if (options && typeof options.maxKeys === 'number') { - maxKeys = options.maxKeys; - } - - var len = qs.length; - // maxKeys <= 0 means that we should not limit keys count - if (maxKeys > 0 && len > maxKeys) { - len = maxKeys; - } - - for (var i = 0; i < len; ++i) { - var x = qs[i].replace(regexp, '%20'), - idx = x.indexOf(eq), - kstr, vstr, k, v; - - if (idx >= 0) { - kstr = x.substr(0, idx); - vstr = x.substr(idx + 1); - } else { - kstr = x; - vstr = ''; - } - - k = decodeURIComponent(kstr); - v = decodeURIComponent(vstr); - - if (!hasOwnProperty(obj, k)) { - obj[k] = v; - } else if (isArray(obj[k])) { - obj[k].push(v); - } else { - obj[k] = [obj[k], v]; - } - } - - return obj; -}; - -var isArray = Array.isArray || function (xs) { - return Object.prototype.toString.call(xs) === '[object Array]'; -}; diff --git a/encode.js b/encode.js deleted file mode 100644 index 76e4cfb..0000000 --- a/encode.js +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; - -var stringifyPrimitive = function(v) { - switch (typeof v) { - case 'string': - return v; - - case 'boolean': - return v ? 'true' : 'false'; - - case 'number': - return isFinite(v) ? v : ''; - - default: - return ''; - } -}; - -module.exports = function(obj, sep, eq, name) { - sep = sep || '&'; - eq = eq || '='; - if (obj === null) { - obj = undefined; - } - - if (typeof obj === 'object') { - return map(objectKeys(obj), function(k) { - var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; - if (isArray(obj[k])) { - return map(obj[k], function(v) { - return ks + encodeURIComponent(stringifyPrimitive(v)); - }).join(sep); - } else { - return ks + encodeURIComponent(stringifyPrimitive(obj[k])); - } - }).join(sep); - - } - - if (!name) return ''; - return encodeURIComponent(stringifyPrimitive(name)) + eq + - encodeURIComponent(stringifyPrimitive(obj)); -}; - -var isArray = Array.isArray || function (xs) { - return Object.prototype.toString.call(xs) === '[object Array]'; -}; - -function map (xs, f) { - if (xs.map) return xs.map(f); - var res = []; - for (var i = 0; i < xs.length; i++) { - res.push(f(xs[i], i)); - } - return res; -} - -var objectKeys = Object.keys || function (obj) { - var res = []; - for (var key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key); - } - return res; -}; diff --git a/index.js b/index.js deleted file mode 100644 index 99826ea..0000000 --- a/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -exports.decode = exports.parse = require('./decode'); -exports.encode = exports.stringify = require('./encode'); diff --git a/package.json b/package.json index 695d8a0..87bf925 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,55 @@ { "name": "querystring-es3", "id": "querystring-es3", - "version": "0.2.1", - "description": "Node's querystring module for all engines. (ES3 compat fork)", - "keywords": [ "commonjs", "query", "querystring" ], - "author": "Irakli Gozalishvili ", + "version": "0.3.0", + "main": "dist/index.js", + "description": "Node API compliant querystring module for all browsers. (ES3 compatible)", + "scripts": { + "build": "babel src -d dist", + "prepare": "npm run build", + "prepublish": "npm run build", + "test": "npm-run-all build test:*", + "test:mocha": "mocha --require babel-register ./test/index.js", + "test:zuul": "zuul --no-coverage -- test" + }, + "keywords": [ + "commonjs", + "query", + "querystring", + "es3" + ], + "author": "SpainTrain ", "repository": { "type": "git", - "url": "git://github.com/mike-spainhower/querystring.git", - "web": "https://github.com/mike-spainhower/querystring" + "url": "git://github.com/SpainTrain/querystring-es3.git", + "web": "https://github.com/SpainTrain/querystring-es3" }, "bugs": { - "url": "http://github.com/mike-spainhower/querystring/issues/" + "url": "http://github.com/SpainTrain/querystring-es3/issues/" }, "devDependencies": { - "test": "~0.x.0", - "phantomify": "~0.x.0", - "retape": "~0.x.0", - "tape": "~0.1.5" + "assert": "~1.4.1", + "babel-cli": "~6.24.0", + "babel-preset-es2015": "~6.24.0", + "babel-preset-es2016": "~6.22.0", + "babel-preset-es2017": "~6.22.0", + "babel-preset-es3": "~1.0.1", + "babel-register": "~6.24.0", + "babelify": "~7.3.0", + "browserify": "~14.1.0", + "eslint": "~3.13.0", + "json3": "~3.3.2", + "mocha": "~3.2.0", + "npm-run-all": "~4.0.2", + "object-inspect": "~1.2.2", + "zuul": "~3.11.1", + "zuul-ngrok": "~4.0.0" }, "engines": { - "node": ">=0.4.x" - }, - "scripts": { - "test": "npm run test-node && npm run test-browser && npm run test-tap", - "test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/common-index.js", - "test-node": "node ./test/common-index.js", - "test-tap": "node ./test/tap-index.js" - }, - "testling": { - "files": "test/tap-index.js", - "browsers": { - "iexplore": [ - 9, - 10 - ], - "chrome": [ - 16, - 20, - 25, - "canary" - ], - "firefox": [ - 10, - 15, - 16, - 17, - 18, - "nightly" - ], - "safari": [ - 5, - 6 - ], - "opera": [ - 12 - ] - } + "node": ">=4" }, - "licenses": [{ - "type" : "MIT", - "url" : "https://github.com/Gozala/enchain/License.md" - }] + "license": "MIT", + "dependencies": { + "buffer": "5.0.5" + } } diff --git a/src/.eslintrc.yaml b/src/.eslintrc.yaml new file mode 100644 index 0000000..671d9e4 --- /dev/null +++ b/src/.eslintrc.yaml @@ -0,0 +1,154 @@ +# https://github.com/nodejs/node/blob/v6.10.2/.eslintrc.yaml + +env: + node: true + es6: true + +parserOptions: + ecmaVersion: 2017 + +rules: + # Possible Errors + # http://eslint.org/docs/rules/#possible-errors + comma-dangle: [2, only-multiline] + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: [2, functions] + no-extra-semi: 2 + no-func-assign: 2 + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-obj-calls: 2 + no-proto: 2 + no-template-curly-in-string: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + no-unsafe-negation: 2 + use-isnan: 2 + valid-typeof: 2 + + # Best Practices + # http://eslint.org/docs/rules/#best-practices + dot-location: [2, property] + no-fallthrough: 2 + no-global-assign: 2 + no-multi-spaces: 2 + no-octal: 2 + no-redeclare: 2 + no-self-assign: 2 + no-throw-literal: 2 + no-unused-labels: 2 + no-useless-call: 2 + no-useless-escape: 2 + no-void: 2 + no-with: 2 + + # Strict Mode + # http://eslint.org/docs/rules/#strict-mode + strict: [2, global] + + # Variables + # http://eslint.org/docs/rules/#variables + no-delete-var: 2 + no-undef: 2 + no-unused-vars: [2, {args: none}] + + # Node.js and CommonJS + # http://eslint.org/docs/rules/#nodejs-and-commonjs + no-mixed-requires: 2 + no-new-require: 2 + no-path-concat: 2 + no-restricted-modules: [2, sys, _linklist] + no-restricted-properties: + - 2 + - object: assert + property: deepEqual + message: Use assert.deepStrictEqual(). + - object: assert + property: equal + message: Use assert.strictEqual() rather than assert.equal(). + - object: assert + property: notEqual + message: Use assert.notStrictEqual() rather than assert.notEqual(). + - property: __defineGetter__ + message: __defineGetter__ is deprecated. + - property: __defineSetter__, + message: __defineSetter__ is deprecated. + + # Stylistic Issues + # http://eslint.org/docs/rules/#stylistic-issues + block-spacing: 2 + brace-style: [2, 1tbs, {allowSingleLine: true}] + comma-spacing: 2 + comma-style: 2 + computed-property-spacing: 2 + eol-last: 2 + func-call-spacing: 2 + func-name-matching: 2 + indent: [2, 2, {ArrayExpression: first, + CallExpression: {arguments: first}, + MemberExpression: 1, + ObjectExpression: first, + SwitchCase: 1}] + key-spacing: [2, {mode: minimum}] + keyword-spacing: 2 + linebreak-style: [2, unix] + max-len: [2, {code: 80, ignoreUrls: true, tabWidth: 2}] + new-parens: 2 + no-mixed-spaces-and-tabs: 2 + no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] + no-tabs: 2 + no-trailing-spaces: 2 + one-var-declaration-per-line: 2 + operator-linebreak: [2, after] + quotes: [2, single, avoid-escape] + semi: 2 + semi-spacing: 2 + space-before-blocks: [2, always] + space-before-function-paren: [2, never] + space-in-parens: [2, never] + space-infix-ops: 2 + space-unary-ops: 2 + unicode-bom: 2 + + # ECMAScript 6 + # http://eslint.org/docs/rules/#ecmascript-6 + arrow-parens: [2, always] + arrow-spacing: [2, {before: true, after: true}] + constructor-super: 2 + no-class-assign: 2 + no-confusing-arrow: 2 + no-const-assign: 2 + no-dupe-class-members: 2 + no-new-symbol: 2 + no-this-before-super: 2 + prefer-const: [2, {ignoreReadBeforeAssign: true}] + rest-spread-spacing: 2 + template-curly-spacing: 2 + +# Global scoped method and vars +globals: + COUNTER_HTTP_CLIENT_REQUEST: false + COUNTER_HTTP_CLIENT_RESPONSE: false + COUNTER_HTTP_SERVER_REQUEST: false + COUNTER_HTTP_SERVER_RESPONSE: false + COUNTER_NET_SERVER_CONNECTION: false + COUNTER_NET_SERVER_CONNECTION_CLOSE: false + DTRACE_HTTP_CLIENT_REQUEST: false + DTRACE_HTTP_CLIENT_RESPONSE: false + DTRACE_HTTP_SERVER_REQUEST: false + DTRACE_HTTP_SERVER_RESPONSE: false + DTRACE_NET_SERVER_CONNECTION: false + DTRACE_NET_STREAM_END: false + LTTNG_HTTP_CLIENT_REQUEST: false + LTTNG_HTTP_CLIENT_RESPONSE: false + LTTNG_HTTP_SERVER_REQUEST: false + LTTNG_HTTP_SERVER_RESPONSE: false + LTTNG_NET_SERVER_CONNECTION: false + LTTNG_NET_STREAM_END: false diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..249eabe --- /dev/null +++ b/src/index.js @@ -0,0 +1,505 @@ +// https://github.com/nodejs/node/blob/v6.10.2/lib/querystring.js + +'use strict'; + +const QueryString = module.exports = { + unescapeBuffer, + // `unescape()` is a JS global, so we need to use a different local name + unescape: qsUnescape, + + // `escape()` is a JS global, so we need to use a different local name + escape: qsEscape, + + stringify, + encode: stringify, + + parse, + decode: parse +}; + +const Buffer = require('buffer').Buffer; +const objectKeys = require('./object-keys'); + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray +const isArray = (arg) => + Object.prototype.toString.call(arg) === '[object Array]'; + +// Production steps of ECMA-262, Edition 5, 15.4.4.14 +// Reference: http://es5.github.io/#x15.4.4.14 +const indexOf = (arr, searchElement, fromIndex) => { + var k; + + if (arr == null) { + throw new TypeError('"arr" is null or not defined'); + } + + var o = Object(arr); + var len = o.length >>> 0; + + if (len === 0) { + return -1; + } + + var n = fromIndex | 0; + + if (n >= len) { + return -1; + } + + k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + + while (k < len) { + if (k in o && o[k] === searchElement) { + return k; + } + k++; + } + return -1; +}; + +// This constructor is used to store parsed query string values. Instantiating +// this is faster than explicitly calling `Object.create(null)` to get a +// "clean" empty object (tested with v8 v4.9). +function ParsedQueryString() {} +ParsedQueryString.prototype = Object.create ? Object.create(null) : {}; // IE8 + +const unhexTable = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47 + +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ... + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // ... 255 +]; +// a safe fast alternative to decodeURIComponent +function unescapeBuffer(s, decodeSpaces) { + var out = Buffer.allocUnsafe(s.length); + var state = 0; + var n, m, hexchar, c; + + for (var inIndex = 0, outIndex = 0; ; inIndex++) { + if (inIndex < s.length) { + c = s.charCodeAt(inIndex); + } else { + if (state > 0) { + out[outIndex++] = 37/*%*/; + if (state === 2) + out[outIndex++] = hexchar; + } + break; + } + switch (state) { + case 0: // Any character + switch (c) { + case 37: // '%' + n = 0; + m = 0; + state = 1; + break; + case 43: // '+' + if (decodeSpaces) + c = 32; // ' ' + // falls through + default: + out[outIndex++] = c; + break; + } + break; + + case 1: // First hex digit + hexchar = c; + n = unhexTable[c]; + if (!(n >= 0)) { + out[outIndex++] = 37/*%*/; + out[outIndex++] = c; + state = 0; + break; + } + state = 2; + break; + + case 2: // Second hex digit + state = 0; + m = unhexTable[c]; + if (!(m >= 0)) { + out[outIndex++] = 37/*%*/; + out[outIndex++] = hexchar; + out[outIndex++] = c; + break; + } + out[outIndex++] = 16 * n + m; + break; + } + } + + // TODO support returning arbitrary buffers. + + return out.slice(0, outIndex); +} + + +function qsUnescape(s, decodeSpaces) { + try { + return decodeURIComponent(s); + } catch (e) { + return QueryString.unescapeBuffer(s, decodeSpaces).toString(); + } +} + + +const hexTable = []; +for (var i = 0; i < 256; ++i) + hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); + +// These characters do not need escaping when generating query strings: +// ! - . _ ~ +// ' ( ) * +// digits +// alpha (uppercase) +// alpha (lowercase) +const noEscape = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0 // 112 - 127 +]; +// QueryString.escape() replaces encodeURIComponent() +// http://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 +function qsEscape(str) { + if (typeof str !== 'string') { + if (typeof str === 'object') + str = String(str); + else + str += ''; + } + var out = ''; + var lastPos = 0; + + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + + // ASCII + if (c < 0x80) { + if (noEscape[c] === 1) + continue; + if (lastPos < i) + out += str.slice(lastPos, i); + lastPos = i + 1; + out += hexTable[c]; + continue; + } + + if (lastPos < i) + out += str.slice(lastPos, i); + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]; + continue; + } + if (c < 0xD800 || c >= 0xE000) { + lastPos = i + 1; + out += hexTable[0xE0 | (c >> 12)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + continue; + } + // Surrogate pair + ++i; + var c2; + if (i < str.length) + c2 = str.charCodeAt(i) & 0x3FF; + else + throw new URIError('URI malformed'); + lastPos = i + 1; + c = 0x10000 + (((c & 0x3FF) << 10) | c2); + out += hexTable[0xF0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3F)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + } + if (lastPos === 0) + return str; + if (lastPos < str.length) + return out + str.slice(lastPos); + return out; +} + +function stringifyPrimitive(v) { + if (typeof v === 'string') + return v; + if (typeof v === 'number' && isFinite(v)) + return '' + v; + if (typeof v === 'boolean') + return v ? 'true' : 'false'; + return ''; +} + + +function stringify(obj, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + + var encode = QueryString.escape; + if (options && typeof options.encodeURIComponent === 'function') { + encode = options.encodeURIComponent; + } + + if (obj !== null && typeof obj === 'object') { + var keys = objectKeys(obj); + var len = keys.length; + var flast = len - 1; + var fields = ''; + for (var i = 0; i < len; ++i) { + var k = keys[i]; + var v = obj[k]; + var ks = encode(stringifyPrimitive(k)) + eq; + + if (isArray(v)) { + var vlen = v.length; + var vlast = vlen - 1; + for (var j = 0; j < vlen; ++j) { + fields += ks + encode(stringifyPrimitive(v[j])); + if (j < vlast) + fields += sep; + } + if (vlen && i < flast) + fields += sep; + } else { + fields += ks + encode(stringifyPrimitive(v)); + if (i < flast) + fields += sep; + } + } + return fields; + } + return ''; +} + +function charCodes(str) { + if (str.length === 0) return []; + if (str.length === 1) return [str.charCodeAt(0)]; + const ret = []; + for (var i = 0; i < str.length; ++i) + ret[ret.length] = str.charCodeAt(i); + return ret; +} +const defSepCodes = [38]; // & +const defEqCodes = [61]; // = + +// Parse a key/val string. +function parse(qs, sep, eq, options) { + const obj = new ParsedQueryString(); + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + var sepCodes = (!sep ? defSepCodes : charCodes(sep + '')); + var eqCodes = (!eq ? defEqCodes : charCodes(eq + '')); + const sepLen = sepCodes.length; + const eqLen = eqCodes.length; + + var pairs = 1000; + if (options && typeof options.maxKeys === 'number') { + // -1 is used in place of a value like Infinity for meaning + // "unlimited pairs" because of additional checks V8 (at least as of v5.4) + // has to do when using variables that contain values like Infinity. Since + // `pairs` is always decremented and checked explicitly for 0, -1 works + // effectively the same as Infinity, while providing a significant + // performance boost. + pairs = (options.maxKeys > 0 ? options.maxKeys : -1); + } + + var decode = QueryString.unescape; + if (options && typeof options.decodeURIComponent === 'function') { + decode = options.decodeURIComponent; + } + const customDecode = (decode !== qsUnescape); + + const keys = []; + var posIdx = 0; + var lastPos = 0; + var sepIdx = 0; + var eqIdx = 0; + var key = ''; + var value = ''; + var keyEncoded = customDecode; + var valEncoded = customDecode; + var encodeCheck = 0; + for (var i = 0; i < qs.length; ++i) { + const code = qs.charCodeAt(i); + + // Try matching key/value pair separator (e.g. '&') + if (code === sepCodes[sepIdx]) { + if (++sepIdx === sepLen) { + // Key/value pair separator match! + const end = i - sepIdx + 1; + if (eqIdx < eqLen) { + // If we didn't find the key/value separator, treat the substring as + // part of the key instead of the value + if (lastPos < end) + key += qs.slice(lastPos, end); + } else if (lastPos < end) + value += qs.slice(lastPos, end); + if (keyEncoded) + key = decodeStr(key, decode); + if (valEncoded) + value = decodeStr(value, decode); + + if (key || value || lastPos - posIdx > sepLen || i === 0) { + // Use a key array lookup instead of using hasOwnProperty(), which is + // slower + if (indexOf(keys, key) === -1) { + obj[key] = value; + keys[keys.length] = key; + } else { + const curValue = obj[key] || ''; + // A simple Array-specific property check is enough here to + // distinguish from a string value and is faster and still safe + // since we are generating all of the values being assigned. + if (curValue.pop) + curValue[curValue.length] = value; + else if (curValue) + obj[key] = [curValue, value]; + } + } else if (i === 1) { + // A pair with repeated sep could be added into obj in the first loop + // and it should be deleted + delete obj[key]; + } + if (--pairs === 0) + break; + keyEncoded = valEncoded = customDecode; + encodeCheck = 0; + key = value = ''; + posIdx = lastPos; + lastPos = i + 1; + sepIdx = eqIdx = 0; + } + continue; + } else { + sepIdx = 0; + if (!valEncoded) { + // Try to match an (valid) encoded byte (once) to minimize unnecessary + // calls to string decoding functions + if (code === 37/*%*/) { + encodeCheck = 1; + } else if (encodeCheck > 0 && + ((code >= 48/*0*/ && code <= 57/*9*/) || + (code >= 65/*A*/ && code <= 70/*F*/) || + (code >= 97/*a*/ && code <= 102/*f*/))) { + if (++encodeCheck === 3) + valEncoded = true; + } else { + encodeCheck = 0; + } + } + } + + // Try matching key/value separator (e.g. '=') if we haven't already + if (eqIdx < eqLen) { + if (code === eqCodes[eqIdx]) { + if (++eqIdx === eqLen) { + // Key/value separator match! + const end = i - eqIdx + 1; + if (lastPos < end) + key += qs.slice(lastPos, end); + encodeCheck = 0; + lastPos = i + 1; + } + continue; + } else { + eqIdx = 0; + if (!keyEncoded) { + // Try to match an (valid) encoded byte once to minimize unnecessary + // calls to string decoding functions + if (code === 37/*%*/) { + encodeCheck = 1; + } else if (encodeCheck > 0 && + ((code >= 48/*0*/ && code <= 57/*9*/) || + (code >= 65/*A*/ && code <= 70/*F*/) || + (code >= 97/*a*/ && code <= 102/*f*/))) { + if (++encodeCheck === 3) + keyEncoded = true; + } else { + encodeCheck = 0; + } + } + } + } + + if (code === 43/*+*/) { + if (eqIdx < eqLen) { + if (lastPos < i) + key += qs.slice(lastPos, i); + key += '%20'; + keyEncoded = true; + } else { + if (lastPos < i) + value += qs.slice(lastPos, i); + value += '%20'; + valEncoded = true; + } + lastPos = i + 1; + } + } + + // Check if we have leftover key or value data + if (pairs !== 0 && (lastPos < qs.length || eqIdx > 0)) { + if (lastPos < qs.length) { + if (eqIdx < eqLen) + key += qs.slice(lastPos); + else if (sepIdx < sepLen) + value += qs.slice(lastPos); + } + if (keyEncoded) + key = decodeStr(key, decode); + if (valEncoded) + value = decodeStr(value, decode); + // Use a key array lookup instead of using hasOwnProperty(), which is + // slower + if (indexOf(keys, key) === -1) { + obj[key] = value; + keys[keys.length] = key; + } else { + const curValue = obj[key]; + // A simple Array-specific property check is enough here to + // distinguish from a string value and is faster and still safe since + // we are generating all of the values being assigned. + if (curValue.pop) + curValue[curValue.length] = value; + else + obj[key] = [curValue, value]; + } + } + + return obj; +} + + +// v8 does not optimize functions with try-catch blocks, so we isolate them here +// to minimize the damage (Note: no longer true as of V8 5.4 -- but still will +// not be inlined). +function decodeStr(s, decoder) { + try { + return decoder(s); + } catch (e) { + return QueryString.unescape(s, true); + } +} diff --git a/src/object-keys.js b/src/object-keys.js new file mode 100644 index 0000000..46d61b9 --- /dev/null +++ b/src/object-keys.js @@ -0,0 +1,47 @@ +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + +'use strict'; + +const objectKeys = Object.keys || (function() { + var hasOwnProperty = Object.prototype.hasOwnProperty; + var hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'); + var dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ]; + var dontEnumsLength = dontEnums.length; + + return function(obj) { + if ( + typeof obj !== 'function' && (typeof obj !== 'object' || obj === null) + ) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + var prop; + var i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; +}()); + +module.exports = objectKeys; diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml new file mode 100644 index 0000000..edc7822 --- /dev/null +++ b/test/.eslintrc.yaml @@ -0,0 +1,149 @@ +# https://github.com/nodejs/node/blob/v6.10.2/.eslintrc.yaml + +env: + mocha: true + node: true + es6: true + +parserOptions: + ecmaVersion: 2017 + +rules: + # Possible Errors + # http://eslint.org/docs/rules/#possible-errors + comma-dangle: [2, only-multiline] + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: [2, functions] + no-extra-semi: 2 + no-func-assign: 2 + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-obj-calls: 2 + no-proto: 2 + no-template-curly-in-string: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + no-unsafe-negation: 2 + use-isnan: 2 + valid-typeof: 2 + + # Best Practices + # http://eslint.org/docs/rules/#best-practices + dot-location: [2, property] + no-fallthrough: 2 + no-global-assign: 2 + no-multi-spaces: 2 + no-octal: 2 + no-redeclare: 2 + no-self-assign: 2 + no-throw-literal: 2 + no-unused-labels: 2 + no-useless-call: 2 + no-useless-escape: 2 + no-void: 2 + no-with: 2 + + # Strict Mode + # http://eslint.org/docs/rules/#strict-mode + strict: [2, global] + + # Variables + # http://eslint.org/docs/rules/#variables + no-delete-var: 2 + no-undef: 2 + no-unused-vars: [2, {args: none}] + + # Node.js and CommonJS + # http://eslint.org/docs/rules/#nodejs-and-commonjs + no-mixed-requires: 2 + no-new-require: 2 + no-path-concat: 2 + no-restricted-modules: [2, sys, _linklist] + no-restricted-properties: + - 2 + - object: assert + property: notEqual + message: Use assert.notStrictEqual() rather than assert.notEqual(). + - property: __defineGetter__ + message: __defineGetter__ is deprecated. + - property: __defineSetter__, + message: __defineSetter__ is deprecated. + + # Stylistic Issues + # http://eslint.org/docs/rules/#stylistic-issues + block-spacing: 2 + brace-style: [2, 1tbs, {allowSingleLine: true}] + comma-spacing: 2 + comma-style: 2 + computed-property-spacing: 2 + eol-last: 2 + func-call-spacing: 2 + func-name-matching: 2 + indent: [2, 2, {ArrayExpression: first, + CallExpression: {arguments: first}, + MemberExpression: 1, + ObjectExpression: first, + SwitchCase: 1}] + key-spacing: [2, {mode: minimum}] + keyword-spacing: 2 + linebreak-style: [2, unix] + max-len: [2, {code: 80, ignoreUrls: true, tabWidth: 2}] + new-parens: 2 + no-mixed-spaces-and-tabs: 2 + no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] + no-tabs: 2 + no-trailing-spaces: 2 + one-var-declaration-per-line: 2 + operator-linebreak: [2, after] + quotes: [2, single, avoid-escape] + semi: 2 + semi-spacing: 2 + space-before-blocks: [2, always] + space-before-function-paren: [2, never] + space-in-parens: [2, never] + space-infix-ops: 2 + space-unary-ops: 2 + unicode-bom: 2 + + # ECMAScript 6 + # http://eslint.org/docs/rules/#ecmascript-6 + arrow-parens: [2, always] + arrow-spacing: [2, {before: true, after: true}] + constructor-super: 2 + no-class-assign: 2 + no-confusing-arrow: 2 + no-const-assign: 2 + no-dupe-class-members: 2 + no-new-symbol: 2 + no-this-before-super: 2 + prefer-const: [2, {ignoreReadBeforeAssign: true}] + rest-spread-spacing: 2 + template-curly-spacing: 2 + +# Global scoped method and vars +globals: + COUNTER_HTTP_CLIENT_REQUEST: false + COUNTER_HTTP_CLIENT_RESPONSE: false + COUNTER_HTTP_SERVER_REQUEST: false + COUNTER_HTTP_SERVER_RESPONSE: false + COUNTER_NET_SERVER_CONNECTION: false + COUNTER_NET_SERVER_CONNECTION_CLOSE: false + DTRACE_HTTP_CLIENT_REQUEST: false + DTRACE_HTTP_CLIENT_RESPONSE: false + DTRACE_HTTP_SERVER_REQUEST: false + DTRACE_HTTP_SERVER_RESPONSE: false + DTRACE_NET_SERVER_CONNECTION: false + DTRACE_NET_STREAM_END: false + LTTNG_HTTP_CLIENT_REQUEST: false + LTTNG_HTTP_CLIENT_RESPONSE: false + LTTNG_HTTP_SERVER_REQUEST: false + LTTNG_HTTP_SERVER_RESPONSE: false + LTTNG_NET_SERVER_CONNECTION: false + LTTNG_NET_STREAM_END: false diff --git a/test/common-index.js b/test/common-index.js deleted file mode 100644 index f356f98..0000000 --- a/test/common-index.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -require("test").run(require("./index")) \ No newline at end of file diff --git a/test/index.js b/test/index.js index 62eb2ac..936ec0d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,210 +1,73 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// test using assert -var qs = require('../'); - -// folding block, commented to pass gjslint -// {{{ -// [ wonkyQS, canonicalQS, obj ] -var qsTestCases = [ - ['foo=918854443121279438895193', - 'foo=918854443121279438895193', - {'foo': '918854443121279438895193'}], - ['foo=bar', 'foo=bar', {'foo': 'bar'}], - ['foo=bar&foo=quux', 'foo=bar&foo=quux', {'foo': ['bar', 'quux']}], - ['foo=1&bar=2', 'foo=1&bar=2', {'foo': '1', 'bar': '2'}], - ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', - 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', - {'my weird field': 'q1!2"\'w$5&7/z8)?' }], - ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', {'foo=baz': 'bar'}], - ['foo=baz=bar', 'foo=baz%3Dbar', {'foo': 'baz=bar'}], - ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', - 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', - { 'str': 'foo', - 'arr': ['1', '2', '3'], - 'somenull': '', - 'undef': ''}], - [' foo = bar ', '%20foo%20=%20bar%20', {' foo ': ' bar '}], - // disable test that fails ['foo=%zx', 'foo=%25zx', {'foo': '%zx'}], - ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', {'foo': '\ufffd' }], - // See: https://github.com/joyent/node/issues/1707 - ['hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', - 'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', - { hasOwnProperty: 'x', - toString: 'foo', - valueOf: 'bar', - __defineGetter__: 'baz' }], - // See: https://github.com/joyent/node/issues/3058 - ['foo&bar=baz', 'foo=&bar=baz', { foo: '', bar: 'baz' }] -]; - -// [ wonkyQS, canonicalQS, obj ] -var qsColonTestCases = [ - ['foo:bar', 'foo:bar', {'foo': 'bar'}], - ['foo:bar;foo:quux', 'foo:bar;foo:quux', {'foo': ['bar', 'quux']}], - ['foo:1&bar:2;baz:quux', - 'foo:1%26bar%3A2;baz:quux', - {'foo': '1&bar:2', 'baz': 'quux'}], - ['foo%3Abaz:bar', 'foo%3Abaz:bar', {'foo:baz': 'bar'}], - ['foo:baz:bar', 'foo:baz%3Abar', {'foo': 'baz:bar'}] -]; - -// [wonkyObj, qs, canonicalObj] -var extendedFunction = function() {}; -extendedFunction.prototype = {a: 'b'}; -var qsWeirdObjects = [ - [{regexp: /./g}, 'regexp=', {'regexp': ''}], - [{regexp: new RegExp('.', 'g')}, 'regexp=', {'regexp': ''}], - [{fn: function() {}}, 'fn=', {'fn': ''}], - [{fn: new Function('')}, 'fn=', {'fn': ''}], - [{math: Math}, 'math=', {'math': ''}], - [{e: extendedFunction}, 'e=', {'e': ''}], - [{d: new Date()}, 'd=', {'d': ''}], - [{d: Date}, 'd=', {'d': ''}], - [{f: new Boolean(false), t: new Boolean(true)}, 'f=&t=', {'f': '', 't': ''}], - [{f: false, t: true}, 'f=false&t=true', {'f': 'false', 't': 'true'}], - [{n: null}, 'n=', {'n': ''}], - [{nan: NaN}, 'nan=', {'nan': ''}], - [{inf: Infinity}, 'inf=', {'inf': ''}] -]; -// }}} - -var qsNoMungeTestCases = [ - ['', {}], - ['foo=bar&foo=baz', {'foo': ['bar', 'baz']}], - ['blah=burp', {'blah': 'burp'}], - ['gragh=1&gragh=3&goo=2', {'gragh': ['1', '3'], 'goo': '2'}], - ['frappucino=muffin&goat%5B%5D=scone&pond=moose', - {'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose'}], - ['trololol=yes&lololo=no', {'trololol': 'yes', 'lololo': 'no'}] -]; - -exports['test basic'] = function(assert) { - assert.strictEqual('918854443121279438895193', - qs.parse('id=918854443121279438895193').id, - 'prase id=918854443121279438895193'); -}; - -exports['test that the canonical qs is parsed properly'] = function(assert) { - qsTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[0]), - 'parse ' + testCase[0]); - }); -}; - - -exports['test that the colon test cases can do the same'] = function(assert) { - qsColonTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[0], ';', ':'), - 'parse ' + testCase[0] + ' -> ; :'); - }); -}; - -exports['test the weird objects, that they get parsed properly'] = function(assert) { - qsWeirdObjects.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[1]), - 'parse ' + testCase[1]); - }); -}; - -exports['test non munge test cases'] = function(assert) { - qsNoMungeTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[0], qs.stringify(testCase[1], '&', '=', false), - 'stringify ' + JSON.stringify(testCase[1]) + ' -> & ='); - }); -}; - -exports['test the nested qs-in-qs case'] = function(assert) { - var f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); - f.q = qs.parse(f.q); - assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, - 'parse a=b&q=x%3Dy%26y%3Dz'); -}; - -exports['test nested in colon'] = function(assert) { - var f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); - f.q = qs.parse(f.q, ';', ':'); - assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, - 'parse a:b;q:x%3Ay%3By%3Az -> ; :'); -}; - -exports['test stringifying'] = function(assert) { - qsTestCases.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[2]), - 'stringify ' + JSON.stringify(testCase[2])); - }); - - qsColonTestCases.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[2], ';', ':'), - 'stringify ' + JSON.stringify(testCase[2]) + ' -> ; :'); - }); - - qsWeirdObjects.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[0]), - 'stringify ' + JSON.stringify(testCase[0])); - }); -}; - -exports['test stringifying nested'] = function(assert) { - var f = qs.stringify({ - a: 'b', - q: qs.stringify({ - x: 'y', - y: 'z' - }) - }); - assert.equal(f, 'a=b&q=x%3Dy%26y%3Dz', - JSON.stringify({ - a: 'b', - 'qs.stringify -> q': { - x: 'y', - y: 'z' - } - })); - - var threw = false; - try { qs.parse(undefined); } catch(error) { threw = true; } - assert.ok(!threw, "does not throws on undefined"); -}; - -exports['test nested in colon'] = function(assert) { - var f = qs.stringify({ - a: 'b', - q: qs.stringify({ - x: 'y', - y: 'z' - }, ';', ':') - }, ';', ':'); - assert.equal(f, 'a:b;q:x%3Ay%3By%3Az', - 'stringify ' + JSON.stringify({ - a: 'b', - 'qs.stringify -> q': { - x: 'y', - y: 'z' - } - }) + ' -> ; : '); - - - assert.deepEqual({}, qs.parse(), 'parse undefined'); -}; +'use strict'; + +// Production steps of ECMA-262, Edition 5, 15.4.4.18 +// Reference: http://es5.github.io/#x15.4.4.18 +if (!Array.prototype.forEach) { + + Array.prototype.forEach = function(callback/*, thisArg*/) { + + var T, k; + + if (this == null) { + throw new TypeError('this is null or not defined'); + } + + // 1. Let O be the result of calling toObject() passing the + // |this| value as the argument. + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get() internal + // method of O with the argument "length". + // 3. Let len be toUint32(lenValue). + var len = O.length >>> 0; + + // 4. If isCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== 'function') { + throw new TypeError(callback + ' is not a function'); + } + + // 5. If thisArg was supplied, let T be thisArg; else let + // T be undefined. + if (arguments.length > 1) { + T = arguments[1]; + } + + // 6. Let k be 0 + k = 0; + + // 7. Repeat, while k < len + while (k < len) { + + var kValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty + // internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + + // i. Let kValue be the result of calling the Get internal + // method of O with argument Pk. + kValue = O[k]; + + // ii. Call the Call internal method of callback with T as + // the this value and argument list containing kValue, k, and O. + callback.call(T, kValue, k, O); + } + // d. Increase k by 1. + k++; + } + // 8. return undefined + }; +} + +// eslint-disable-next-line no-undef +const isIE8 = typeof document != 'undefined' && document.documentMode === 8; + +require('./test-querystring'); +require('./test-querystring-escape'); +!isIE8 && require('./test-querystring-maxKeys-non-finite'); +require('./test-querystring-multichar-separator'); diff --git a/test/tap-index.js b/test/tap-index.js deleted file mode 100644 index 70679b3..0000000 --- a/test/tap-index.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -require("retape")(require("./index")) \ No newline at end of file diff --git a/test/test-querystring-escape.js b/test/test-querystring-escape.js new file mode 100644 index 0000000..00bd8d6 --- /dev/null +++ b/test/test-querystring-escape.js @@ -0,0 +1,36 @@ +// https://github.com/nodejs/node/blob/v6.10.2/test/parallel/test-querystring-escape.js + +'use strict'; + +const assert = require('assert'); + +const qs = require('..'); + +describe('test-querystring-escape', function() { + it('does basic escaping', function() { + assert.deepEqual(qs.escape(5), '5'); + assert.deepEqual(qs.escape('test'), 'test'); + assert.deepEqual(qs.escape({}), '%5Bobject%20Object%5D'); + assert.deepEqual(qs.escape([5, 10]), '5%2C10'); + assert.deepEqual(qs.escape('Ŋōđĕ'), '%C5%8A%C5%8D%C4%91%C4%95'); + }); + + it('using toString for objects', function() { + assert.strictEqual( + qs.escape({test: 5, toString: () => 'test', valueOf: () => 10 }), + 'test' + ); + }); + + it('toString is not callable, must throw an error', function() { + assert.throws(() => qs.escape({toString: 5})); + }); + + it('should use valueOf instead of non-callable toString', function() { + assert.strictEqual(qs.escape({toString: 5, valueOf: () => 'test'}), 'test'); + }); + + it('throws when given Symbol', function() { + assert.throws(() => qs.escape(Symbol('test'))); + }); +}); diff --git a/test/test-querystring-maxKeys-non-finite.js b/test/test-querystring-maxKeys-non-finite.js new file mode 100644 index 0000000..960034d --- /dev/null +++ b/test/test-querystring-maxKeys-non-finite.js @@ -0,0 +1,65 @@ +// https://github.com/nodejs/node/blob/v6.10.2/test/parallel/test-querystring-maxKeys-non-finite.js + +'use strict'; +// This test was originally written to test a regression +// that was introduced by +// https://github.com/nodejs/node/pull/2288#issuecomment-179543894 + +const assert = require('assert'); +const objectKeys = require('../src/object-keys'); +const parse = require('..').parse; + +/* +taken from express-js/body-parser +https://github.com/expressjs/body-parser/ +blob/ed25264fb494cf0c8bc992b8257092cd4f694d5e/test/urlencoded.js#L636-L651 +*/ +function createManyParams(count) { + var str = ''; + + if (count === 0) { + return str; + } + + str += '0=0'; + + for (var i = 1; i < count; i++) { + var n = i.toString(36); + str += '&' + n + '=' + n; + } + + return str; +} + +describe('test-querystring-maxKeys-non-finite', function() { + const count = 10000; + const originalMaxLength = 1000; + const params = createManyParams(count); + + // thealphanerd + // 27def4f introduced a change to parse that would cause Inifity + // to be passed to String.prototype.split as an argument for limit + // In this instance split will always return an empty array + // this test confirms that the output of parse is the expected length + // when passed Infinity as the argument for maxKeys + const resultInfinity = + parse(params, undefined, undefined, {maxKeys: Infinity}); + const resultNaN = parse(params, undefined, undefined, {maxKeys: NaN}); + const resultInfinityString = parse(params, undefined, undefined, { + maxKeys: 'Infinity' + }); + const resultNaNString = parse(params, undefined, undefined, {maxKeys: 'NaN'}); + + it('Non Finite maxKeys should return the length of input', function() { + assert.equal(objectKeys(resultInfinity).length, count); + assert.equal(objectKeys(resultNaN).length, count); + }); + + it( + 'Strings maxKeys should return the maxLength defined by parses internals', + function() { + assert.equal(objectKeys(resultInfinityString).length, originalMaxLength); + assert.equal(objectKeys(resultNaNString).length, originalMaxLength); + }, + ); +}); diff --git a/test/test-querystring-multichar-separator.js b/test/test-querystring-multichar-separator.js new file mode 100644 index 0000000..6978dc6 --- /dev/null +++ b/test/test-querystring-multichar-separator.js @@ -0,0 +1,42 @@ +// https://github.com/nodejs/node/blob/v6.10.2/test/parallel/test-querystring-multichar-separator.js + +'use strict'; + +const assert = require('assert'); +const objectKeys = require('../src/object-keys'); +const qs = require('..'); + +function check(actual, expected) { + Object.create && assert(!(actual instanceof Object)); + assert.deepEqual( + objectKeys(actual).sort(), + objectKeys(expected).sort(), + ); + objectKeys(expected).forEach(function(key) { + assert.deepEqual(actual[key], expected[key]); + }); +} + +describe('test-querystring-multichar-separator', function() { + it('passes', function() { + check( + qs.parse('foo=>bar&&bar=>baz', '&&', '=>'), + {foo: 'bar', bar: 'baz'}, + ); + + assert.equal( + qs.stringify({foo: 'bar', bar: 'baz'}, '&&', '=>'), + 'foo=>bar&&bar=>baz', + ); + + check( + qs.parse('foo==>bar, bar==>baz', ', ', '==>'), + {foo: 'bar', bar: 'baz'}, + ); + + assert.equal( + qs.stringify({foo: 'bar', bar: 'baz'}, ', ', '==>'), + 'foo==>bar, bar==>baz', + ); + }); +}); diff --git a/test/test-querystring.js b/test/test-querystring.js new file mode 100644 index 0000000..7a2d750 --- /dev/null +++ b/test/test-querystring.js @@ -0,0 +1,417 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + + +// https://github.com/nodejs/node/blob/v6.10.2/test/parallel/test-querystring.js +'use strict'; + +// test using assert +const assert = require('assert'); +const JSON = require('json3'); +const inspect = require('object-inspect'); +const objectKeys = require('../src/object-keys'); +const qs = require('../'); + +const hasObjectCreate = !Object.create; + +function createWithNoPrototype(properties) { + const noProto = !hasObjectCreate ? Object.create(null) : {}; + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} + +const qsTestCases = [ + !hasObjectCreate ? ['', '', {}] : [ + '__proto__=1', + '__proto__=1', + createWithNoPrototype([{key: '__proto__', value: '1'}])], + ['__defineGetter__=asdf', + '__defineGetter__=asdf', + JSON.parse('{"__defineGetter__":"asdf"}')], + ['foo=918854443121279438895193', + 'foo=918854443121279438895193', + {'foo': '918854443121279438895193'}], + ['foo=bar', 'foo=bar', {'foo': 'bar'}], + ['foo=bar&foo=quux', 'foo=bar&foo=quux', {'foo': ['bar', 'quux']}], + ['foo=1&bar=2', 'foo=1&bar=2', {'foo': '1', 'bar': '2'}], + ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', + 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', + {'my weird field': 'q1!2"\'w$5&7/z8)?' }], + ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', {'foo=baz': 'bar'}], + ['foo=baz=bar', 'foo=baz%3Dbar', {'foo': 'baz=bar'}], + ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + { 'str': 'foo', + 'arr': ['1', '2', '3'], + 'somenull': '', + 'undef': ''}], + [' foo = bar ', '%20foo%20=%20bar%20', {' foo ': ' bar '}], + ['foo=%zx', 'foo=%25zx', {'foo': '%zx'}], + ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', {'foo': '\ufffd' }], + // See: https://github.com/joyent/node/issues/1707 + ['hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + hasObjectCreate ? + '__defineGetter__=baz&toString=foo&valueOf=bar&hasOwnProperty=x' : + 'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + { hasOwnProperty: 'x', + toString: 'foo', + valueOf: 'bar', + __defineGetter__: 'baz' }], + // See: https://github.com/joyent/node/issues/3058 + ['foo&bar=baz', 'foo=&bar=baz', { foo: '', bar: 'baz' }], + ['a=b&c&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&c=&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&=c&d=e', 'a=b&=c&d=e', { a: 'b', '': 'c', d: 'e' }], + ['a=b&=&c=d', 'a=b&=&c=d', { a: 'b', '': '', c: 'd' }], + ['&&foo=bar&&', 'foo=bar', { foo: 'bar' }], + ['&&&&', '', {}], + ['&=&', '=', { '': '' }], + ['&=&=', '=&=', { '': [ '', '' ]}], + ['+foo=+bar', '%20foo=%20bar', { ' foo': ' bar' }], + [null, '', {}], + [undefined, '', {}] +]; + +// [ wonkyQS, canonicalQS, obj ] +var qsColonTestCases = [ + ['foo:bar', 'foo:bar', {'foo': 'bar'}], + ['foo:bar;foo:quux', 'foo:bar;foo:quux', {'foo': ['bar', 'quux']}], + ['foo:1&bar:2;baz:quux', + 'foo:1%26bar%3A2;baz:quux', + {'foo': '1&bar:2', 'baz': 'quux'}], + ['foo%3Abaz:bar', 'foo%3Abaz:bar', {'foo:baz': 'bar'}], + ['foo:baz:bar', 'foo:baz%3Abar', {'foo': 'baz:bar'}] +]; + +// [wonkyObj, qs, canonicalObj] +var extendedFunction = function() {}; +extendedFunction.prototype = {a: 'b'}; +var qsWeirdObjects = [ + [{regexp: /./g}, 'regexp=', {'regexp': ''}], + [{regexp: new RegExp('.', 'g')}, 'regexp=', {'regexp': ''}], + [{fn: function() {}}, 'fn=', {'fn': ''}], + [{fn: new Function('')}, 'fn=', {'fn': ''}], + [{math: Math}, 'math=', {'math': ''}], + [{e: extendedFunction}, 'e=', {'e': ''}], + [{d: new Date()}, 'd=', {'d': ''}], + [{d: Date}, 'd=', {'d': ''}], + [{f: new Boolean(false), t: new Boolean(true)}, 'f=&t=', {'f': '', 't': ''}], + [{f: false, t: true}, 'f=false&t=true', {'f': 'false', 't': 'true'}], + [{n: null}, 'n=', {'n': ''}], + [{nan: NaN}, 'nan=', {'nan': ''}], + [{inf: Infinity}, 'inf=', {'inf': ''}], + [{a: [], b: []}, '', {}] +]; +// }}} + +var qsNoMungeTestCases = [ + ['', {}], + ['foo=bar&foo=baz', {'foo': ['bar', 'baz']}], + ['blah=burp', {'blah': 'burp'}], + ['a=!-._~\'()*', {'a': '!-._~\'()*'}], + ['a=abcdefghijklmnopqrstuvwxyz', {'a': 'abcdefghijklmnopqrstuvwxyz'}], + ['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', {'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}], + ['a=0123456789', {'a': '0123456789'}], + ['gragh=1&gragh=3&goo=2', {'gragh': ['1', '3'], 'goo': '2'}], + ['frappucino=muffin&goat%5B%5D=scone&pond=moose', + {'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose'}], + ['trololol=yes&lololo=no', {'trololol': 'yes', 'lololo': 'no'}] +]; + +const qsUnescapeTestCases = [ + ['there is nothing to unescape here', + 'there is nothing to unescape here'], + ['there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped', + 'there are several spaces that need to be unescaped'], + ['there%2Qare%0-fake%escaped values in%%%%this%9Hstring', + 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'], + ['%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37', + ' !"#$%&\'()*+,-./01234567'] +]; + +function check(actual, expected, input) { + if (Object.create) { + assert(!(actual instanceof Object)); + } + const actualKeys = objectKeys(actual).sort(); + const expectedKeys = objectKeys(expected).sort(); + let msg; + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Actual keys: ${inspect(actualKeys)}\n` + + `Expected keys: ${inspect(expectedKeys)}`; + } + assert.deepEqual(actualKeys, expectedKeys, msg); + expectedKeys.forEach(function(key) { + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Key: ${inspect(key)}\n` + + `Actual value: ${inspect(actual[key])}\n` + + `Expected value: ${inspect(expected[key])}`; + } else { + msg = undefined; + } + assert.deepEqual(actual[key], expected[key], msg); + }); +} + +describe('test-querystring', function() { + it('performs basic parsing', function() { + assert.strictEqual( + '918854443121279438895193', + qs.parse('id=918854443121279438895193').id, + 'parse id=918854443121279438895193', + ); + }); + + it('test that the canonical qs is parsed properly', function() { + qsTestCases.forEach(function(testCase) { + check(qs.parse(testCase[0]), testCase[2], testCase[0]); + }); + }); + + it('test that the colon test cases can do the same', function() { + qsColonTestCases.forEach(function(testCase) { + check(qs.parse(testCase[0], ';', ':'), testCase[2]); + }); + }); + + it('test the weird objects, that they get parsed properly', function() { + qsWeirdObjects.forEach(function(testCase) { + check(qs.parse(testCase[1]), testCase[2]); + }); + }); + + it('test non munge test cases', function() { + qsNoMungeTestCases.forEach(function(testCase) { + assert.deepEqual(testCase[0], qs.stringify(testCase[1], '&', '=')); + }); + }); + + it('test the nested qs-in-qs case', function() { + const f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); + check(f, createWithNoPrototype([ + { key: 'a', value: 'b'}, + {key: 'q', value: 'x=y&y=z'} + ])); + + f.q = qs.parse(f.q); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y'}, + {key: 'y', value: 'z' } + ]); + check(f.q, expectedInternal); + }); + + it('test nested in colon', function() { + const f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); + check(f, createWithNoPrototype([ + {key: 'a', value: 'b'}, + {key: 'q', value: 'x:y;y:z'} + ])); + f.q = qs.parse(f.q, ';', ':'); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y'}, + {key: 'y', value: 'z' } + ]); + check(f.q, expectedInternal); + }); + + it('test stringifying basic', function() { + qsTestCases.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[2])); + }); + + qsColonTestCases.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[2], ';', ':')); + }); + + qsWeirdObjects.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[0])); + }); + }); + + it('test stringifying invalid surrogate pair throws URIError', function() { + assert.throws(function() { + qs.stringify({ foo: '\udc00' }); + }, URIError); + }); + + + it('test stringifying coerce numbers to string', function() { + assert.strictEqual('foo=0', qs.stringify({ foo: 0 })); + assert.strictEqual('foo=0', qs.stringify({ foo: -0 })); + assert.strictEqual('foo=3', qs.stringify({ foo: 3 })); + assert.strictEqual('foo=-72.42', qs.stringify({ foo: -72.42 })); + assert.strictEqual('foo=', qs.stringify({ foo: NaN })); + assert.strictEqual('foo=', qs.stringify({ foo: Infinity })); + }); + + it('test stringifying nested', function() { + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }) + }); + assert.equal(f, 'a=b&q=x%3Dy%26y%3Dz'); + }); + + it('test stringifying nested in colon', function() { + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }, ';', ':') + }, ';', ':'); + assert.equal(f, 'a:b;q:x%3Ay%3By%3Az'); + }); + + it('test stringifying empty string', function() { + assert.strictEqual(qs.stringify(), ''); + assert.strictEqual(qs.stringify(0), ''); + assert.strictEqual(qs.stringify([]), ''); + assert.strictEqual(qs.stringify(null), ''); + assert.strictEqual(qs.stringify(true), ''); + + check(qs.parse(), {}); + }); + + it('empty sep', function() { + check(qs.parse('a', []), { a: '' }); + }); + + it('empty eq', function() { + check(qs.parse('a', null, []), { '': 'a' }); + }); + + it('Test limiting', function() { + assert.equal( + objectKeys(qs.parse('a=1&b=1&c=1', null, null, { maxKeys: 1 })).length, + 1, + ); + }); + + it('Test removing limit', function() { + if (Object.create) { // Crashed IE8, skip on IE8 + function testUnlimitedKeys() { + const query = {}; + for (var i = 0; i < 2000; i++) query[i] = i; + const url = qs.stringify(query); + assert.equal( + objectKeys(qs.parse(url, null, null, { maxKeys: 0 })).length, + 2000); + } + testUnlimitedKeys(); + } + }); + + it('buffer', function() { + var b = qs.unescapeBuffer('%d3%f2Ug%1f6v%24%5e%98%cb' + + '%0d%ac%a2%2f%9d%eb%d8%a2%e6'); + // + assert.equal(0xd3, b[0]); + assert.equal(0xf2, b[1]); + assert.equal(0x55, b[2]); + assert.equal(0x67, b[3]); + assert.equal(0x1f, b[4]); + assert.equal(0x36, b[5]); + assert.equal(0x76, b[6]); + assert.equal(0x24, b[7]); + assert.equal(0x5e, b[8]); + assert.equal(0x98, b[9]); + assert.equal(0xcb, b[10]); + assert.equal(0x0d, b[11]); + assert.equal(0xac, b[12]); + assert.equal(0xa2, b[13]); + assert.equal(0x2f, b[14]); + assert.equal(0x9d, b[15]); + assert.equal(0xeb, b[16]); + assert.equal(0xd8, b[17]); + assert.equal(0xa2, b[18]); + assert.equal(0xe6, b[19]); + + assert.strictEqual(qs.unescapeBuffer('a+b', true).toString(), 'a b'); + assert.strictEqual(qs.unescapeBuffer('a%').toString(), 'a%'); + assert.strictEqual(qs.unescapeBuffer('a%2').toString(), 'a%2'); + assert.strictEqual(qs.unescapeBuffer('a%20').toString(), 'a '); + assert.strictEqual(qs.unescapeBuffer('a%2g').toString(), 'a%2g'); + assert.strictEqual(qs.unescapeBuffer('a%%').toString(), 'a%%'); + }); + + + it('Test custom decode', function() { + function demoDecode(str) { + return str + str; + } + check( + qs.parse('a=a&b=b&c=c', null, null, { decodeURIComponent: demoDecode }), + { aa: 'aa', bb: 'bb', cc: 'cc' }, + ); + }); + + it('Test QueryString.unescape', function() { + function errDecode(str) { + throw new Error('To jump to the catch scope'); + } + check(qs.parse('a=a', null, null, { decodeURIComponent: errDecode }), + { a: 'a' }); + }); + + it('Test custom encode', function() { + function demoEncode(str) { + return str[0]; + } + var obj = { aa: 'aa', bb: 'bb', cc: 'cc' }; + assert.equal( + qs.stringify(obj, null, null, { encodeURIComponent: demoEncode }), + 'a=a&b=b&c=c'); + }); + + it('Test QueryString.unescapeBuffer', function() { + qsUnescapeTestCases.forEach(function(testCase) { + assert.strictEqual(qs.unescape(testCase[0]), testCase[1]); + assert.strictEqual( + qs.unescapeBuffer(testCase[0]).toString(), testCase[1], + ); + }); + }); + + it('test overriding .unescape', function() { + var prevUnescape = qs.unescape; + qs.unescape = function(str) { + return str.replace(/o/g, '_'); + }; + check( + qs.parse('foo=bor'), + createWithNoPrototype([{key: 'f__', value: 'b_r'}]), + ); + qs.unescape = prevUnescape; + }); + + it('test separator and "equals" parsing order', function() { + check(qs.parse('foo&bar', '&', '&'), { foo: '', bar: '' }); + }); +});