• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22// Query String Utilities
23
24'use strict';
25
26const {
27  Array,
28  ArrayIsArray,
29  Int8Array,
30  MathAbs,
31  NumberIsFinite,
32  ObjectCreate,
33  ObjectKeys,
34  String,
35  StringPrototypeCharCodeAt,
36  StringPrototypeSlice,
37  decodeURIComponent,
38} = primordials;
39
40const { Buffer } = require('buffer');
41const {
42  encodeStr,
43  hexTable,
44  isHexTable
45} = require('internal/querystring');
46const QueryString = module.exports = {
47  unescapeBuffer,
48  // `unescape()` is a JS global, so we need to use a different local name
49  unescape: qsUnescape,
50
51  // `escape()` is a JS global, so we need to use a different local name
52  escape: qsEscape,
53
54  stringify,
55  encode: stringify,
56
57  parse,
58  decode: parse
59};
60
61const unhexTable = new Int8Array([
62  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
63  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
64  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
65  +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63
66  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
67  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
68  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
69  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
70  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
71  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
72  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
73  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
74  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
75  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
76  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
77  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // ... 255
78]);
79/**
80 * A safe fast alternative to decodeURIComponent
81 * @param {string} s
82 * @param {boolean} decodeSpaces
83 * @returns {string}
84 */
85function unescapeBuffer(s, decodeSpaces) {
86  const out = Buffer.allocUnsafe(s.length);
87  let index = 0;
88  let outIndex = 0;
89  let currentChar;
90  let nextChar;
91  let hexHigh;
92  let hexLow;
93  const maxLength = s.length - 2;
94  // Flag to know if some hex chars have been decoded
95  let hasHex = false;
96  while (index < s.length) {
97    currentChar = StringPrototypeCharCodeAt(s, index);
98    if (currentChar === 43 /* '+' */ && decodeSpaces) {
99      out[outIndex++] = 32; // ' '
100      index++;
101      continue;
102    }
103    if (currentChar === 37 /* '%' */ && index < maxLength) {
104      currentChar = StringPrototypeCharCodeAt(s, ++index);
105      hexHigh = unhexTable[currentChar];
106      if (!(hexHigh >= 0)) {
107        out[outIndex++] = 37; // '%'
108        continue;
109      } else {
110        nextChar = StringPrototypeCharCodeAt(s, ++index);
111        hexLow = unhexTable[nextChar];
112        if (!(hexLow >= 0)) {
113          out[outIndex++] = 37; // '%'
114          index--;
115        } else {
116          hasHex = true;
117          currentChar = hexHigh * 16 + hexLow;
118        }
119      }
120    }
121    out[outIndex++] = currentChar;
122    index++;
123  }
124  return hasHex ? out.slice(0, outIndex) : out;
125}
126
127/**
128 * @param {string} s
129 * @param {boolean} decodeSpaces
130 * @returns {string}
131 */
132function qsUnescape(s, decodeSpaces) {
133  try {
134    return decodeURIComponent(s);
135  } catch {
136    return QueryString.unescapeBuffer(s, decodeSpaces).toString();
137  }
138}
139
140
141// These characters do not need escaping when generating query strings:
142// ! - . _ ~
143// ' ( ) *
144// digits
145// alpha (uppercase)
146// alpha (lowercase)
147const noEscape = new Int8Array([
148  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
149  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
150  0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47
151  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
152  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
153  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95
154  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
155  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,  // 112 - 127
156]);
157
158/**
159 * QueryString.escape() replaces encodeURIComponent()
160 * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4
161 * @param {any} str
162 * @returns {string}
163 */
164function qsEscape(str) {
165  if (typeof str !== 'string') {
166    if (typeof str === 'object')
167      str = String(str);
168    else
169      str += '';
170  }
171
172  return encodeStr(str, noEscape, hexTable);
173}
174
175/**
176 * @param {string | number | bigint | boolean | symbol | undefined | null} v
177 * @returns {string}
178 */
179function stringifyPrimitive(v) {
180  if (typeof v === 'string')
181    return v;
182  if (typeof v === 'number' && NumberIsFinite(v))
183    return '' + v;
184  if (typeof v === 'bigint')
185    return '' + v;
186  if (typeof v === 'boolean')
187    return v ? 'true' : 'false';
188  return '';
189}
190
191/**
192 * @param {string | number | bigint | boolean} v
193 * @param {(v: string) => string} encode
194 * @returns
195 */
196function encodeStringified(v, encode) {
197  if (typeof v === 'string')
198    return (v.length ? encode(v) : '');
199  if (typeof v === 'number' && NumberIsFinite(v)) {
200    // Values >= 1e21 automatically switch to scientific notation which requires
201    // escaping due to the inclusion of a '+' in the output
202    return (MathAbs(v) < 1e21 ? '' + v : encode('' + v));
203  }
204  if (typeof v === 'bigint')
205    return '' + v;
206  if (typeof v === 'boolean')
207    return v ? 'true' : 'false';
208  return '';
209}
210
211/**
212 * @param {string | number | boolean | null} v
213 * @param {(v: string) => string} encode
214 * @returns {string}
215 */
216function encodeStringifiedCustom(v, encode) {
217  return encode(stringifyPrimitive(v));
218}
219
220/**
221 * @param {Record<string, string | number | boolean
222 * | ReadonlyArray<string | number | boolean> | null>} obj
223 * @param {string} [sep]
224 * @param {string} [eq]
225 * @param {{ encodeURIComponent?: (v: string) => string }} [options]
226 * @returns {string}
227 */
228function stringify(obj, sep, eq, options) {
229  sep = sep || '&';
230  eq = eq || '=';
231
232  let encode = QueryString.escape;
233  if (options && typeof options.encodeURIComponent === 'function') {
234    encode = options.encodeURIComponent;
235  }
236  const convert =
237    (encode === qsEscape ? encodeStringified : encodeStringifiedCustom);
238
239  if (obj !== null && typeof obj === 'object') {
240    const keys = ObjectKeys(obj);
241    const len = keys.length;
242    let fields = '';
243    for (let i = 0; i < len; ++i) {
244      const k = keys[i];
245      const v = obj[k];
246      let ks = convert(k, encode);
247      ks += eq;
248
249      if (ArrayIsArray(v)) {
250        const vlen = v.length;
251        if (vlen === 0) continue;
252        if (fields)
253          fields += sep;
254        for (let j = 0; j < vlen; ++j) {
255          if (j)
256            fields += sep;
257          fields += ks;
258          fields += convert(v[j], encode);
259        }
260      } else {
261        if (fields)
262          fields += sep;
263        fields += ks;
264        fields += convert(v, encode);
265      }
266    }
267    return fields;
268  }
269  return '';
270}
271
272/**
273 * @param {string} str
274 * @returns {number[]}
275 */
276function charCodes(str) {
277  if (str.length === 0) return [];
278  if (str.length === 1) return [StringPrototypeCharCodeAt(str, 0)];
279  const ret = new Array(str.length);
280  for (let i = 0; i < str.length; ++i)
281    ret[i] = StringPrototypeCharCodeAt(str, i);
282  return ret;
283}
284const defSepCodes = [38]; // &
285const defEqCodes = [61]; // =
286
287function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
288  if (key.length > 0 && keyEncoded)
289    key = decodeStr(key, decode);
290  if (value.length > 0 && valEncoded)
291    value = decodeStr(value, decode);
292
293  if (obj[key] === undefined) {
294    obj[key] = value;
295  } else {
296    const curValue = obj[key];
297    // A simple Array-specific property check is enough here to
298    // distinguish from a string value and is faster and still safe
299    // since we are generating all of the values being assigned.
300    if (curValue.pop)
301      curValue[curValue.length] = value;
302    else
303      obj[key] = [curValue, value];
304  }
305}
306
307/**
308 * Parse a key/val string.
309 * @param {string} qs
310 * @param {string} sep
311 * @param {string} eq
312 * @param {{
313 *   maxKeys?: number;
314 *   decodeURIComponent?(v: string): string;
315 *   }} [options]
316 * @returns {Record<string, string | string[]>}
317 */
318function parse(qs, sep, eq, options) {
319  const obj = ObjectCreate(null);
320
321  if (typeof qs !== 'string' || qs.length === 0) {
322    return obj;
323  }
324
325  const sepCodes = (!sep ? defSepCodes : charCodes(String(sep)));
326  const eqCodes = (!eq ? defEqCodes : charCodes(String(eq)));
327  const sepLen = sepCodes.length;
328  const eqLen = eqCodes.length;
329
330  let pairs = 1000;
331  if (options && typeof options.maxKeys === 'number') {
332    // -1 is used in place of a value like Infinity for meaning
333    // "unlimited pairs" because of additional checks V8 (at least as of v5.4)
334    // has to do when using variables that contain values like Infinity. Since
335    // `pairs` is always decremented and checked explicitly for 0, -1 works
336    // effectively the same as Infinity, while providing a significant
337    // performance boost.
338    pairs = (options.maxKeys > 0 ? options.maxKeys : -1);
339  }
340
341  let decode = QueryString.unescape;
342  if (options && typeof options.decodeURIComponent === 'function') {
343    decode = options.decodeURIComponent;
344  }
345  const customDecode = (decode !== qsUnescape);
346
347  let lastPos = 0;
348  let sepIdx = 0;
349  let eqIdx = 0;
350  let key = '';
351  let value = '';
352  let keyEncoded = customDecode;
353  let valEncoded = customDecode;
354  const plusChar = (customDecode ? '%20' : ' ');
355  let encodeCheck = 0;
356  for (let i = 0; i < qs.length; ++i) {
357    const code = StringPrototypeCharCodeAt(qs, i);
358
359    // Try matching key/value pair separator (e.g. '&')
360    if (code === sepCodes[sepIdx]) {
361      if (++sepIdx === sepLen) {
362        // Key/value pair separator match!
363        const end = i - sepIdx + 1;
364        if (eqIdx < eqLen) {
365          // We didn't find the (entire) key/value separator
366          if (lastPos < end) {
367            // Treat the substring as part of the key instead of the value
368            key += StringPrototypeSlice(qs, lastPos, end);
369          } else if (key.length === 0) {
370            // We saw an empty substring between separators
371            if (--pairs === 0)
372              return obj;
373            lastPos = i + 1;
374            sepIdx = eqIdx = 0;
375            continue;
376          }
377        } else if (lastPos < end) {
378          value += StringPrototypeSlice(qs, lastPos, end);
379        }
380
381        addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
382
383        if (--pairs === 0)
384          return obj;
385        keyEncoded = valEncoded = customDecode;
386        key = value = '';
387        encodeCheck = 0;
388        lastPos = i + 1;
389        sepIdx = eqIdx = 0;
390      }
391    } else {
392      sepIdx = 0;
393      // Try matching key/value separator (e.g. '=') if we haven't already
394      if (eqIdx < eqLen) {
395        if (code === eqCodes[eqIdx]) {
396          if (++eqIdx === eqLen) {
397            // Key/value separator match!
398            const end = i - eqIdx + 1;
399            if (lastPos < end)
400              key += StringPrototypeSlice(qs, lastPos, end);
401            encodeCheck = 0;
402            lastPos = i + 1;
403          }
404          continue;
405        } else {
406          eqIdx = 0;
407          if (!keyEncoded) {
408            // Try to match an (valid) encoded byte once to minimize unnecessary
409            // calls to string decoding functions
410            if (code === 37/* % */) {
411              encodeCheck = 1;
412              continue;
413            } else if (encodeCheck > 0) {
414              if (isHexTable[code] === 1) {
415                if (++encodeCheck === 3)
416                  keyEncoded = true;
417                continue;
418              } else {
419                encodeCheck = 0;
420              }
421            }
422          }
423        }
424        if (code === 43/* + */) {
425          if (lastPos < i)
426            key += StringPrototypeSlice(qs, lastPos, i);
427          key += plusChar;
428          lastPos = i + 1;
429          continue;
430        }
431      }
432      if (code === 43/* + */) {
433        if (lastPos < i)
434          value += StringPrototypeSlice(qs, lastPos, i);
435        value += plusChar;
436        lastPos = i + 1;
437      } else if (!valEncoded) {
438        // Try to match an (valid) encoded byte (once) to minimize unnecessary
439        // calls to string decoding functions
440        if (code === 37/* % */) {
441          encodeCheck = 1;
442        } else if (encodeCheck > 0) {
443          if (isHexTable[code] === 1) {
444            if (++encodeCheck === 3)
445              valEncoded = true;
446          } else {
447            encodeCheck = 0;
448          }
449        }
450      }
451    }
452  }
453
454  // Deal with any leftover key or value data
455  if (lastPos < qs.length) {
456    if (eqIdx < eqLen)
457      key += StringPrototypeSlice(qs, lastPos);
458    else if (sepIdx < sepLen)
459      value += StringPrototypeSlice(qs, lastPos);
460  } else if (eqIdx === 0 && key.length === 0) {
461    // We ended on an empty substring
462    return obj;
463  }
464
465  addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
466
467  return obj;
468}
469
470
471/**
472 * V8 does not optimize functions with try-catch blocks, so we isolate them here
473 * to minimize the damage (Note: no longer true as of V8 5.4 -- but still will
474 * not be inlined).
475 * @param {string} s
476 * @param {(v: string) => string} decoder
477 * @returns {string}
478 */
479function decodeStr(s, decoder) {
480  try {
481    return decoder(s);
482  } catch {
483    return QueryString.unescape(s, true);
484  }
485}
486