File
Blob: src/node/internal/internal_querystring.ts
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | // Copyright Joyent and Node contributors. All rights reserved. MIT license. |
| 5 | |
| 6 | import { Buffer } from 'node-internal:internal_buffer'; |
| 7 | import { ERR_INVALID_URI } from 'node-internal:internal_errors'; |
| 8 | |
| 9 | type EncodeFunction = (value: string) => string; |
| 10 | type DecodeFunction = (value: string) => string; |
| 11 | |
| 12 | export const hexTable = Array.from({ length: 256 }, (_, i) => { |
| 13 | return '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); |
| 14 | }); |
| 15 | |
| 16 | // prettier-ignore |
| 17 | const isHexTable = new Int8Array([ |
| 18 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 |
| 19 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 |
| 20 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47 |
| 21 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 |
| 22 | 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79 |
| 23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95 |
| 24 | 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111 |
| 25 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127 |
| 26 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... |
| 27 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 29 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 30 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 31 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 32 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
| 33 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ... 256 |
| 34 | ]); |
| 35 | |
| 36 | /* eslint-disable */ |
| 37 | export function encodeStr( |
| 38 | str: string, |
| 39 | noEscapeTable: Int8Array, |
| 40 | hexTable: string[] |
| 41 | ): string { |
| 42 | const len = str.length; |
| 43 | if (len === 0) return ''; |
| 44 | |
| 45 | let out = ''; |
| 46 | let lastPos = 0; |
| 47 | let i = 0; |
| 48 | |
| 49 | outer: for (; i < len; i++) { |
| 50 | let c = str.charCodeAt(i); |
| 51 | |
| 52 | // ASCII |
| 53 | while (c < 0x80) { |
| 54 | if (noEscapeTable[c] !== 1) { |
| 55 | if (lastPos < i) out += str.slice(lastPos, i); |
| 56 | lastPos = i + 1; |
| 57 | out += hexTable[c]!; |
| 58 | } |
| 59 | |
| 60 | if (++i === len) break outer; |
| 61 | |
| 62 | c = str.charCodeAt(i); |
| 63 | } |
| 64 | |
| 65 | if (lastPos < i) out += str.slice(lastPos, i); |
| 66 | |
| 67 | // Multi-byte characters ... |
| 68 | if (c < 0x800) { |
| 69 | lastPos = i + 1; |
| 70 | out += hexTable[0xc0 | (c >> 6)]! + hexTable[0x80 | (c & 0x3f)]!; |
| 71 | continue; |
| 72 | } |
| 73 | if (c < 0xd800 || c >= 0xe000) { |
| 74 | lastPos = i + 1; |
| 75 | out += |
| 76 | hexTable[0xe0 | (c >> 12)]! + |
| 77 | hexTable[0x80 | ((c >> 6) & 0x3f)]! + |
| 78 | hexTable[0x80 | (c & 0x3f)]; |
| 79 | continue; |
| 80 | } |
| 81 | // Surrogate pair |
| 82 | ++i; |
| 83 | |
| 84 | // This branch should never happen because all URLSearchParams entries |
| 85 | // should already be converted to USVString. But, included for |
| 86 | // completion's sake anyway. |
| 87 | if (i >= len) throw new ERR_INVALID_URI(); |
| 88 | |
| 89 | const c2 = str.charCodeAt(i) & 0x3ff; |
| 90 | |
| 91 | lastPos = i + 1; |
| 92 | c = 0x10000 + (((c & 0x3ff) << 10) | c2); |
| 93 | out += |
| 94 | hexTable[0xf0 | (c >> 18)]! + |
| 95 | hexTable[0x80 | ((c >> 12) & 0x3f)]! + |
| 96 | hexTable[0x80 | ((c >> 6) & 0x3f)] + |
| 97 | hexTable[0x80 | (c & 0x3f)]; |
| 98 | } |
| 99 | if (lastPos === 0) return str; |
| 100 | if (lastPos < len) return out + str.slice(lastPos); |
| 101 | return out; |
| 102 | } |
| 103 | /* eslint-enable */ |
| 104 | |
| 105 | /* eslint-disable @typescript-eslint/no-unnecessary-type-conversion */ |
| 106 | |
| 107 | // prettier-ignore |
| 108 | const unhexTable = new Int8Array([ |
| 109 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15 |
| 110 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31 |
| 111 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47 |
| 112 | +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63 |
| 113 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79 |
| 114 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95 |
| 115 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111 |
| 116 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127 |
| 117 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ... |
| 118 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 119 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 120 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 121 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 122 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 123 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 124 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255 |
| 125 | ]); |
| 126 | |
| 127 | export function unescapeBuffer( |
| 128 | s: string, |
| 129 | decodeSpaces: boolean |
| 130 | ): Buffer | Uint8Array { |
| 131 | const out = Buffer.allocUnsafe(s.length); |
| 132 | let index: number = 0; |
| 133 | let outIndex: number = 0; |
| 134 | let currentChar: number; |
| 135 | let nextChar: number; |
| 136 | let hexHigh: number; |
| 137 | let hexLow: number; |
| 138 | const maxLength = s.length - 2; |
| 139 | // Flag to know if some hex chars have been decoded |
| 140 | let hasHex = false; |
| 141 | while (index < s.length) { |
| 142 | currentChar = s.charCodeAt(index); |
| 143 | if (currentChar === 43 /* '+' */ && decodeSpaces) { |
| 144 | out[outIndex++] = 32; // ' ' |
| 145 | index++; |
| 146 | continue; |
| 147 | } |
| 148 | if (currentChar === 37 /* '%' */ && index < maxLength) { |
| 149 | currentChar = s.charCodeAt(++index); |
| 150 | hexHigh = unhexTable[currentChar] as number; |
| 151 | if (!(hexHigh >= 0)) { |
| 152 | out[outIndex++] = 37; // '%' |
| 153 | continue; |
| 154 | } else { |
| 155 | nextChar = s.charCodeAt(++index); |
| 156 | hexLow = unhexTable[nextChar] as number; |
| 157 | if (!(hexLow >= 0)) { |
| 158 | out[outIndex++] = 37; // '%' |
| 159 | index--; |
| 160 | } else { |
| 161 | hasHex = true; |
| 162 | currentChar = hexHigh * 16 + hexLow; |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | out[outIndex++] = currentChar; |
| 167 | index++; |
| 168 | } |
| 169 | return hasHex ? out.slice(0, outIndex) : out; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * @param {string} s |
| 174 | * @param {boolean} decodeSpaces |
| 175 | * @returns {string} |
| 176 | */ |
| 177 | export function unescape(s: string, decodeSpaces: boolean): string { |
| 178 | try { |
| 179 | return decodeURIComponent(s); |
| 180 | } catch { |
| 181 | return unescapeBuffer(s, decodeSpaces).toString(); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // These characters do not need escaping when generating query strings: |
| 186 | // ! - . _ ~ |
| 187 | // ' ( ) * |
| 188 | // digits |
| 189 | // alpha (uppercase) |
| 190 | // alpha (lowercase) |
| 191 | |
| 192 | // prettier-ignore |
| 193 | const noEscape = new Int8Array([ |
| 194 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 |
| 195 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 |
| 196 | 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47 |
| 197 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 |
| 198 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 |
| 199 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95 |
| 200 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 |
| 201 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 |
| 202 | ]); |
| 203 | |
| 204 | /** |
| 205 | * QueryString.escape() replaces encodeURIComponent() |
| 206 | * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 |
| 207 | * @param {any} input |
| 208 | * @returns {string} |
| 209 | */ |
| 210 | export function escape(input: unknown): string { |
| 211 | let str: string; |
| 212 | if (typeof input !== 'string') { |
| 213 | // eslint-disable-next-line @typescript-eslint/no-base-to-string |
| 214 | if (typeof input === 'object') str = String(input); |
| 215 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands,@typescript-eslint/no-base-to-string |
| 216 | else str = input + ''; |
| 217 | } else { |
| 218 | str = input; |
| 219 | } |
| 220 | |
| 221 | return encodeStr(str, noEscape, hexTable); |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * @param {string | number | bigint | boolean | symbol | undefined | null} v |
| 226 | * @returns {string} |
| 227 | */ |
| 228 | function stringifyPrimitive(v: unknown): string { |
| 229 | if (typeof v === 'string') return v; |
| 230 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands |
| 231 | if (typeof v === 'number' && Number.isFinite(v)) return '' + v; |
| 232 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands |
| 233 | if (typeof v === 'bigint') return '' + v; |
| 234 | if (typeof v === 'boolean') return v ? 'true' : 'false'; |
| 235 | return ''; |
| 236 | } |
| 237 | |
| 238 | function encodeStringified(v: unknown, encode: EncodeFunction): string { |
| 239 | if (typeof v === 'string') return v.length ? encode(v) : ''; |
| 240 | if (typeof v === 'number' && Number.isFinite(v)) { |
| 241 | // Values >= 1e21 automatically switch to scientific notation which requires |
| 242 | // escaping due to the inclusion of a '+' in the output |
| 243 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands |
| 244 | return Math.abs(v) < 1e21 ? '' + v : encode('' + v); |
| 245 | } |
| 246 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands |
| 247 | if (typeof v === 'bigint') return '' + v; |
| 248 | if (typeof v === 'boolean') return v ? 'true' : 'false'; |
| 249 | return ''; |
| 250 | } |
| 251 | |
| 252 | function encodeStringifiedCustom(v: unknown, encode: EncodeFunction): string { |
| 253 | return encode(stringifyPrimitive(v)); |
| 254 | } |
| 255 | |
| 256 | export function stringify( |
| 257 | obj: unknown, |
| 258 | sep?: string, |
| 259 | eq?: string, |
| 260 | options?: { encodeURIComponent?: EncodeFunction } |
| 261 | ): string { |
| 262 | sep ||= '&'; |
| 263 | eq ||= '='; |
| 264 | |
| 265 | let encode = escape as EncodeFunction; |
| 266 | if (options && typeof options.encodeURIComponent === 'function') { |
| 267 | encode = options.encodeURIComponent; |
| 268 | } |
| 269 | const convert = |
| 270 | encode === escape ? encodeStringified : encodeStringifiedCustom; |
| 271 | |
| 272 | if (obj !== null && typeof obj === 'object') { |
| 273 | const keys = Object.keys(obj); |
| 274 | const len = keys.length; |
| 275 | let fields = ''; |
| 276 | for (let i = 0; i < len; ++i) { |
| 277 | const k = keys[i] as keyof typeof obj; |
| 278 | const v = obj[k] as unknown; |
| 279 | let ks = convert(k, encode); |
| 280 | ks += eq; |
| 281 | |
| 282 | if (Array.isArray(v)) { |
| 283 | const vlen = v.length; |
| 284 | if (vlen === 0) continue; |
| 285 | if (fields) fields += sep; |
| 286 | for (let j = 0; j < vlen; ++j) { |
| 287 | if (j) fields += sep; |
| 288 | fields += ks; |
| 289 | fields += convert(v[j], encode); |
| 290 | } |
| 291 | } else { |
| 292 | if (fields) fields += sep; |
| 293 | fields += ks; |
| 294 | fields += convert(v, encode); |
| 295 | } |
| 296 | } |
| 297 | return fields; |
| 298 | } |
| 299 | return ''; |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * @param {string} str |
| 304 | * @returns {number[]} |
| 305 | */ |
| 306 | function charCodes(str: string): number[] { |
| 307 | if (str.length === 0) return []; |
| 308 | if (str.length === 1) return [str.charCodeAt(0)]; |
| 309 | return Array.from({ length: str.length }, (_, i) => str.charCodeAt(i)); |
| 310 | } |
| 311 | const defSepCodes = [38]; // & |
| 312 | const defEqCodes = [61]; // = |
| 313 | |
| 314 | function addKeyVal( |
| 315 | obj: Record<string, unknown>, |
| 316 | key: string, |
| 317 | value: string, |
| 318 | keyEncoded: boolean, |
| 319 | valEncoded: boolean, |
| 320 | decode: DecodeFunction |
| 321 | ): void { |
| 322 | if (key.length > 0 && keyEncoded) key = decodeStr(key, decode); |
| 323 | if (value.length > 0 && valEncoded) value = decodeStr(value, decode); |
| 324 | |
| 325 | if (obj[key] === undefined) { |
| 326 | obj[key] = value; |
| 327 | } else { |
| 328 | const curValue = obj[key]; |
| 329 | // A simple Array-specific property check is enough here to |
| 330 | // distinguish from a string value and is faster and still safe |
| 331 | // since we are generating all of the values being assigned. |
| 332 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 333 | // @ts-expect-error TS18046 |
| 334 | if (curValue.pop) { |
| 335 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 336 | // @ts-expect-error TS18046 |
| 337 | curValue[curValue.length] = value; // eslint-disable-line @typescript-eslint/no-unsafe-member-access |
| 338 | } else { |
| 339 | obj[key] = [curValue, value]; |
| 340 | } |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | export function parse( |
| 345 | qs: string, |
| 346 | sep?: string, |
| 347 | eq?: string, |
| 348 | options?: { |
| 349 | maxKeys?: number; |
| 350 | decodeURIComponent?: DecodeFunction; |
| 351 | } |
| 352 | ): Record<string, unknown> { |
| 353 | const obj = { __proto__: null }; |
| 354 | |
| 355 | if (typeof qs !== 'string' || qs.length === 0) { |
| 356 | return obj; |
| 357 | } |
| 358 | |
| 359 | const sepCodes = !sep ? defSepCodes : charCodes(String(sep)); |
| 360 | const eqCodes = !eq ? defEqCodes : charCodes(String(eq)); |
| 361 | const sepLen = sepCodes.length; |
| 362 | const eqLen = eqCodes.length; |
| 363 | |
| 364 | let pairs = 1000; |
| 365 | if (options && typeof options.maxKeys === 'number') { |
| 366 | // -1 is used in place of a value like Infinity for meaning |
| 367 | // "unlimited pairs" because of additional checks V8 (at least as of v5.4) |
| 368 | // has to do when using variables that contain values like Infinity. Since |
| 369 | // `pairs` is always decremented and checked explicitly for 0, -1 works |
| 370 | // effectively the same as Infinity, while providing a significant |
| 371 | // performance boost. |
| 372 | pairs = options.maxKeys > 0 ? options.maxKeys : -1; |
| 373 | } |
| 374 | |
| 375 | let decode = unescape as DecodeFunction; |
| 376 | if (options && typeof options.decodeURIComponent === 'function') { |
| 377 | decode = options.decodeURIComponent; |
| 378 | } |
| 379 | const customDecode = decode !== unescape; |
| 380 | |
| 381 | let lastPos = 0; |
| 382 | let sepIdx = 0; |
| 383 | let eqIdx = 0; |
| 384 | let key = ''; |
| 385 | let value = ''; |
| 386 | let keyEncoded = customDecode; |
| 387 | let valEncoded = customDecode; |
| 388 | const plusChar = customDecode ? '%20' : ' '; |
| 389 | let encodeCheck = 0; |
| 390 | for (let i = 0; i < qs.length; ++i) { |
| 391 | const code = qs.charCodeAt(i); |
| 392 | |
| 393 | // Try matching key/value pair separator (e.g. '&') |
| 394 | if (code === sepCodes[sepIdx]) { |
| 395 | if (++sepIdx === sepLen) { |
| 396 | // Key/value pair separator match! |
| 397 | const end = i - sepIdx + 1; |
| 398 | if (eqIdx < eqLen) { |
| 399 | // We didn't find the (entire) key/value separator |
| 400 | if (lastPos < end) { |
| 401 | // Treat the substring as part of the key instead of the value |
| 402 | key += qs.slice(lastPos, end); |
| 403 | } else if (key.length === 0) { |
| 404 | // We saw an empty substring between separators |
| 405 | if (--pairs === 0) return obj; |
| 406 | lastPos = i + 1; |
| 407 | sepIdx = eqIdx = 0; |
| 408 | continue; |
| 409 | } |
| 410 | } else if (lastPos < end) { |
| 411 | value += qs.slice(lastPos, end); |
| 412 | } |
| 413 | |
| 414 | addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); |
| 415 | |
| 416 | if (--pairs === 0) return obj; |
| 417 | keyEncoded = valEncoded = customDecode; |
| 418 | key = value = ''; |
| 419 | encodeCheck = 0; |
| 420 | lastPos = i + 1; |
| 421 | sepIdx = eqIdx = 0; |
| 422 | } |
| 423 | } else { |
| 424 | sepIdx = 0; |
| 425 | // Try matching key/value separator (e.g. '=') if we haven't already |
| 426 | if (eqIdx < eqLen) { |
| 427 | if (code === eqCodes[eqIdx]) { |
| 428 | if (++eqIdx === eqLen) { |
| 429 | // Key/value separator match! |
| 430 | const end = i - eqIdx + 1; |
| 431 | if (lastPos < end) key += qs.slice(lastPos, end); |
| 432 | encodeCheck = 0; |
| 433 | lastPos = i + 1; |
| 434 | } |
| 435 | continue; |
| 436 | } else { |
| 437 | eqIdx = 0; |
| 438 | if (!keyEncoded) { |
| 439 | // Try to match an (valid) encoded byte once to minimize unnecessary |
| 440 | // calls to string decoding functions |
| 441 | if (code === 37 /* % */) { |
| 442 | encodeCheck = 1; |
| 443 | continue; |
| 444 | } else if (encodeCheck > 0) { |
| 445 | if (isHexTable[code] === 1) { |
| 446 | if (++encodeCheck === 3) keyEncoded = true; |
| 447 | continue; |
| 448 | } else { |
| 449 | encodeCheck = 0; |
| 450 | } |
| 451 | } |
| 452 | } |
| 453 | } |
| 454 | if (code === 43 /* + */) { |
| 455 | if (lastPos < i) key += qs.slice(lastPos, i); |
| 456 | key += plusChar; |
| 457 | lastPos = i + 1; |
| 458 | continue; |
| 459 | } |
| 460 | } |
| 461 | if (code === 43 /* + */) { |
| 462 | if (lastPos < i) value += qs.slice(lastPos, i); |
| 463 | value += plusChar; |
| 464 | lastPos = i + 1; |
| 465 | } else if (!valEncoded) { |
| 466 | // Try to match an (valid) encoded byte (once) to minimize unnecessary |
| 467 | // calls to string decoding functions |
| 468 | if (code === 37 /* % */) { |
| 469 | encodeCheck = 1; |
| 470 | } else if (encodeCheck > 0) { |
| 471 | if (isHexTable[code] === 1) { |
| 472 | if (++encodeCheck === 3) valEncoded = true; |
| 473 | } else { |
| 474 | encodeCheck = 0; |
| 475 | } |
| 476 | } |
| 477 | } |
| 478 | } |
| 479 | } |
| 480 | |
| 481 | // Deal with any leftover key or value data |
| 482 | if (lastPos < qs.length) { |
| 483 | if (eqIdx < eqLen) key += qs.slice(lastPos); |
| 484 | else if (sepIdx < sepLen) value += qs.slice(lastPos); |
| 485 | } else if (eqIdx === 0 && key.length === 0) { |
| 486 | // We ended on an empty substring |
| 487 | return obj; |
| 488 | } |
| 489 | |
| 490 | addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); |
| 491 | |
| 492 | return obj; |
| 493 | } |
| 494 | |
| 495 | /** |
| 496 | * V8 does not optimize functions with try-catch blocks, so we isolate them here |
| 497 | * to minimize the damage (Note: no longer true as of V8 5.4 -- but still will |
| 498 | * not be inlined). |
| 499 | */ |
| 500 | function decodeStr(s: string, decoder: DecodeFunction): string { |
| 501 | try { |
| 502 | return decoder(s); |
| 503 | } catch { |
| 504 | return unescape(s, true); |
| 505 | } |
| 506 | } |