diff --git a/lib/querystring.js b/lib/querystring.js index b574978bc7..af320cf8f8 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); + } +}