diff --git a/benchmark/querystring/querystring-unescape.js b/benchmark/querystring/querystring-unescape.js new file mode 100644 index 00000000000000..af977411567c99 --- /dev/null +++ b/benchmark/querystring/querystring-unescape.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common.js'); +const querystring = require('querystring'); + +const bench = common.createBenchmark(main, { + input: [ + 'there is nothing to unescape here', + 'there+are+spaces+to+unescape+here', + 'there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped', + '%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37', + 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring', + 'there%2Qare%0-fake%escaped%20values in%%%%this%9Hstring', + '%%2a', + '%2sf%2a', + '%2%2af%2a', + ], + n: [10e6], + decodeSpaces: [1, 0], +}); + +function main({ input, n, decodeSpaces }) { + decodeSpaces = !!decodeSpaces; + bench.start(); + for (let i = 0; i < n; i += 1) + querystring.unescape(input, decodeSpaces); + bench.end(n); +} diff --git a/lib/querystring.js b/lib/querystring.js index c4cbca0c5a2733..da398ecc1f91db 100644 --- a/lib/querystring.js +++ b/lib/querystring.js @@ -28,10 +28,15 @@ const { ArrayIsArray, Int8Array, MathAbs, + NumberParseInt, NumberIsFinite, ObjectKeys, + RegExpPrototypeExec, String, + StringFromCharCode, StringPrototypeCharCodeAt, + StringPrototypeIndexOf, + StringPrototypeReplace, StringPrototypeSlice, decodeURIComponent, } = primordials; @@ -125,17 +130,40 @@ function unescapeBuffer(s, decodeSpaces) { /** * @param {string} s - * @param {boolean} decodeSpaces + * @param {boolean} [decodeSpaces=false] * @returns {string} */ -function qsUnescape(s, decodeSpaces) { - try { - return decodeURIComponent(s); - } catch { - return QueryString.unescapeBuffer(s, decodeSpaces).toString(); +function qsUnescape(s, decodeSpaces = false) { + if (StringPrototypeIndexOf(s, '%') === -1) { + return s; + } + + if (RegExpPrototypeExec(MALFORMED_URI_REGEX, s) !== null) { + if (decodeSpaces) { + return StringPrototypeReplace(s, decodeSpacesRegexp, decodeSpacesReplacer); + } + return StringPrototypeReplace(s, baseRegexp, baseReplacer); + } + return decodeURIComponent(s); + } +const MALFORMED_URI_REGEX = /(?:%(?:[^0-9a-fA-F]|[0-9a-fA-F][^0-9a-fA-F]))/u; + +const baseRegexp = /(%[\da-fA-F]{2})/gu; + +function baseReplacer(match) { + return StringFromCharCode(NumberParseInt(match[1] + match[2], 16)); +} + +const decodeSpacesRegexp = /(%[\da-fA-F]{2}|[+])/gu; + +function decodeSpacesReplacer(match) { + return match === '+' ? + ' ' : + StringFromCharCode(NumberParseInt(match[1] + match[2], 16)); +} // These characters do not need escaping when generating query strings: // ! - . _ ~ diff --git a/test/parallel/test-querystring.js b/test/parallel/test-querystring.js index b24ec5b569bd03..68ad7507eced34 100644 --- a/test/parallel/test-querystring.js +++ b/test/parallel/test-querystring.js @@ -170,10 +170,14 @@ const qsNoMungeTestCases = [ const qsUnescapeTestCases = [ ['there is nothing to unescape here', 'there is nothing to unescape here'], + ['there+is+nothing+to+unescape+here', + 'there+is+nothing+to+unescape+here'], ['there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped', 'there are several spaces that need to be unescaped'], ['there%2Qare%0-fake%escaped values in%%%%this%9Hstring', 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'], + ['there%2Qare%0-fake%escaped%20values in%%%%this%9Hstring', + 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'], ['%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37', ' !"#$%&\'()*+,-./01234567'], ['%%2a', '%*'],