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