• 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  MathAbs,
30  ObjectCreate,
31  ObjectKeys,
32} = primordials;
33
34const { Buffer } = require('buffer');
35const {
36  encodeStr,
37  hexTable,
38  isHexTable
39} = require('internal/querystring');
40const QueryString = module.exports = {
41  unescapeBuffer,
42  // `unescape()` is a JS global, so we need to use a different local name
43  unescape: qsUnescape,
44
45  // `escape()` is a JS global, so we need to use a different local name
46  escape: qsEscape,
47
48  stringify,
49  encode: stringify,
50
51  parse,
52  decode: parse
53};
54
55const unhexTable = [
56  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
57  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
58  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
59  +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63
60  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
61  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
62  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
63  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
64  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
65  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
66  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
67  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
68  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
69  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
70  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
71  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1  // ... 255
72];
73// A safe fast alternative to decodeURIComponent
74function unescapeBuffer(s, decodeSpaces) {
75  const out = Buffer.allocUnsafe(s.length);
76  let index = 0;
77  let outIndex = 0;
78  let currentChar;
79  let nextChar;
80  let hexHigh;
81  let hexLow;
82  const maxLength = s.length - 2;
83  // Flag to know if some hex chars have been decoded
84  let hasHex = false;
85  while (index < s.length) {
86    currentChar = s.charCodeAt(index);
87    if (currentChar === 43 /* '+' */ && decodeSpaces) {
88      out[outIndex++] = 32; // ' '
89      index++;
90      continue;
91    }
92    if (currentChar === 37 /* '%' */ && index < maxLength) {
93      currentChar = s.charCodeAt(++index);
94      hexHigh = unhexTable[currentChar];
95      if (!(hexHigh >= 0)) {
96        out[outIndex++] = 37; // '%'
97        continue;
98      } else {
99        nextChar = s.charCodeAt(++index);
100        hexLow = unhexTable[nextChar];
101        if (!(hexLow >= 0)) {
102          out[outIndex++] = 37; // '%'
103          index--;
104        } else {
105          hasHex = true;
106          currentChar = hexHigh * 16 + hexLow;
107        }
108      }
109    }
110    out[outIndex++] = currentChar;
111    index++;
112  }
113  return hasHex ? out.slice(0, outIndex) : out;
114}
115
116
117function qsUnescape(s, decodeSpaces) {
118  try {
119    return decodeURIComponent(s);
120  } catch {
121    return QueryString.unescapeBuffer(s, decodeSpaces).toString();
122  }
123}
124
125
126// These characters do not need escaping when generating query strings:
127// ! - . _ ~
128// ' ( ) *
129// digits
130// alpha (uppercase)
131// alpha (lowercase)
132const noEscape = [
133  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
134  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
135  0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47
136  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
137  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
138  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95
139  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
140  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0  // 112 - 127
141];
142// QueryString.escape() replaces encodeURIComponent()
143// https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4
144function qsEscape(str) {
145  if (typeof str !== 'string') {
146    if (typeof str === 'object')
147      str = String(str);
148    else
149      str += '';
150  }
151
152  return encodeStr(str, noEscape, hexTable);
153}
154
155function stringifyPrimitive(v) {
156  if (typeof v === 'string')
157    return v;
158  if (typeof v === 'number' && isFinite(v))
159    return '' + v;
160  if (typeof v === 'boolean')
161    return v ? 'true' : 'false';
162  return '';
163}
164
165
166function encodeStringified(v, encode) {
167  if (typeof v === 'string')
168    return (v.length ? encode(v) : '');
169  if (typeof v === 'number' && isFinite(v)) {
170    // Values >= 1e21 automatically switch to scientific notation which requires
171    // escaping due to the inclusion of a '+' in the output
172    return (MathAbs(v) < 1e21 ? '' + v : encode('' + v));
173  }
174  if (typeof v === 'boolean')
175    return v ? 'true' : 'false';
176  return '';
177}
178
179
180function encodeStringifiedCustom(v, encode) {
181  return encode(stringifyPrimitive(v));
182}
183
184
185function stringify(obj, sep, eq, options) {
186  sep = sep || '&';
187  eq = eq || '=';
188
189  let encode = QueryString.escape;
190  if (options && typeof options.encodeURIComponent === 'function') {
191    encode = options.encodeURIComponent;
192  }
193  const convert =
194    (encode === qsEscape ? encodeStringified : encodeStringifiedCustom);
195
196  if (obj !== null && typeof obj === 'object') {
197    const keys = ObjectKeys(obj);
198    const len = keys.length;
199    let fields = '';
200    for (let i = 0; i < len; ++i) {
201      const k = keys[i];
202      const v = obj[k];
203      let ks = convert(k, encode);
204      ks += eq;
205
206      if (ArrayIsArray(v)) {
207        const vlen = v.length;
208        if (vlen === 0) continue;
209        if (fields)
210          fields += sep;
211        for (let j = 0; j < vlen; ++j) {
212          if (j)
213            fields += sep;
214          fields += ks;
215          fields += convert(v[j], encode);
216        }
217      } else {
218        if (fields)
219          fields += sep;
220        fields += ks;
221        fields += convert(v, encode);
222      }
223    }
224    return fields;
225  }
226  return '';
227}
228
229function charCodes(str) {
230  if (str.length === 0) return [];
231  if (str.length === 1) return [str.charCodeAt(0)];
232  const ret = new Array(str.length);
233  for (let i = 0; i < str.length; ++i)
234    ret[i] = str.charCodeAt(i);
235  return ret;
236}
237const defSepCodes = [38]; // &
238const defEqCodes = [61]; // =
239
240function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
241  if (key.length > 0 && keyEncoded)
242    key = decodeStr(key, decode);
243  if (value.length > 0 && valEncoded)
244    value = decodeStr(value, decode);
245
246  if (obj[key] === undefined) {
247    obj[key] = value;
248  } else {
249    const curValue = obj[key];
250    // A simple Array-specific property check is enough here to
251    // distinguish from a string value and is faster and still safe
252    // since we are generating all of the values being assigned.
253    if (curValue.pop)
254      curValue[curValue.length] = value;
255    else
256      obj[key] = [curValue, value];
257  }
258}
259
260// Parse a key/val string.
261function parse(qs, sep, eq, options) {
262  const obj = ObjectCreate(null);
263
264  if (typeof qs !== 'string' || qs.length === 0) {
265    return obj;
266  }
267
268  const sepCodes = (!sep ? defSepCodes : charCodes(sep + ''));
269  const eqCodes = (!eq ? defEqCodes : charCodes(eq + ''));
270  const sepLen = sepCodes.length;
271  const eqLen = eqCodes.length;
272
273  let pairs = 1000;
274  if (options && typeof options.maxKeys === 'number') {
275    // -1 is used in place of a value like Infinity for meaning
276    // "unlimited pairs" because of additional checks V8 (at least as of v5.4)
277    // has to do when using variables that contain values like Infinity. Since
278    // `pairs` is always decremented and checked explicitly for 0, -1 works
279    // effectively the same as Infinity, while providing a significant
280    // performance boost.
281    pairs = (options.maxKeys > 0 ? options.maxKeys : -1);
282  }
283
284  let decode = QueryString.unescape;
285  if (options && typeof options.decodeURIComponent === 'function') {
286    decode = options.decodeURIComponent;
287  }
288  const customDecode = (decode !== qsUnescape);
289
290  let lastPos = 0;
291  let sepIdx = 0;
292  let eqIdx = 0;
293  let key = '';
294  let value = '';
295  let keyEncoded = customDecode;
296  let valEncoded = customDecode;
297  const plusChar = (customDecode ? '%20' : ' ');
298  let encodeCheck = 0;
299  for (let i = 0; i < qs.length; ++i) {
300    const code = qs.charCodeAt(i);
301
302    // Try matching key/value pair separator (e.g. '&')
303    if (code === sepCodes[sepIdx]) {
304      if (++sepIdx === sepLen) {
305        // Key/value pair separator match!
306        const end = i - sepIdx + 1;
307        if (eqIdx < eqLen) {
308          // We didn't find the (entire) key/value separator
309          if (lastPos < end) {
310            // Treat the substring as part of the key instead of the value
311            key += qs.slice(lastPos, end);
312          } else if (key.length === 0) {
313            // We saw an empty substring between separators
314            if (--pairs === 0)
315              return obj;
316            lastPos = i + 1;
317            sepIdx = eqIdx = 0;
318            continue;
319          }
320        } else if (lastPos < end) {
321          value += qs.slice(lastPos, end);
322        }
323
324        addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
325
326        if (--pairs === 0)
327          return obj;
328        keyEncoded = valEncoded = customDecode;
329        key = value = '';
330        encodeCheck = 0;
331        lastPos = i + 1;
332        sepIdx = eqIdx = 0;
333      }
334    } else {
335      sepIdx = 0;
336      // Try matching key/value separator (e.g. '=') if we haven't already
337      if (eqIdx < eqLen) {
338        if (code === eqCodes[eqIdx]) {
339          if (++eqIdx === eqLen) {
340            // Key/value separator match!
341            const end = i - eqIdx + 1;
342            if (lastPos < end)
343              key += qs.slice(lastPos, end);
344            encodeCheck = 0;
345            lastPos = i + 1;
346          }
347          continue;
348        } else {
349          eqIdx = 0;
350          if (!keyEncoded) {
351            // Try to match an (valid) encoded byte once to minimize unnecessary
352            // calls to string decoding functions
353            if (code === 37/* % */) {
354              encodeCheck = 1;
355              continue;
356            } else if (encodeCheck > 0) {
357              if (isHexTable[code] === 1) {
358                if (++encodeCheck === 3)
359                  keyEncoded = true;
360                continue;
361              } else {
362                encodeCheck = 0;
363              }
364            }
365          }
366        }
367        if (code === 43/* + */) {
368          if (lastPos < i)
369            key += qs.slice(lastPos, i);
370          key += plusChar;
371          lastPos = i + 1;
372          continue;
373        }
374      }
375      if (code === 43/* + */) {
376        if (lastPos < i)
377          value += qs.slice(lastPos, i);
378        value += plusChar;
379        lastPos = i + 1;
380      } else if (!valEncoded) {
381        // Try to match an (valid) encoded byte (once) to minimize unnecessary
382        // calls to string decoding functions
383        if (code === 37/* % */) {
384          encodeCheck = 1;
385        } else if (encodeCheck > 0) {
386          if (isHexTable[code] === 1) {
387            if (++encodeCheck === 3)
388              valEncoded = true;
389          } else {
390            encodeCheck = 0;
391          }
392        }
393      }
394    }
395  }
396
397  // Deal with any leftover key or value data
398  if (lastPos < qs.length) {
399    if (eqIdx < eqLen)
400      key += qs.slice(lastPos);
401    else if (sepIdx < sepLen)
402      value += qs.slice(lastPos);
403  } else if (eqIdx === 0 && key.length === 0) {
404    // We ended on an empty substring
405    return obj;
406  }
407
408  addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
409
410  return obj;
411}
412
413
414// v8 does not optimize functions with try-catch blocks, so we isolate them here
415// to minimize the damage (Note: no longer true as of V8 5.4 -- but still will
416// not be inlined).
417function decodeStr(s, decoder) {
418  try {
419    return decoder(s);
420  } catch {
421    return QueryString.unescape(s, true);
422  }
423}
424