Skip to content

Commit 0965b28

Browse files
anonrigaduh95
authored andcommitted
querystring: speed up default parse and unescape
Skip the %XX walk in unescapeBuffer when the input has no '%'. Add a dedicated '&'/'=' scanner for the default parse path so it does not build separator code arrays or run the multi-char state machine. Official benchmark/querystring/querystring-parse.js: encodemany is about 38% faster, manyblankpairs about 17%, encodelast about 10%, noencode about 8%. Official querystring-unescapebuffer.js with no escapes is about 36% faster. Assisted-by: a closed-source coding agent Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Co-authored-by: Yagiz Nizipli <anonrig@users.noreply.github.com> PR-URL: #66175 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 19d8d19 commit 0965b28

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

‎lib/querystring.js‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
ObjectKeys,
3333
String,
3434
StringPrototypeCharCodeAt,
35+
StringPrototypeIndexOf,
3536
StringPrototypeSlice,
3637
decodeURIComponent,
3738
} = primordials;
@@ -82,6 +83,11 @@ const unhexTable = new Int8Array([
8283
* @returns {string}
8384
*/
8485
function unescapeBuffer(s, decodeSpaces) {
86+
// No encoded bytes: latin1-copy the string without walking %XX.
87+
if (StringPrototypeIndexOf(s, '%') === -1) {
88+
if (!decodeSpaces || StringPrototypeIndexOf(s, '+') === -1)
89+
return Buffer.from(s, 'latin1');
90+
}
8591
const out = Buffer.allocUnsafe(s.length);
8692
let index = 0;
8793
let outIndex = 0;
@@ -303,6 +309,120 @@ function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
303309
}
304310
}
305311

312+
/**
313+
* Default querystring parser: separator '&', equals '=', native unescape.
314+
* Single-character separators avoid the sep/eq index state machine.
315+
* @param {string} qs
316+
* @param {Record<string, string | string[]>} obj
317+
* @returns {Record<string, string | string[]>}
318+
*/
319+
function parseAmpEq(qs, obj) {
320+
let pairs = 1000;
321+
let lastPos = 0;
322+
let eqPos = -1;
323+
let key = '';
324+
let value = '';
325+
let keyEncoded = false;
326+
let valEncoded = false;
327+
let encodeCheck = 0;
328+
const plusChar = ' ';
329+
const len = qs.length;
330+
331+
for (let i = 0; i < len; ++i) {
332+
const code = StringPrototypeCharCodeAt(qs, i);
333+
334+
if (code === 38) { // '&'
335+
const end = i;
336+
if (eqPos === -1) {
337+
if (lastPos < end)
338+
key += StringPrototypeSlice(qs, lastPos, end);
339+
else if (key.length === 0) {
340+
if (--pairs === 0)
341+
return obj;
342+
lastPos = i + 1;
343+
encodeCheck = 0;
344+
continue;
345+
}
346+
} else if (lastPos < end) {
347+
value += StringPrototypeSlice(qs, lastPos, end);
348+
}
349+
350+
addKeyVal(obj, key, value, keyEncoded, valEncoded, qsUnescape);
351+
352+
if (--pairs === 0)
353+
return obj;
354+
keyEncoded = false;
355+
valEncoded = false;
356+
key = '';
357+
value = '';
358+
encodeCheck = 0;
359+
lastPos = i + 1;
360+
eqPos = -1;
361+
continue;
362+
}
363+
364+
if (eqPos === -1) {
365+
if (code === 61) { // '='
366+
if (lastPos < i)
367+
key += StringPrototypeSlice(qs, lastPos, i);
368+
encodeCheck = 0;
369+
lastPos = i + 1;
370+
eqPos = i;
371+
continue;
372+
}
373+
if (!keyEncoded) {
374+
if (code === 37) {
375+
encodeCheck = 1;
376+
} else if (encodeCheck > 0) {
377+
if (isHexTable[code] === 1) {
378+
if (++encodeCheck === 3)
379+
keyEncoded = true;
380+
} else {
381+
encodeCheck = 0;
382+
}
383+
}
384+
}
385+
if (code === 43) {
386+
if (lastPos < i)
387+
key += StringPrototypeSlice(qs, lastPos, i);
388+
key += plusChar;
389+
lastPos = i + 1;
390+
}
391+
continue;
392+
}
393+
394+
if (code === 43) {
395+
if (lastPos < i)
396+
value += StringPrototypeSlice(qs, lastPos, i);
397+
value += plusChar;
398+
lastPos = i + 1;
399+
} else if (!valEncoded) {
400+
if (code === 37) {
401+
encodeCheck = 1;
402+
} else if (encodeCheck > 0) {
403+
if (isHexTable[code] === 1) {
404+
if (++encodeCheck === 3)
405+
valEncoded = true;
406+
} else {
407+
encodeCheck = 0;
408+
}
409+
}
410+
}
411+
}
412+
413+
if (lastPos < len) {
414+
if (eqPos === -1)
415+
key += StringPrototypeSlice(qs, lastPos);
416+
else
417+
value += StringPrototypeSlice(qs, lastPos);
418+
} else if (eqPos === -1 && key.length === 0) {
419+
return obj;
420+
}
421+
422+
addKeyVal(obj, key, value, keyEncoded, valEncoded, qsUnescape);
423+
return obj;
424+
}
425+
306426
/**
307427
* Parse a key/val string.
308428
* @param {string} qs
@@ -321,6 +441,16 @@ function parse(qs, sep, eq, options) {
321441
return obj;
322442
}
323443

444+
// Default '&' / '=' / unescape: single-char scanner, no sep/eq arrays.
445+
if ((sep === undefined || sep === '&') &&
446+
(eq === undefined || eq === '=') &&
447+
QueryString.unescape === qsUnescape &&
448+
(!options ||
449+
(options.maxKeys === undefined &&
450+
options.decodeURIComponent === undefined))) {
451+
return parseAmpEq(qs, obj);
452+
}
453+
324454
const sepCodes = (!sep ? defSepCodes : charCodes(String(sep)));
325455
const eqCodes = (!eq ? defEqCodes : charCodes(String(eq)));
326456
const sepLen = sepCodes.length;

0 commit comments

Comments
 (0)