From 8c2d24ee400bc4567335e97ee6004c3baa6ef66f Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 4 Nov 2021 18:05:31 +0300 Subject: [PATCH] fix: base64 generation and unicode characters (#197) --- lib/getHashDigest.js | 10 ++++-- lib/hash/BatchedHash.js | 64 ++++++++++++++++++++++++++++++++++++ lib/hash/wasm-hash.js | 2 +- test/getHashDigest.test.js | 4 +-- test/interpolateName.test.js | 10 +++--- 5 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 lib/hash/BatchedHash.js diff --git a/lib/getHashDigest.js b/lib/getHashDigest.js index 820ae1d..7098f06 100644 --- a/lib/getHashDigest.js +++ b/lib/getHashDigest.js @@ -40,6 +40,7 @@ function encodeBufferToBase(buffer, base) { } let createMd4 = undefined; +let BatchedHash = undefined; function getHashDigest(buffer, hashType, digestType, maxLength) { hashType = hashType || 'md4'; @@ -53,9 +54,13 @@ function getHashDigest(buffer, hashType, digestType, maxLength) { if (error.code === 'ERR_OSSL_EVP_UNSUPPORTED' && hashType === 'md4') { if (createMd4 === undefined) { createMd4 = require('./hash/md4'); + + if (BatchedHash === undefined) { + BatchedHash = require('./hash/BatchedHash'); + } } - hash = createMd4(); + hash = new BatchedHash(createMd4()); } if (!hash) { @@ -72,8 +77,7 @@ function getHashDigest(buffer, hashType, digestType, maxLength) { digestType === 'base49' || digestType === 'base52' || digestType === 'base58' || - digestType === 'base62' || - digestType === 'base64' + digestType === 'base62' ) { return encodeBufferToBase(hash.digest(), digestType.substr(4)).substr( 0, diff --git a/lib/hash/BatchedHash.js b/lib/hash/BatchedHash.js new file mode 100644 index 0000000..6ec6a44 --- /dev/null +++ b/lib/hash/BatchedHash.js @@ -0,0 +1,64 @@ +const MAX_SHORT_STRING = require('./wasm-hash').MAX_SHORT_STRING; + +class BatchedHash { + constructor(hash) { + this.string = undefined; + this.encoding = undefined; + this.hash = hash; + } + + /** + * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} + * @param {string|Buffer} data data + * @param {string=} inputEncoding data encoding + * @returns {this} updated hash + */ + update(data, inputEncoding) { + if (this.string !== undefined) { + if ( + typeof data === 'string' && + inputEncoding === this.encoding && + this.string.length + data.length < MAX_SHORT_STRING + ) { + this.string += data; + + return this; + } + + this.hash.update(this.string, this.encoding); + this.string = undefined; + } + + if (typeof data === 'string') { + if ( + data.length < MAX_SHORT_STRING && + // base64 encoding is not valid since it may contain padding chars + (!inputEncoding || !inputEncoding.startsWith('ba')) + ) { + this.string = data; + this.encoding = inputEncoding; + } else { + this.hash.update(data, inputEncoding); + } + } else { + this.hash.update(data); + } + + return this; + } + + /** + * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} + * @param {string=} encoding encoding of the return value + * @returns {string|Buffer} digest + */ + digest(encoding) { + if (this.string !== undefined) { + this.hash.update(this.string, this.encoding); + } + + return this.hash.digest(encoding); + } +} + +module.exports = BatchedHash; diff --git a/lib/hash/wasm-hash.js b/lib/hash/wasm-hash.js index c2c2bd6..d8f2817 100644 --- a/lib/hash/wasm-hash.js +++ b/lib/hash/wasm-hash.js @@ -82,7 +82,7 @@ class WasmHash { endPos += 2; } else { // bail-out for weird chars - endPos += mem.write(data.slice(endPos), endPos, encoding); + endPos += mem.write(data.slice(i), endPos, encoding); break; } } diff --git a/test/getHashDigest.test.js b/test/getHashDigest.test.js index 28f2fc3..9fa1355 100644 --- a/test/getHashDigest.test.js +++ b/test/getHashDigest.test.js @@ -12,7 +12,7 @@ describe('getHashDigest()', () => { '6f8db599de986fab7a21625b7916589c', ], ['test string', 'md5', 'hex', 4, '6f8d'], - ['test string', 'md5', 'base64', undefined, '2sm1pVmS8xuGJLCdWpJoRL'], + ['test string', 'md5', 'base64', undefined, 'b421md6Yb6t6IWJbeRZYnA=='], ['test string', 'md5', 'base52', undefined, 'dJnldHSAutqUacjgfBQGLQx'], ['test string', 'md5', 'base26', 6, 'bhtsgu'], [ @@ -20,7 +20,7 @@ describe('getHashDigest()', () => { 'sha512', 'base64', undefined, - '2IS-kbfIPnVflXb9CzgoNESGCkvkb0urMmucPD9z8q6HuYz8RShY1-tzSUpm5-Ivx_u4H1MEzPgAhyhaZ7RKog', + 'EObWR69EYkRC84jCwUp4f/ixfmFluD12fsBHdo2MvLcaGjIm58x4Frx5wEJ9lKnaaIxBo5kse/Xk18w+C+XbrA==', ], [ 'test string', diff --git a/test/interpolateName.test.js b/test/interpolateName.test.js index c56e298..4ed7c5b 100644 --- a/test/interpolateName.test.js +++ b/test/interpolateName.test.js @@ -62,13 +62,13 @@ describe('interpolateName()', () => { '/app/img/image.png', '[sha512:hash:base64:7].[ext]', 'test content', - '2BKDTjl.png', + 'DL9MrvO.png', ], [ '/app/img/image.png', '[sha512:contenthash:base64:7].[ext]', 'test content', - '2BKDTjl.png', + 'DL9MrvO.png', ], [ '/app/dir/file.png', @@ -116,13 +116,13 @@ describe('interpolateName()', () => { '/lib/components/modal/modal.css', '[name].[md5:hash:base64:20].[ext]', 'test content', - 'modal.1n8osQznuT8jOAwdzg_n.css', + 'modal.lHP90NiApDwht3eNNIch.css', ], [ '/lib/components/modal/modal.css', '[name].[md5:contenthash:base64:20].[ext]', 'test content', - 'modal.1n8osQznuT8jOAwdzg_n.css', + 'modal.lHP90NiApDwht3eNNIch.css', ], // Should not interpret without `hash` or `contenthash` [ @@ -265,7 +265,7 @@ describe('interpolateName()', () => { ], [ [{}, '[hash:base64]', { content: 'test string' }], - '2LIG3oc1uBNmwOoL7kXgoK', + 'Lgbt1PFiMmjFpRcw2KCyrw==', 'should interpolate [hash] token with options', ], [