From ed35a00247e2c50ffb59cc3f137c987fc2ca8dae Mon Sep 17 00:00:00 2001 From: Douglas Christopher Wilson Date: Sun, 24 Aug 2014 21:40:41 -0400 Subject: [PATCH] Add weak ETag generation --- HISTORY.md | 1 + README.md | 20 ++++++++++--------- index.js | 54 +++++++++++++++++++++++++++++++++++++++++++++------- package.json | 3 +++ test/test.js | 32 ++++++++++++++++++++++++++----- 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 94568e5..da0c298 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,7 @@ unreleased ========== * Add fast-path for empty entity + * Add weak ETag generation * Shrink size of generated ETags 1.0.1 / 2014-08-24 diff --git a/README.md b/README.md index 502f22c..da36124 100644 --- a/README.md +++ b/README.md @@ -20,23 +20,25 @@ $ npm install etag var etag = require('etag') ``` -### etag(str) +### etag(entity, [options]) -Generate a strong ETag for the given string. This string should be the -complete body and is assumed to be UTF-8. +Generate a strong ETag for the given entity. This should be the complete +body of the entity. Both strings are `Buffer`s are accepted. By default, +a string will generate a weak ETag while a `Buffer` will generate a strong +ETag (this can be overwritten by `options.weak`). ```js res.setHeader('ETag', etag(body)) ``` -### etag(buf) +#### Options -Generate a strong ETag for the given `Buffer`. This buffer should be the -complete body. +`etag` accepts these properties in the options object. -```js -res.setHeader('ETag', etag(buf)) -``` +##### weak + +Specifies if a "strong" or a "weak" ETag will be generated. The ETag can only +really be a strong as the given input. ## Testing diff --git a/index.js b/index.js index fd9b651..ad5c437 100644 --- a/index.js +++ b/index.js @@ -14,17 +14,20 @@ module.exports = etag * Module dependencies. */ +var crc = require('crc').crc32 var crypto = require('crypto') /** * Create a simple ETag. * * @param {string|Buffer} entity + * @param {object} [options] + * @param {boolean} [options.weak] * @return {String} * @api public */ -function etag(entity) { +function etag(entity, options) { if (entity == null) { throw new TypeError('argument entity is required') } @@ -35,14 +38,51 @@ function etag(entity) { throw new TypeError('argument entity must be string or Buffer') } - if (entity.length === 0) { - // fast-path empty body - return '"1B2M2Y8AsgTpgAmY7PhCfg=="' + var buf = !isBuffer + ? new Buffer(entity, 'utf8') + : entity + var weak = options && typeof options.weak === 'boolean' + ? options.weak + : !isBuffer + + return weak + ? 'W/"' + weakhash(buf) + '"' + : '"' + stronghash(buf) + '"' +} + +/** + * Generate a strong hash. + * + * @param {Buffer} entity + * @return {String} + * @api private + */ + +function stronghash(buf) { + if (buf.length === 0) { + // fast-path empty + return '1B2M2Y8AsgTpgAmY7PhCfg==' } - var hash = crypto + return crypto .createHash('md5') - .update(entity, 'utf8') + .update(buf) .digest('base64') - return '"' + hash + '"' +} + +/** + * Generate a weak hash. + * + * @param {Buffer} entity + * @return {String} + * @api private + */ + +function weakhash(buf) { + if (buf.length === 0) { + // fast-path empty + return '0-0' + } + + return buf.length.toString(16) + '-' + crc(buf).toString(16) } diff --git a/package.json b/package.json index 7d74250..6d68618 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "res" ], "repository": "jshttp/etag", + "dependencies": { + "crc": "2.1.1" + }, "devDependencies": { "istanbul": "0.3.0", "mocha": "~1.21.4" diff --git a/test/test.js b/test/test.js index 8e2e35b..bb5bb67 100644 --- a/test/test.js +++ b/test/test.js @@ -12,21 +12,21 @@ describe('etag(entity)', function () { }) describe('when "entity" is a string', function () { - it('should generate an ETag', function () { - assert.equal(etag('beep boop'), '"Z34SGyQ2IB7YzB7HMkCjrQ=="') + it('should generate a weak ETag', function () { + assert.equal(etag('beep boop'), 'W/"9-7f3ee715"') }) it('should work containing Unicode', function () { - assert.equal(etag('论'), '"aW9HeLTk2Yt6lf7zJYElgw=="') + assert.equal(etag('论'), 'W/"3-438093ff"') }) it('should work for empty string', function () { - assert.equal(etag(''), '"1B2M2Y8AsgTpgAmY7PhCfg=="') + assert.equal(etag(''), 'W/"0-0"') }) }) describe('when "entity" is a Buffer', function () { - it('should generate an ETag', function () { + it('should generate a strong ETag', function () { assert.equal(etag(new Buffer([1, 2, 3])), '"Uonfc331cyb83SJZevsfrA=="') }) @@ -34,4 +34,26 @@ describe('etag(entity)', function () { assert.equal(etag(new Buffer(0)), '"1B2M2Y8AsgTpgAmY7PhCfg=="') }) }) + + describe('with "weak" option', function () { + describe('when "false"', function () { + it('should generate a strong ETag for a string', function () { + assert.equal(etag('beep boop', {weak: false}), '"Z34SGyQ2IB7YzB7HMkCjrQ=="') + }) + + it('should generate a strong ETag for a Buffer', function () { + assert.equal(etag(new Buffer([1, 2, 3]), {weak: false}), '"Uonfc331cyb83SJZevsfrA=="') + }) + }) + + describe('when "true"', function () { + it('should generate a strong ETag for a string', function () { + assert.equal(etag('beep boop', {weak: true}), 'W/"9-7f3ee715"') + }) + + it('should generate a strong ETag for a Buffer', function () { + assert.equal(etag(new Buffer([1, 2, 3]), {weak: true}), 'W/"3-55bc801d"') + }) + }) + }) })