diff --git a/benchmark/parseRepeated.js b/benchmark/parseRepeated.js new file mode 100644 index 0000000..f26f962 --- /dev/null +++ b/benchmark/parseRepeated.js @@ -0,0 +1,74 @@ +/** + * Module dependencies. + */ + +var benchmark = require('benchmark') +var benchmarks = require('beautify-benchmark') + +/** + * Globals for benchmark.js + */ + +global.cookie = require('..') + +var suite = new benchmark.Suite() + +suite.add({ + name: 'simple', + minSamples: 100, + fn: 'var val = cookie.parseRepeated("foo=bar")' +}) + +suite.add({ + name: 'decode', + minSamples: 100, + fn: 'var val = cookie.parseRepeated("foo=hello%20there!")' +}) + +suite.add({ + name: 'unquote', + minSamples: 100, + fn: 'var val = cookie.parseRepeated("foo=\\"foo bar\\"")' +}) + +suite.add({ + name: 'duplicates', + minSamples: 100, + fn: 'var val = cookie.parseRepeated(' + JSON.stringify(gencookies(2) + '; ' + gencookies(2)) + ')' +}) + +suite.add({ + name: '10 cookies', + minSamples: 100, + fn: 'var val = cookie.parseRepeated(' + JSON.stringify(gencookies(10)) + ')' +}) + +suite.add({ + name: '100 cookies', + minSamples: 100, + fn: 'var val = cookie.parseRepeated(' + JSON.stringify(gencookies(100)) + ')' +}) + +suite.on('start', function onCycle (event) { + process.stdout.write(' cookie.parseRepeated\n\n') +}) + +suite.on('cycle', function onCycle (event) { + benchmarks.add(event.target) +}) + +suite.on('complete', function onComplete () { + benchmarks.log() +}) + +suite.run({async: false}) + +function gencookies (num) { + var str = '' + + for (var i = 0; i < num; i++) { + str += '; foo' + i + '=bar' + } + + return str.substr(2) +} diff --git a/index.js b/index.js index ab2e467..a1190f8 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ exports.parse = parse; exports.serialize = serialize; +exports.parseRepeated = parseRepeated; /** * Module variables. @@ -82,6 +83,57 @@ function parse(str, options) { return obj; } +/** + * Parse a cookie header. + * + * Parse the given cookie header string into an object + * The object has the various cookies as keys(names) => [value1, value2] + * + * @param {string} str + * @param {object} [options] + * @return {object} + * @public + */ + +function parseRepeated(str, options) { + if (typeof str !== 'string') { + throw new TypeError('argument str must be a string'); + } + + var obj = {} + var opt = options || {}; + var pairs = str.split(pairSplitRegExp); + var dec = opt.decode || decode; + + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i]; + var eq_idx = pair.indexOf('='); + + // skip things that don't look like key=value + if (eq_idx < 0) { + continue; + } + + var key = pair.substr(0, eq_idx).trim() + var val = pair.substr(++eq_idx, pair.length).trim(); + + // quoted values + if ('"' == val[0]) { + val = val.slice(1, -1); + } + + // assign initial value + if (undefined == obj[key]) { + obj[key] = [tryDecode(val, dec)]; + } else { + // concat to previous value(s) + obj[key] = obj[key].concat(tryDecode(val, dec)); + } + } + + return obj; +} + /** * Serialize data into a cookie header. * diff --git a/test/parseRepeated.js b/test/parseRepeated.js new file mode 100644 index 0000000..15c1ff6 --- /dev/null +++ b/test/parseRepeated.js @@ -0,0 +1,72 @@ + +var assert = require('assert'); + +var cookie = require('..'); + +suite('parseRepeated'); + +test('argument validation', function() { + assert.throws(cookie.parseRepeated.bind(), /argument str must be a string/); + assert.throws(cookie.parseRepeated.bind(null, 42), /argument str must be a string/); +}); + +test('basic', function() { + assert.deepEqual({ foo: ['bar'] }, cookie.parseRepeated('foo=bar')); + assert.deepEqual({ foo: ['123'] }, cookie.parseRepeated('foo=123')); +}); + +test('ignore spaces', function() { + assert.deepEqual({ FOO: ['bar'], baz: ['raz'] }, + cookie.parseRepeated('FOO = bar; baz = raz')); +}); + +test('escaping', function() { + assert.deepEqual({ foo: ['bar=123456789&name=Magic+Mouse'] }, + cookie.parseRepeated('foo="bar=123456789&name=Magic+Mouse"')); + + assert.deepEqual({ email: [' ",;/'] }, + cookie.parseRepeated('email=%20%22%2c%3b%2f')); +}); + +test('ignore escaping error and return original value', function() { + assert.deepEqual({ foo: ['%1'], bar: ['bar'] }, cookie.parseRepeated('foo=%1;bar=bar')); +}); + +test('ignore non values', function() { + assert.deepEqual({ foo: ['%1'], bar: ['bar'] }, cookie.parseRepeated('foo=%1;bar=bar;HttpOnly;Secure')); +}); + +test('unencoded', function() { + assert.deepEqual({ foo: ['bar=123456789&name=Magic+Mouse'] }, + cookie.parseRepeated('foo="bar=123456789&name=Magic+Mouse"',{ + decode: function(value) { return value; } + })); + + assert.deepEqual({ email: ['%20%22%2c%3b%2f'] }, + cookie.parseRepeated('email=%20%22%2c%3b%2f',{ + decode: function(value) { return value; } + })); +}); + +test('dates', function() { + assert.deepEqual({ priority: ['true'], Path: ['/'], expires: ['Wed, 29 Jan 2014 17:43:25 GMT'] }, + cookie.parseRepeated('priority=true; expires=Wed, 29 Jan 2014 17:43:25 GMT; Path=/',{ + decode: function(value) { return value; } + })); +}); + +test('missing value', function() { + assert.deepEqual({ bar: ['1'], fizz: [''], buzz: ['2'] }, + cookie.parseRepeated('foo; bar=1; fizz= ; buzz=2',{ + decode: function(value) { return value; } + })); +}); + +test('assign repeated values into an array', function() { + assert.deepEqual({ foo: ['%1', 'boo'], bar: ['bar'] }, + cookie.parseRepeated('foo=%1;bar=bar;foo=boo')); + assert.deepEqual({ foo: ['false', 'true'], bar: ['bar'] }, + cookie.parseRepeated('foo=false;bar=bar;foo=true')); + assert.deepEqual({ foo: ['', 'boo'], bar: ['bar'] }, + cookie.parseRepeated('foo=;bar=bar;foo=boo')); +});