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