repo_description: Node.js JavaScript runtime ✨🐢🚀✨
repo_topics: javascript js linux macos mit node nodejs runtime windows
code: // 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,
    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-[REDACTED_IP]
   * @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 ||= '&';
    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 = { __proto__: 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);
    }
  }
  