Skip to content

Commit

Permalink
Add support for md4 in Node >=18.
Browse files Browse the repository at this point in the history
  • Loading branch information
iclanton committed Aug 24, 2023
1 parent 3956274 commit 6f6ae98
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 4 deletions.
45 changes: 41 additions & 4 deletions lib/util/createHash.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ class Hash {
}
}

exports.Hash = Hash;
/** @typedef {typeof Hash} HashConstructor */

class BulkUpdateDecorator extends Hash {
/**
* @param {Hash} hash hash
Expand Down Expand Up @@ -118,6 +115,16 @@ class DebugHash extends Hash {
}
}

/** @type {typeof import("crypto") | undefined} */
let crypto = undefined;
/** @type {typeof import("./hash/md4") | undefined} */
let createMd4 = undefined;
/** @type {typeof import("./hash/BatchedHash") | undefined} */
let BatchedHash = undefined;

/** @type {number} */
const NODE_MAJOR_VERSION = parseInt(process.versions.node, 10);

/**
* Creates a hash by name or function
* @param {string | HashConstructor} algorithm the algorithm name or a constructor creating a hash
Expand All @@ -127,11 +134,41 @@ module.exports = algorithm => {
if (typeof algorithm === "function") {
return new BulkUpdateDecorator(new algorithm());
}

switch (algorithm) {
// TODO add non-cryptographic algorithm here
case "debug":
return new DebugHash();
case "md4":
if (NODE_MAJOR_VERSION >= 18) {
if (createMd4 === undefined) {
createMd4 = require("./hash/md4");
if (BatchedHash === undefined) {
BatchedHash = require("./hash/BatchedHash");
}
}
return new /** @type {typeof import("./hash/BatchedHash")} */ (BatchedHash)(
createMd4()
);
}
// If we are on Node.js < 18, fall through to the default case
// eslint-disable-next-line no-fallthrough

case "native-md4":
if (NODE_MAJOR_VERSION >= 18) {
if (crypto === undefined) crypto = require("crypto");
return new BulkUpdateDecorator(
/** @type {typeof import("crypto")} */ (crypto).createHash("md4")
);
}
// If we are on Node.js < 18, fall through to the default case
// eslint-disable-next-line no-fallthrough

default:
return new BulkUpdateDecorator(require("crypto").createHash(algorithm));
if (crypto === undefined) crypto = require("crypto");
return new BulkUpdateDecorator(crypto.createHash(algorithm));
}
};

module.exports.Hash = Hash;
/** @typedef {typeof Hash} HashConstructor */
71 changes: 71 additions & 0 deletions lib/util/hash/BatchedHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

// From Webpack 5
// https://github.com/webpack/webpack/blob/853bfda35a0080605c09e1bdeb0103bcb9367a10/lib/util/hash/BatchedHash.js

const { Hash } = require("../createHash");
const MAX_SHORT_STRING = require("./wasm-hash").MAX_SHORT_STRING;

class BatchedHash extends Hash {
constructor(hash) {
super();
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;
25 changes: 25 additions & 0 deletions lib/util/hash/md4.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

174 changes: 174 additions & 0 deletions lib/util/hash/wasm-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

// From Webpack 5
// https://github.com/webpack/webpack/blob/853bfda35a0080605c09e1bdeb0103bcb9367a10/lib/util/hash/wasm-hash.js

// 65536 is the size of a wasm memory page
// 64 is the maximum chunk size for every possible wasm hash implementation
// 4 is the maximum number of bytes per char for string encoding (max is utf-8)
// ~3 makes sure that it's always a block of 4 chars, so avoid partially encoded bytes for base64
const MAX_SHORT_STRING = Math.floor((65536 - 64) / 4) & ~3;

class WasmHash {
/**
* @param {WebAssembly.Instance} instance wasm instance
* @param {WebAssembly.Instance[]} instancesPool pool of instances
* @param {number} chunkSize size of data chunks passed to wasm
* @param {number} digestSize size of digest returned by wasm
*/
constructor(instance, instancesPool, chunkSize, digestSize) {
const exports = /** @type {any} */ (instance.exports);
exports.init();
this.exports = exports;
this.mem = Buffer.from(exports.memory.buffer, 0, 65536);
this.buffered = 0;
this.instancesPool = instancesPool;
this.chunkSize = chunkSize;
this.digestSize = digestSize;
}

reset() {
this.buffered = 0;
this.exports.init();
}

/**
* @param {Buffer | string} data data
* @param {BufferEncoding=} encoding encoding
* @returns {this} itself
*/
update(data, encoding) {
if (typeof data === "string") {
while (data.length > MAX_SHORT_STRING) {
this._updateWithShortString(data.slice(0, MAX_SHORT_STRING), encoding);
data = data.slice(MAX_SHORT_STRING);
}
this._updateWithShortString(data, encoding);
return this;
}
this._updateWithBuffer(data);
return this;
}

/**
* @param {string} data data
* @param {BufferEncoding | 'utf-8'} encoding encoding
* @returns {void}
*/
_updateWithShortString(data, encoding) {
const { exports, buffered, mem, chunkSize } = this;
let endPos;
if (data.length < 70) {
if (!encoding || encoding === "utf-8" || encoding === "utf8") {
endPos = buffered;
for (let i = 0; i < data.length; i++) {
const cc = data.charCodeAt(i);
if (cc < 0x80) mem[endPos++] = cc;
else if (cc < 0x800) {
mem[endPos] = (cc >> 6) | 0xc0;
mem[endPos + 1] = (cc & 0x3f) | 0x80;
endPos += 2;
} else {
// bail-out for weird chars
const slicedData = data.slice(i);
endPos += mem.write(
slicedData,
endPos,
slicedData.length,
encoding
);
break;
}
}
} else if (encoding === "latin1") {
endPos = buffered;
for (let i = 0; i < data.length; i++) {
const cc = data.charCodeAt(i);
mem[endPos++] = cc;
}
} else {
endPos = buffered + mem.write(data, buffered, data.length, encoding);
}
} else {
endPos = buffered + mem.write(data, buffered, data.length, encoding);
}
if (endPos < chunkSize) {
this.buffered = endPos;
} else {
const l = endPos & ~(this.chunkSize - 1);
exports.update(l);
const newBuffered = endPos - l;
this.buffered = newBuffered;
if (newBuffered > 0) mem.copyWithin(0, l, endPos);
}
}

/**
* @param {Buffer} data data
* @returns {void}
*/
_updateWithBuffer(data) {
const { exports, buffered, mem } = this;
const length = data.length;
if (buffered + length < this.chunkSize) {
data.copy(mem, buffered, 0, length);
this.buffered += length;
} else {
const l = (buffered + length) & ~(this.chunkSize - 1);
if (l > 65536) {
let i = 65536 - buffered;
data.copy(mem, buffered, 0, i);
exports.update(65536);
const stop = l - buffered - 65536;
while (i < stop) {
data.copy(mem, 0, i, i + 65536);
exports.update(65536);
i += 65536;
}
data.copy(mem, 0, i, l - buffered);
exports.update(l - buffered - i);
} else {
data.copy(mem, buffered, 0, l - buffered);
exports.update(l);
}
const newBuffered = length + buffered - l;
this.buffered = newBuffered;
if (newBuffered > 0) data.copy(mem, 0, length - newBuffered, length);
}
}

digest(type) {
const { exports, buffered, mem, digestSize } = this;
exports.final(buffered);
this.instancesPool.push(this);
const hex = mem.toString("latin1", 0, digestSize);
if (type === "hex") return hex;
if (type === "binary" || !type) return Buffer.from(hex, "hex");
return Buffer.from(hex, "hex").toString(type);
}
}

const create = (wasmModule, instancesPool, chunkSize, digestSize) => {
if (instancesPool.length > 0) {
const old = instancesPool.pop();
old.reset();
return old;
} else {
return new WasmHash(
// This will only get called on Node 18+
// eslint-disable-next-line no-undef
new WebAssembly.Instance(wasmModule),
instancesPool,
chunkSize,
digestSize
);
}
};

module.exports.create = create;
module.exports.MAX_SHORT_STRING = MAX_SHORT_STRING;

0 comments on commit 6f6ae98

Please sign in to comment.