Skip to content

Commit

Permalink
[minor] Improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
lpinca committed Jan 12, 2018
1 parent a0594a1 commit 9cf2dbb
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 114 deletions.
191 changes: 117 additions & 74 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,152 @@
'use strict';

var ParseError = require('./lib/error')
, ascii = require('./lib/ascii')
, isDelimiter = ascii.isDelimiter
, isTokenChar = ascii.isTokenChar
, isExtended = ascii.isExtended
, isPrint = ascii.isPrint;

//
// Expose the parser.
//
module.exports = parse;
var util = require('util');

var ParseError = require('./lib/error');
var ascii = require('./lib/ascii');

var isDelimiter = ascii.isDelimiter;
var isTokenChar = ascii.isTokenChar;
var isExtended = ascii.isExtended;
var isPrint = ascii.isPrint;

/**
* Unescape a string.
*
* @param {string} str The string to unescape.
* @returns {string} A new unescaped string.
* @private
*/
function decode(str) {
return str.replace(/\\(.)/g, '$1');
}

/**
* Parse the `Forwarded` header.
* Build an error message when an unexpected character is found.
*
* @param {String} input The field value of the header.
* @returns {Array<Object>}
* @api public
* @param {string} header The header field value.
* @param {number} position The position of the unexpected character.
* @returns {string} The error message.
* @private
*/
function parse(input) {
var forwarded = {}
, escape = false
, quote = false
, lock = false
, ows = false
, output = []
, token = ''
, parameter
, code
, ch;

for (var i = 0; i < input.length; i++) {
code = input.charCodeAt(i);
ch = input.charAt(i);

if (!parameter) {
if (ows && (code === 0x20/*' '*/|| code === 0x09/*'\t'*/)) {
function unexpectedCharacterMessage(header, position) {
return util.format(
"Unexpected character '%s' at index %d",
header.charAt(position),
position
);
}

/**
* Parse the `Forwarded` header field value into an array of objects.
*
* @param {string} header The header field value.
* @returns {Object[]}
* @public
*/
function parse(header) {
var mustUnescape = false;
var isEscaping = false;
var inQuotes = false;
var forwarded = {};
var output = [];
var start = -1;
var end = -1;
var parameter;
var code;

for (var i = 0; i < header.length; i++) {
code = header.charCodeAt(i);

if (parameter === undefined) {
if (start === -1 && (code === 0x20/*' '*/|| code === 0x09/*'\t'*/)) {
continue;
}
if (code === 0x3D/*'='*/&& token) {
parameter = token.toLowerCase();
token = '';
} else if (isTokenChar(code)) {
ows = false;
token += ch;

if (isTokenChar(code)) {
if (start === -1) start = i;
} else if (code === 0x3D/*'='*/ && start !== -1) {
parameter = header.slice(start, i).toLowerCase();
start = -1;
} else {
throw new ParseError(input, "Unexpected character '"+ ch +"'", i);
throw new ParseError(unexpectedCharacterMessage(header, i), header);
}
} else {
if (escape && (code === 0x09 || isPrint(code) || isExtended(code))) {
escape = false;
token += ch;
if (isEscaping && (code === 0x09 || isPrint(code) || isExtended(code))) {
isEscaping = false;
} else if (isTokenChar(code)) {
if (end !== -1) {
throw new ParseError(unexpectedCharacterMessage(header, i), header);
}

if (start === -1) start = i;
} else if (isDelimiter(code) || isExtended(code)) {
if (quote) {
if (inQuotes) {
if (code === 0x22/*'"'*/) {
quote = false;
lock = true;
inQuotes = false;
end = i;
} else if (code === 0x5C/*'\'*/) {
escape = true;
if (start === -1) start = i;
isEscaping = mustUnescape = true;
} else if (start === -1) {
start = i;
}
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3D) {
inQuotes = true;
} else if (
(code === 0x2C/*','*/|| code === 0x3B/*';'*/) &&
(start !== -1 || end !== -1)
) {
if (start !== -1) {
if (end === -1) end = i;
forwarded[parameter] = mustUnescape
? decode(header.slice(start, end))
: header.slice(start, end);
} else {
token += ch;
forwarded[parameter] = '';
}
} else if (code === 0x22 && input.charCodeAt(i - 1) === 0x3D) {
quote = true;
} else if ((code === 0x2C/*','*/|| code === 0x3B/*';'*/) && (token || lock)) {
forwarded[parameter] = token;

if (code === 0x2C) {
output.push(forwarded);
forwarded = {};
}
parameter = token = '';
lock = false;
ows = true;
} else {
throw new ParseError(input, "Unexpected character '"+ ch +"'", i);
}
} else if (isTokenChar(code)) {
if (quote || !lock) {
token += ch;

parameter = undefined;
start = end = -1;
} else {
throw new ParseError(input, "Unexpected character '"+ ch +"'", i);
throw new ParseError(unexpectedCharacterMessage(header, i), header);
}
} else if (code === 0x20 || code === 0x09) {
if (quote) {
token += ch;
} else if (lock) {
ows = true;
} else if (token) {
lock = ows = true;
if (end !== -1) continue;

if (inQuotes) {
if (start === -1) start = i;
} else if (start !== -1) {
end = i;
} else {
throw new ParseError(input, "Unexpected character '"+ ch +"'", i);
throw new ParseError(unexpectedCharacterMessage(header, i), header);
}
} else {
throw new ParseError(input, "Unexpected character '"+ ch +"'", i);
throw new ParseError(unexpectedCharacterMessage(header, i), header);
}
}
}

if (!token && !lock || !parameter || quote || ows) {
throw new ParseError(input, 'Unexpected end of input');
if (parameter === undefined || inQuotes || start === -1 && end === -1) {
throw new ParseError('Unexpected end of input', header);
}

forwarded[parameter] = token;
output.push(forwarded);
if (start !== -1) {
if (end === -1) end = i;
forwarded[parameter] = mustUnescape
? decode(header.slice(start, end))
: header.slice(start, end);
} else {
forwarded[parameter] = '';
}

output.push(forwarded);
return output;
}

module.exports = parse;
42 changes: 20 additions & 22 deletions lib/ascii.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
'use strict';

//
// Expose the helper functions.
//
exports.isDelimiter = isDelimiter;
exports.isTokenChar = isTokenChar;
exports.isExtended = isExtended;
exports.isPrint = isPrint;

/**
* Check if a character is not allowed in a token as defined in section 3.2.6
* of RFC 7230.
* Check if a character is a delimiter as defined in section 3.2.6 of RFC 7230.
*
*
* @param {Number} code The code of the character to check.
* @returns {Boolean}
* @api public
* @param {number} code The code of the character to check.
* @returns {boolean} `true` if the character is a delimiter, else `false`.
* @public
*/
function isDelimiter(code) {
return code === 0x22 // '"'
Expand All @@ -33,9 +24,9 @@ function isDelimiter(code) {
* Check if a character is allowed in a token as defined in section 3.2.6
* of RFC 7230.
*
* @param {Number} code The code of the character to check.
* @returns {Boolean}
* @api public
* @param {number} code The code of the character to check.
* @returns {boolean} `true` if the character is allowed, else `false`.
* @public
*/
function isTokenChar(code) {
return code === 0x21 // '!'
Expand All @@ -54,9 +45,9 @@ function isTokenChar(code) {
/**
* Check if a character is a printable ASCII character.
*
* @param {Number} code The code of the character to check.
* @returns {Boolean}
* @api public
* @param {number} code The code of the character to check.
* @returns {boolean} `true` if `code` is in the %x20-7E range, else `false`.
* @public
*/
function isPrint(code) {
return code >= 0x20 && code <= 0x7E;
Expand All @@ -65,10 +56,17 @@ function isPrint(code) {
/**
* Check if a character is an extended ASCII character.
*
* @param {Number} code The code of the character to check.
* @returns {Boolean}
* @api public
* @param {number} code The code of the character to check.
* @returns {boolean} `true` if `code` is in the %x80-FF range, else `false`.
* @public
*/
function isExtended(code) {
return code >= 0x80 && code <= 0xFF;
}

module.exports = {
isDelimiter: isDelimiter,
isTokenChar: isTokenChar,
isExtended: isExtended,
isPrint: isPrint
};
19 changes: 7 additions & 12 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,22 @@

var util = require('util');

//
// Expose the contructor.
//
module.exports = ParseError;

/**
* An error thrown by the parser on unexpected input.
*
* @constructor
* @param {String} input The unexpected input.
* @param {String} message The error message.
* @param {Number} [index] The character position where the error occurred.
* @api public
* @param {string} message The error message.
* @param {string} input The unexpected input.
* @public
*/
function ParseError(input, message, index) {
function ParseError(message, input) {
Error.captureStackTrace(this, ParseError);

this.name = this.constructor.name;
this.input = input;
this.message = message;
if (index) this.message += ' at index '+ index;
this.input = input;
}

util.inherits(ParseError, Error);

module.exports = ParseError;
12 changes: 6 additions & 6 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ test('handles double quotes and escaped characters', function (t) {
'foo=""',
'foo=" "',
'foo="\t"',
'foo=" \t"',
'foo="\\""',
'foo="\\\\"',
'foo="\\\\\\a"',
'foo="¥"',
'foo="\\§"'
].join(',')), [
Expand All @@ -35,8 +37,10 @@ test('handles double quotes and escaped characters', function (t) {
{ foo: '' },
{ foo: ' ' },
{ foo: '\t' },
{ foo: ' \t' },
{ foo: '"' },
{ foo: '\\' },
{ foo: '\\a' },
{ foo: '¥' },
{ foo: '§' }
]);
Expand Down Expand Up @@ -65,9 +69,9 @@ test('ignores the optional white spaces', function (t) {
});

test('ignores the case of the parameter names', function (t) {
t.deepEqual(parse('foo=a,Foo=b'), [
t.deepEqual(parse('foo=a,Foo=""'), [
{ foo: 'a' },
{ foo: 'b' }
{ foo: '' }
]);

t.end();
Expand Down Expand Up @@ -154,9 +158,5 @@ test('throws an error if it detects a premature end of input', function (t) {
parse('foo="bar');
}, /Unexpected end of input/);

t.throws(function () {
parse('foo=bar ');
}, /Unexpected end of input/);

t.end();
});

0 comments on commit 9cf2dbb

Please sign in to comment.