querystringparser
Version:
Extremely fast querystring parser
407 lines (367 loc) • 11.3 kB
JavaScript
/**
* Copyright (c) 2013 Petka Antonov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:</p>
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
"use strict";
var QueryStringSerializer = require("./querystringserializer.js");
module.exports = QueryStringParser;
var rplus = /\+/g;
var rint = /^[0-9]+$/;
var isArray = Array.isArray;
var haveProp = {}.hasOwnProperty;
function QueryStringParser() {
this.containsSparse = false;
this.cacheKey = "";
this.cacheVal = null;
}
QueryStringParser.maxLength = 32768;
QueryStringParser.maxDepth = 4;
QueryStringParser.maxKeys = 256;
QueryStringParser.parse = function QueryStringParser$Parse(str) {
if (typeof str === "string") {
var maxLength = QueryStringParser.maxLength;
if (str.length > maxLength) {
throw new RangeError(
"str is too large (" +
"QueryStringParser.maxLength=" + maxLength + ")"
);
}
var parser = new QueryStringParser();
return parser.parseString(str, false);
}
else if (str !== null && typeof str === "object") {
var parser = new QueryStringParser();
return parser.parseObject(str);
}
return {};
};
QueryStringParser.stringify =
function QueryStringParser$Stringify(value) {
var serializer = new QueryStringSerializer();
return serializer.serialize(value);
};
QueryStringParser.prototype.decode =
function QueryStringParser$decode(str, shouldDecode, containsPlus) {
if (shouldDecode === false) return str;
if (containsPlus === true) str = str.replace(rplus, " ");
try {
return decodeURIComponent(str);
}
catch (e) {
return str;
}
};
QueryStringParser.prototype.maybeArrayIndex =
function QueryStringParser$maybeArrayIndex(str, arrayLength) {
var len = str.length;
if (len === 0) {
return arrayLength;
}
var ch = str.charCodeAt(0);
if (ch === 48) {
return len > 1 ? -1 : 0;
}
else if (48 <= ch && ch <= 57) {
if (len === 1) {
return ch - 48;
}
else if (rint.test(str)) {
var v = parseInt(str, 10);
if (0 < v && v <= 1073741822) {
return v;
}
}
}
return -1;
};
QueryStringParser.prototype.getSlot =
function QueryStringParser$getSlot(dictionary, prevKey, curKey) {
var slot;
if (!(haveProp.call(dictionary, prevKey))) {
var index = this.maybeArrayIndex(curKey, 0);
if (index > -1) {
slot = [];
}
else {
slot = {};
}
dictionary[prevKey] = slot;
}
else {
slot = dictionary[prevKey];
}
return slot;
};
QueryStringParser.prototype.placeNestedValue =
function QueryStringParser$placeNestedValue
(dictionary, key, value, i, prevKey, curKey) {
var slot = this.getSlot(dictionary, prevKey, curKey);
var index = -1;
if (isArray(slot)) {
index = this.maybeArrayIndex(curKey, slot.length);
}
var len = key.length;
var depth = 2;
var maxDepth = QueryStringParser.maxDepth;
var start = -1;
for (; i < len; ++i) {
var ch = key.charCodeAt(i);
if (ch === 91) {
start = i + 1;
}
else if (ch === 93 &&
start > -1) {
prevKey = curKey;
curKey = start === i ? "" : key.substring(start, i);
start = -1;
depth++;
if (depth > maxDepth) {
throw new RangeError("Nesting depth of keys is too large " +
"(QueryStringParser.maxDepth="+maxDepth+")" );
}
slot = this.getSlot(slot, prevKey, curKey);
index = isArray(slot)
? this.maybeArrayIndex(curKey, slot.length)
: -1;
}
}
if(index > -1) {
if (value !== "") {
if (index === slot.length) {
slot.push(value);
}
else {
this.containsSparse = true;
slot[index] = value;
}
}
}
else {
this.insert(slot, curKey, value);
}
};
QueryStringParser.prototype.insert =
function QueryStringParser$insert(dictionary, key, value) {
var ret = null;
if (haveProp.call(dictionary, key)) {
var prev = dictionary[key];
if( isArray(prev) ) {
prev.push(value);
ret = prev;
}
else {
ret = [prev, value];
dictionary[key] = ret;
}
}
else {
dictionary[key] = value;
}
return ret;
};
QueryStringParser.prototype.push =
function QueryStringParser$push(dictionary, key, value) {
var ret = null;
if (haveProp.call(dictionary, key)) {
var prev = dictionary[key];
prev.push(value);
ret = prev;
}
else {
ret = [value];
dictionary[key] = ret;
}
return ret;
};
QueryStringParser.prototype.maybePlaceNestedValue =
function QueryStringParser$maybePlaceNestedValue(dictionary, key, value) {
var len = key.length;
if (key.charCodeAt(len - 1) !== 93) {
this.placeValue(dictionary, key, value, false);
return;
}
var start = -1;
var i = 0;
var curKey;
var prevKey;
for (; i < len; ++i) {
var ch = key.charCodeAt(i);
if (ch === 91) {
start = i + 1;
prevKey = key.slice(0, i);
}
else if (ch === 93) {
if (start < 0) {
this.placeValue(dictionary, key, value, false);
return;
}
curKey = start === i ? "" : key.slice(start, i);
i++;
break;
}
}
if (curKey === void 0) {
this.placeValue(dictionary, key, value, false);
return;
}
if (curKey === "" && value !== "" && i === len) {
if (key === this.cacheKey) {
this.cacheVal.push(value);
}
else {
this.cacheKey = key;
this.cacheVal = this.push(dictionary, prevKey, value);
}
}
else {
this.placeNestedValue(dictionary, key, value, i, prevKey, curKey);
}
};
QueryStringParser.prototype.placeValue =
function QueryStringParser$placeValue(dictionary, key, value, possiblyNested) {
if (possiblyNested === true) {
this.maybePlaceNestedValue(dictionary, key, value);
return;
}
if (key === this.cacheKey) {
this.cacheVal.push(value);
return;
}
var cache = this.insert(dictionary, key, value);
if (cache !== null) {
this.cacheKey = key;
this.cacheVal = cache;
}
};
QueryStringParser.prototype.compact =
function QueryStringParser$compact(obj) {
if (isArray(obj)) {
var ret = [];
var keys = Object.keys(obj);
for( var i = 0, len = keys.length; i < len; ++i ) {
ret.push(obj[keys[i]]);
}
return ret;
}
else if (typeof obj === "object") {
var keys = Object.keys(obj);
for( var i = 0, len = keys.length; i < len; ++i ) {
var key = keys[i];
obj[key] = this.compact(obj[key]);
}
}
else {
return obj;
}
};
QueryStringParser.prototype.parseObject =
function QueryStringParser$parseObject(obj) {
var keys = Object.keys(obj);
var len = keys.length;
if (len === 0) {
return {};
}
len--;
var ret = "";
var key;
for( var i = 0; i < len; ++i ) {
key = keys[i];
ret += key + "=" + obj[key] + "&";
}
key = keys[i];
ret += key + "=" + obj[key];
return this.parseString(ret, true);
};
QueryStringParser.prototype.parseString =
function QueryStringParser$parseString(str, noDecode) {
var maxKeys = QueryStringParser.maxKeys;
var keys = 0;
var decodeKey = false;
var decodeValue = false;
var possiblyNested = false;
var len = str.length;
var i = 0;
var dictionary = {};
var keyStart = 0;
var keyEnd = 0;
var valueStart = 0;
var valueEnd = 0;
var left = 0;
var lastIndex = len - 1;
var containsPlus = false;
for (; i < len; ++i) {
var ch = str.charCodeAt(i);
if (ch === 91) {
left++;
}
else if (left > 0 && ch === 93) {
possiblyNested = true;
left--;
}
else if (left === 0 && ch === 61) {
var j = i + 1;
keyEnd = i - 1;
valueEnd = valueStart = j;
var key = str.slice(keyStart, keyEnd + 1);
key = this.decode(key, decodeKey, containsPlus);
decodeKey = false;
for (; j < len; ++j) {
ch = str.charCodeAt(j);
if ((ch === 43 || ch === 37) && !noDecode) {
if (ch === 43) containsPlus = true;
decodeValue = true;
}
if (ch === 38 || j === lastIndex) {
valueEnd = j;
i = j;
if (ch === 38) {
valueEnd--;
}
var value = str.slice(valueStart, valueEnd + 1);
value = this.decode(value, decodeValue, containsPlus);
this.placeValue(dictionary, key, value, possiblyNested);
containsPlus = decodeValue = false;
possiblyNested = false;
keyStart = j + 1;
keys++;
if (keys > maxKeys) {
throw new RangeError("Amount of keys is too large " +
"(QueryStringParser.maxKeys=" + maxKeys + ")");
}
break;
}
}
}
else if ((ch === 43 || ch === 37) && !noDecode) {
if (ch === 43) containsPlus = true;
decodeKey = true;
}
}
if (keyStart !== len) {
var value = "";
var key = str.slice(keyStart, len);
key = this.decode(key, decodeKey, containsPlus);
this.placeValue(dictionary, key, value, possiblyNested);
}
if (this.containsSparse) {
this.compact(dictionary);
}
return dictionary;
};