| // 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. |
| |
| // Query String Utilities |
| |
| 'use strict'; |
| |
| const { |
| Array, |
| ArrayIsArray, |
| Int8Array, |
| MathAbs, |
| NumberIsFinite, |
| ObjectCreate, |
| ObjectKeys, |
| String, |
| StringPrototypeCharCodeAt, |
| StringPrototypeSlice, |
| decodeURIComponent, |
| } = primordials; |
| |
| const { Buffer } = require('buffer'); |
| const { |
| encodeStr, |
| hexTable, |
| isHexTable |
| } = require('internal/querystring'); |
| 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 unhexTable = new Int8Array([ |
| -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 |
| * @param {string} s |
| * @param {boolean} decodeSpaces |
| * @returns {string} |
| */ |
| function unescapeBuffer(s, decodeSpaces) { |
| const out = Buffer.allocUnsafe(s.length); |
| let index = 0; |
| let outIndex = 0; |
| let currentChar; |
| let nextChar; |
| let hexHigh; |
| let hexLow; |
| const maxLength = s.length - 2; |
| // Flag to know if some hex chars have been decoded |
| let hasHex = false; |
| while (index < s.length) { |
| currentChar = StringPrototypeCharCodeAt(s, index); |
| if (currentChar === 43 /* '+' */ && decodeSpaces) { |
| out[outIndex++] = 32; // ' ' |
| index++; |
| continue; |
| } |
| if (currentChar === 37 /* '%' */ && index < maxLength) { |
| currentChar = StringPrototypeCharCodeAt(s, ++index); |
| hexHigh = unhexTable[currentChar]; |
| if (!(hexHigh >= 0)) { |
| out[outIndex++] = 37; // '%' |
| continue; |
| } else { |
| nextChar = StringPrototypeCharCodeAt(s, ++index); |
| hexLow = unhexTable[nextChar]; |
| if (!(hexLow >= 0)) { |
| out[outIndex++] = 37; // '%' |
| index--; |
| } else { |
| hasHex = true; |
| currentChar = hexHigh * 16 + hexLow; |
| } |
| } |
| } |
| out[outIndex++] = currentChar; |
| index++; |
| } |
| return hasHex ? out.slice(0, outIndex) : out; |
| } |
| |
| /** |
| * @param {string} s |
| * @param {boolean} decodeSpaces |
| * @returns {string} |
| */ |
| function qsUnescape(s, decodeSpaces) { |
| try { |
| return decodeURIComponent(s); |
| } catch { |
| return QueryString.unescapeBuffer(s, decodeSpaces).toString(); |
| } |
| } |
| |
| |
| // These characters do not need escaping when generating query strings: |
| // ! - . _ ~ |
| // ' ( ) * |
| // digits |
| // alpha (uppercase) |
| // alpha (lowercase) |
| const noEscape = new Int8Array([ |
| 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() |
| * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 |
| * @param {any} str |
| * @returns {string} |
| */ |
| function qsEscape(str) { |
| if (typeof str !== 'string') { |
| if (typeof str === 'object') |
| str = String(str); |
| else |
| str += ''; |
| } |
| |
| return encodeStr(str, noEscape, hexTable); |
| } |
| |
| /** |
| * @param {string | number | bigint | boolean | symbol | undefined | null} v |
| * @returns {string} |
| */ |
| function stringifyPrimitive(v) { |
| if (typeof v === 'string') |
| return v; |
| if (typeof v === 'number' && NumberIsFinite(v)) |
| return '' + v; |
| if (typeof v === 'bigint') |
| return '' + v; |
| if (typeof v === 'boolean') |
| return v ? 'true' : 'false'; |
| return ''; |
| } |
| |
| /** |
| * @param {string | number | bigint | boolean} v |
| * @param {(v: string) => string} encode |
| * @returns {string} |
| */ |
| function encodeStringified(v, encode) { |
| if (typeof v === 'string') |
| return (v.length ? encode(v) : ''); |
| if (typeof v === 'number' && NumberIsFinite(v)) { |
| // Values >= 1e21 automatically switch to scientific notation which requires |
| // escaping due to the inclusion of a '+' in the output |
| return (MathAbs(v) < 1e21 ? '' + v : encode('' + v)); |
| } |
| if (typeof v === 'bigint') |
| return '' + v; |
| if (typeof v === 'boolean') |
| return v ? 'true' : 'false'; |
| return ''; |
| } |
| |
| /** |
| * @param {string | number | boolean | null} v |
| * @param {(v: string) => string} encode |
| * @returns {string} |
| */ |
| function encodeStringifiedCustom(v, encode) { |
| return encode(stringifyPrimitive(v)); |
| } |
| |
| /** |
| * @param {Record<string, string | number | boolean |
| * | ReadonlyArray<string | number | boolean> | null>} obj |
| * @param {string} [sep] |
| * @param {string} [eq] |
| * @param {{ encodeURIComponent?: (v: string) => string }} [options] |
| * @returns {string} |
| */ |
| function stringify(obj, sep, eq, options) { |
| sep = sep || '&'; |
| eq = eq || '='; |
| |
| let encode = QueryString.escape; |
| if (options && typeof options.encodeURIComponent === 'function') { |
| encode = options.encodeURIComponent; |
| } |
| const convert = |
| (encode === qsEscape ? encodeStringified : encodeStringifiedCustom); |
| |
| if (obj !== null && typeof obj === 'object') { |
| const keys = ObjectKeys(obj); |
| const len = keys.length; |
| let fields = ''; |
| for (let i = 0; i < len; ++i) { |
| const k = keys[i]; |
| const v = obj[k]; |
| let ks = convert(k, encode); |
| ks += eq; |
| |
| if (ArrayIsArray(v)) { |
| const vlen = v.length; |
| if (vlen === 0) continue; |
| if (fields) |
| fields += sep; |
| for (let j = 0; j < vlen; ++j) { |
| if (j) |
| fields += sep; |
| fields += ks; |
| fields += convert(v[j], encode); |
| } |
| } else { |
| if (fields) |
| fields += sep; |
| fields += ks; |
| fields += convert(v, encode); |
| } |
| } |
| return fields; |
| } |
| return ''; |
| } |
| |
| /** |
| * @param {string} str |
| * @returns {number[]} |
| */ |
| function charCodes(str) { |
| if (str.length === 0) return []; |
| if (str.length === 1) return [StringPrototypeCharCodeAt(str, 0)]; |
| const ret = new Array(str.length); |
| for (let i = 0; i < str.length; ++i) |
| ret[i] = StringPrototypeCharCodeAt(str, i); |
| return ret; |
| } |
| const defSepCodes = [38]; // & |
| const defEqCodes = [61]; // = |
| |
| function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) { |
| if (key.length > 0 && keyEncoded) |
| key = decodeStr(key, decode); |
| if (value.length > 0 && valEncoded) |
| value = decodeStr(value, decode); |
| |
| if (obj[key] === undefined) { |
| obj[key] = value; |
| } 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]; |
| } |
| } |
| |
| /** |
| * Parse a key/val string. |
| * @param {string} qs |
| * @param {string} sep |
| * @param {string} eq |
| * @param {{ |
| * maxKeys?: number; |
| * decodeURIComponent?(v: string): string; |
| * }} [options] |
| * @returns {Record<string, string | string[]>} |
| */ |
| function parse(qs, sep, eq, options) { |
| const obj = ObjectCreate(null); |
| |
| if (typeof qs !== 'string' || qs.length === 0) { |
| return obj; |
| } |
| |
| const sepCodes = (!sep ? defSepCodes : charCodes(String(sep))); |
| const eqCodes = (!eq ? defEqCodes : charCodes(String(eq))); |
| const sepLen = sepCodes.length; |
| const eqLen = eqCodes.length; |
| |
| let 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); |
| } |
| |
| let decode = QueryString.unescape; |
| if (options && typeof options.decodeURIComponent === 'function') { |
| decode = options.decodeURIComponent; |
| } |
| const customDecode = (decode !== qsUnescape); |
| |
| let lastPos = 0; |
| let sepIdx = 0; |
| let eqIdx = 0; |
| let key = ''; |
| let value = ''; |
| let keyEncoded = customDecode; |
| let valEncoded = customDecode; |
| const plusChar = (customDecode ? '%20' : ' '); |
| let encodeCheck = 0; |
| for (let i = 0; i < qs.length; ++i) { |
| const code = StringPrototypeCharCodeAt(qs, 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) { |
| // We didn't find the (entire) key/value separator |
| if (lastPos < end) { |
| // Treat the substring as part of the key instead of the value |
| key += StringPrototypeSlice(qs, lastPos, end); |
| } else if (key.length === 0) { |
| // We saw an empty substring between separators |
| if (--pairs === 0) |
| return obj; |
| lastPos = i + 1; |
| sepIdx = eqIdx = 0; |
| continue; |
| } |
| } else if (lastPos < end) { |
| value += StringPrototypeSlice(qs, lastPos, end); |
| } |
| |
| addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); |
| |
| if (--pairs === 0) |
| return obj; |
| keyEncoded = valEncoded = customDecode; |
| key = value = ''; |
| encodeCheck = 0; |
| lastPos = i + 1; |
| sepIdx = eqIdx = 0; |
| } |
| } else { |
| sepIdx = 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 += StringPrototypeSlice(qs, 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; |
| continue; |
| } else if (encodeCheck > 0) { |
| if (isHexTable[code] === 1) { |
| if (++encodeCheck === 3) |
| keyEncoded = true; |
| continue; |
| } else { |
| encodeCheck = 0; |
| } |
| } |
| } |
| } |
| if (code === 43/* + */) { |
| if (lastPos < i) |
| key += StringPrototypeSlice(qs, lastPos, i); |
| key += plusChar; |
| lastPos = i + 1; |
| continue; |
| } |
| } |
| if (code === 43/* + */) { |
| if (lastPos < i) |
| value += StringPrototypeSlice(qs, lastPos, i); |
| value += plusChar; |
| lastPos = i + 1; |
| } else 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) { |
| if (isHexTable[code] === 1) { |
| if (++encodeCheck === 3) |
| valEncoded = true; |
| } else { |
| encodeCheck = 0; |
| } |
| } |
| } |
| } |
| } |
| |
| // Deal with any leftover key or value data |
| if (lastPos < qs.length) { |
| if (eqIdx < eqLen) |
| key += StringPrototypeSlice(qs, lastPos); |
| else if (sepIdx < sepLen) |
| value += StringPrototypeSlice(qs, lastPos); |
| } else if (eqIdx === 0 && key.length === 0) { |
| // We ended on an empty substring |
| return obj; |
| } |
| |
| addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); |
| |
| 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). |
| * @param {string} s |
| * @param {(v: string) => string} decoder |
| * @returns {string} |
| */ |
| function decodeStr(s, decoder) { |
| try { |
| return decoder(s); |
| } catch { |
| return QueryString.unescape(s, true); |
| } |
| } |