From 6610767f5d46ec746eee755aa7125c556f8db0d7 Mon Sep 17 00:00:00 2001
From: Brian White <mscdex@mscdex.net>
Date: Thu, 19 Feb 2015 09:53:04 -0500
Subject: [PATCH] querystring: optimize parse and stringify

parse optimizations:

* Move try-catch to separate function to keep entire function from
being deoptimized.
* Use key array lookup instead of using hasOwnProperty.
* Avoid decoding known empty strings.
* Avoid possibly unnecessary switch to slower decoder for values if
key decoding throws.

stringify optimizations:

* Use manual loop for default encoder instead of encodeURIComponent.
* Use string concatenation instead of joining an array of strings.
* Avoid caching result of typeof.
---
 lib/querystring.js | 125 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 90 insertions(+), 35 deletions(-)

diff --git a/lib/querystring.js b/lib/querystring.js
index b574978bc78230..af320cf8f86397 100644
--- a/lib/querystring.js
+++ b/lib/querystring.js
@@ -4,13 +4,6 @@
 
 const QueryString = exports;
 
-// If obj.hasOwnProperty has been overridden, then calling
-// obj.hasOwnProperty(prop) will break.
-// See: https://github.com/joyent/node/issues/1707
-function hasOwnProperty(obj, prop) {
-  return Object.prototype.hasOwnProperty.call(obj, prop);
-}
-
 
 function charCode(c) {
   return c.charCodeAt(0);
@@ -93,19 +86,68 @@ QueryString.unescape = function(s, decodeSpaces) {
 };
 
 
+var hexTable = new Array(256);
+for (var i = 0; i < 256; ++i)
+  hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
 QueryString.escape = function(str) {
-  return encodeURIComponent(str);
+  var len = str.length;
+  var out = '';
+  var i, c;
+
+  if (len === 0)
+    return str;
+
+  for (i = 0; i < len; ++i) {
+    c = str.charCodeAt(i);
+
+    // These characters do not need escaping (in order):
+    // ! - . _ ~
+    // ' ( ) *
+    // digits
+    // alpha (uppercase)
+    // alpha (lowercase)
+    if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E ||
+        (c >= 0x27 && c <= 0x2A) ||
+        (c >= 0x30 && c <= 0x39) ||
+        (c >= 0x41 && c <= 0x5A) ||
+        (c >= 0x61 && c <= 0x7A)) {
+      out += str[i];
+      continue;
+    }
+
+    // Other ASCII characters
+    if (c < 0x80) {
+      out += hexTable[c];
+      continue;
+    }
+
+    // Multi-byte characters ...
+    if (c < 0x800) {
+      out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
+      continue;
+    }
+    if (c < 0xD800 || c >= 0xE000) {
+      out += hexTable[0xE0 | (c >> 12)] +
+             hexTable[0x80 | ((c >> 6) & 0x3F)] +
+             hexTable[0x80 | (c & 0x3F)];
+      continue;
+    }
+    // Surrogate pair
+    ++i;
+    c = 0x10000 + (((c & 0x3FF) << 10) | (str.charCodeAt(i) & 0x3FF));
+    out += hexTable[0xF0 | (c >> 18)] +
+           hexTable[0x80 | ((c >> 12) & 0x3F)] +
+           hexTable[0x80 | ((c >> 6) & 0x3F)] +
+           hexTable[0x80 | (c & 0x3F)];
+  }
+  return out;
 };
 
 var stringifyPrimitive = function(v) {
-  let type = typeof v;
-
-  if (type === 'string')
+  if (typeof v === 'string' || (typeof v === 'number' && isFinite(v)))
     return v;
-  if (type === 'boolean')
+  if (typeof v === 'boolean')
     return v ? 'true' : 'false';
-  if (type === 'number')
-    return isFinite(v) ? v : '';
   return '';
 };
 
@@ -121,21 +163,31 @@ QueryString.stringify = QueryString.encode = function(obj, sep, eq, options) {
 
   if (obj !== null && typeof obj === 'object') {
     var keys = Object.keys(obj);
-    var fields = [];
-
-    for (var i = 0; i < keys.length; i++) {
+    var len = keys.length;
+    var flast = len - 1;
+    var fields = '';
+    for (var i = 0; i < len; ++i) {
       var k = keys[i];
       var v = obj[k];
       var ks = encode(stringifyPrimitive(k)) + eq;
 
       if (Array.isArray(v)) {
-        for (var j = 0; j < v.length; j++)
-          fields.push(ks + encode(stringifyPrimitive(v[j])));
+        var vlen = v.length;
+        var vlast = vlen - 1;
+        for (var j = 0; j < vlen; ++j) {
+          fields += ks + encode(stringifyPrimitive(v[j]));
+          if (j < vlast)
+            fields += sep;
+        }
+        if (vlen && i < flast)
+          fields += sep;
       } else {
-        fields.push(ks + encode(stringifyPrimitive(v)));
+        fields += ks + encode(stringifyPrimitive(v));
+        if (i < flast)
+          fields += sep;
       }
     }
-    return fields.join(sep);
+    return fields;
   }
   return '';
 };
@@ -169,29 +221,23 @@ QueryString.parse = QueryString.decode = function(qs, sep, eq, options) {
     decode = options.decodeURIComponent;
   }
 
+  var keys = [];
   for (var i = 0; i < len; ++i) {
     var x = qs[i].replace(regexp, '%20'),
         idx = x.indexOf(eq),
-        kstr, vstr, k, v;
+        k, v;
 
     if (idx >= 0) {
-      kstr = x.substr(0, idx);
-      vstr = x.substr(idx + 1);
+      k = decodeStr(x.substring(0, idx), decode);
+      v = decodeStr(x.substring(idx + 1), decode);
     } else {
-      kstr = x;
-      vstr = '';
+      k = decodeStr(x, decode);
+      v = '';
     }
 
-    try {
-      k = decode(kstr);
-      v = decode(vstr);
-    } catch (e) {
-      k = QueryString.unescape(kstr, true);
-      v = QueryString.unescape(vstr, true);
-    }
-
-    if (!hasOwnProperty(obj, k)) {
+    if (keys.indexOf(k) === -1) {
       obj[k] = v;
+      keys.push(k);
     } else if (Array.isArray(obj[k])) {
       obj[k].push(v);
     } else {
@@ -201,3 +247,12 @@ QueryString.parse = QueryString.decode = function(qs, sep, eq, options) {
 
   return obj;
 };
+
+
+function decodeStr(s, decoder) {
+  try {
+    return decoder(s);
+  } catch (e) {
+    return QueryString.unescape(s, true);
+  }
+}