| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const Hash = require("../Hash");
|
|---|
| 9 |
|
|---|
| 10 | // 65536 is the size of a wasm memory page
|
|---|
| 11 | // 64 is the maximum chunk size for every possible wasm hash implementation
|
|---|
| 12 | // 4 is the maximum number of bytes per char for string encoding (max is utf-8)
|
|---|
| 13 | // ~3 makes sure that it's always a block of 4 chars, so avoid partially encoded bytes for base64
|
|---|
| 14 | const MAX_SHORT_STRING = Math.floor((65536 - 64) / 4) & ~3;
|
|---|
| 15 |
|
|---|
| 16 | /**
|
|---|
| 17 | * Represents the wasm hash runtime component.
|
|---|
| 18 | * @typedef {object} WasmExports
|
|---|
| 19 | * @property {WebAssembly.Memory} memory
|
|---|
| 20 | * @property {() => void} init
|
|---|
| 21 | * @property {(length: number) => void} update
|
|---|
| 22 | * @property {(length: number) => void} final
|
|---|
| 23 | */
|
|---|
| 24 |
|
|---|
| 25 | class WasmHash extends Hash {
|
|---|
| 26 | /**
|
|---|
| 27 | * Creates an instance of WasmHash.
|
|---|
| 28 | * @param {WebAssembly.Instance} instance wasm instance
|
|---|
| 29 | * @param {WebAssembly.Instance[]} instancesPool pool of instances
|
|---|
| 30 | * @param {number} chunkSize size of data chunks passed to wasm
|
|---|
| 31 | * @param {number} digestSize size of digest returned by wasm
|
|---|
| 32 | */
|
|---|
| 33 | constructor(instance, instancesPool, chunkSize, digestSize) {
|
|---|
| 34 | super();
|
|---|
| 35 |
|
|---|
| 36 | const exports = /** @type {WasmExports} */ (instance.exports);
|
|---|
| 37 | exports.init();
|
|---|
| 38 | /** @type {WasmExports} */
|
|---|
| 39 | this.exports = exports;
|
|---|
| 40 | /** @type {Buffer} */
|
|---|
| 41 | this.mem = Buffer.from(exports.memory.buffer, 0, 65536);
|
|---|
| 42 | /** @type {number} */
|
|---|
| 43 | this.buffered = 0;
|
|---|
| 44 | /** @type {WebAssembly.Instance[]} */
|
|---|
| 45 | this.instancesPool = instancesPool;
|
|---|
| 46 | /** @type {number} */
|
|---|
| 47 | this.chunkSize = chunkSize;
|
|---|
| 48 | /** @type {number} */
|
|---|
| 49 | this.digestSize = digestSize;
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | reset() {
|
|---|
| 53 | this.buffered = 0;
|
|---|
| 54 | this.exports.init();
|
|---|
| 55 | }
|
|---|
| 56 |
|
|---|
| 57 | /**
|
|---|
| 58 | * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding}
|
|---|
| 59 | * @overload
|
|---|
| 60 | * @param {string | Buffer} data data
|
|---|
| 61 | * @returns {Hash} updated hash
|
|---|
| 62 | */
|
|---|
| 63 | /**
|
|---|
| 64 | * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding}
|
|---|
| 65 | * @overload
|
|---|
| 66 | * @param {string} data data
|
|---|
| 67 | * @param {string=} inputEncoding data encoding
|
|---|
| 68 | * @returns {this} updated hash
|
|---|
| 69 | */
|
|---|
| 70 | /**
|
|---|
| 71 | * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding}
|
|---|
| 72 | * @param {string | Buffer} data data
|
|---|
| 73 | * @param {string=} inputEncoding data encoding
|
|---|
| 74 | * @returns {this} updated hash
|
|---|
| 75 | */
|
|---|
| 76 | update(data, inputEncoding) {
|
|---|
| 77 | if (typeof data === "string") {
|
|---|
| 78 | while (data.length > MAX_SHORT_STRING) {
|
|---|
| 79 | this._updateWithShortString(
|
|---|
| 80 | data.slice(0, MAX_SHORT_STRING),
|
|---|
| 81 | /** @type {NodeJS.BufferEncoding} */
|
|---|
| 82 | (inputEncoding)
|
|---|
| 83 | );
|
|---|
| 84 | data = data.slice(MAX_SHORT_STRING);
|
|---|
| 85 | }
|
|---|
| 86 | this._updateWithShortString(
|
|---|
| 87 | data,
|
|---|
| 88 | /** @type {NodeJS.BufferEncoding} */
|
|---|
| 89 | (inputEncoding)
|
|---|
| 90 | );
|
|---|
| 91 | return this;
|
|---|
| 92 | }
|
|---|
| 93 | this._updateWithBuffer(data);
|
|---|
| 94 | return this;
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | /**
|
|---|
| 98 | * Update with short string.
|
|---|
| 99 | * @param {string} data data
|
|---|
| 100 | * @param {BufferEncoding=} encoding encoding
|
|---|
| 101 | * @returns {void}
|
|---|
| 102 | */
|
|---|
| 103 | _updateWithShortString(data, encoding) {
|
|---|
| 104 | const { exports, buffered, mem, chunkSize } = this;
|
|---|
| 105 | /** @type {number} */
|
|---|
| 106 | let endPos;
|
|---|
| 107 | if (data.length < 70) {
|
|---|
| 108 | // eslint-disable-next-line unicorn/text-encoding-identifier-case
|
|---|
| 109 | if (!encoding || encoding === "utf-8" || encoding === "utf8") {
|
|---|
| 110 | endPos = buffered;
|
|---|
| 111 | for (let i = 0; i < data.length; i++) {
|
|---|
| 112 | const cc = data.charCodeAt(i);
|
|---|
| 113 | if (cc < 0x80) {
|
|---|
| 114 | mem[endPos++] = cc;
|
|---|
| 115 | } else if (cc < 0x800) {
|
|---|
| 116 | mem[endPos] = (cc >> 6) | 0xc0;
|
|---|
| 117 | mem[endPos + 1] = (cc & 0x3f) | 0x80;
|
|---|
| 118 | endPos += 2;
|
|---|
| 119 | } else {
|
|---|
| 120 | // bail-out for weird chars
|
|---|
| 121 | endPos += mem.write(data.slice(i), endPos, encoding);
|
|---|
| 122 | break;
|
|---|
| 123 | }
|
|---|
| 124 | }
|
|---|
| 125 | } else if (encoding === "latin1") {
|
|---|
| 126 | endPos = buffered;
|
|---|
| 127 | for (let i = 0; i < data.length; i++) {
|
|---|
| 128 | const cc = data.charCodeAt(i);
|
|---|
| 129 | mem[endPos++] = cc;
|
|---|
| 130 | }
|
|---|
| 131 | } else {
|
|---|
| 132 | endPos = buffered + mem.write(data, buffered, encoding);
|
|---|
| 133 | }
|
|---|
| 134 | } else {
|
|---|
| 135 | endPos = buffered + mem.write(data, buffered, encoding);
|
|---|
| 136 | }
|
|---|
| 137 | if (endPos < chunkSize) {
|
|---|
| 138 | this.buffered = endPos;
|
|---|
| 139 | } else {
|
|---|
| 140 | const l = endPos & ~(this.chunkSize - 1);
|
|---|
| 141 | exports.update(l);
|
|---|
| 142 | const newBuffered = endPos - l;
|
|---|
| 143 | this.buffered = newBuffered;
|
|---|
| 144 | if (newBuffered > 0) mem.copyWithin(0, l, endPos);
|
|---|
| 145 | }
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | /**
|
|---|
| 149 | * Update with buffer.
|
|---|
| 150 | * @param {Buffer} data data
|
|---|
| 151 | * @returns {void}
|
|---|
| 152 | */
|
|---|
| 153 | _updateWithBuffer(data) {
|
|---|
| 154 | const { exports, buffered, mem } = this;
|
|---|
| 155 | const length = data.length;
|
|---|
| 156 | if (buffered + length < this.chunkSize) {
|
|---|
| 157 | data.copy(mem, buffered, 0, length);
|
|---|
| 158 | this.buffered += length;
|
|---|
| 159 | } else {
|
|---|
| 160 | const l = (buffered + length) & ~(this.chunkSize - 1);
|
|---|
| 161 | if (l > 65536) {
|
|---|
| 162 | let i = 65536 - buffered;
|
|---|
| 163 | data.copy(mem, buffered, 0, i);
|
|---|
| 164 | exports.update(65536);
|
|---|
| 165 | const stop = l - buffered - 65536;
|
|---|
| 166 | while (i < stop) {
|
|---|
| 167 | data.copy(mem, 0, i, i + 65536);
|
|---|
| 168 | exports.update(65536);
|
|---|
| 169 | i += 65536;
|
|---|
| 170 | }
|
|---|
| 171 | data.copy(mem, 0, i, l - buffered);
|
|---|
| 172 | exports.update(l - buffered - i);
|
|---|
| 173 | } else {
|
|---|
| 174 | data.copy(mem, buffered, 0, l - buffered);
|
|---|
| 175 | exports.update(l);
|
|---|
| 176 | }
|
|---|
| 177 | const newBuffered = length + buffered - l;
|
|---|
| 178 | this.buffered = newBuffered;
|
|---|
| 179 | if (newBuffered > 0) data.copy(mem, 0, length - newBuffered, length);
|
|---|
| 180 | }
|
|---|
| 181 | }
|
|---|
| 182 |
|
|---|
| 183 | /**
|
|---|
| 184 | * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding}
|
|---|
| 185 | * @overload
|
|---|
| 186 | * @returns {Buffer} digest
|
|---|
| 187 | */
|
|---|
| 188 | /**
|
|---|
| 189 | * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding}
|
|---|
| 190 | * @overload
|
|---|
| 191 | * @param {string=} encoding encoding of the return value
|
|---|
| 192 | * @returns {string} digest
|
|---|
| 193 | */
|
|---|
| 194 | /**
|
|---|
| 195 | * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding}
|
|---|
| 196 | * @param {string=} encoding encoding of the return value
|
|---|
| 197 | * @returns {string | Buffer} digest
|
|---|
| 198 | */
|
|---|
| 199 | digest(encoding) {
|
|---|
| 200 | const { exports, buffered, mem, digestSize } = this;
|
|---|
| 201 | exports.final(buffered);
|
|---|
| 202 | this.instancesPool.push(this);
|
|---|
| 203 | const hex = mem.toString("latin1", 0, digestSize);
|
|---|
| 204 | if (encoding === "hex") return hex;
|
|---|
| 205 | if (encoding === "binary" || !encoding) return Buffer.from(hex, "hex");
|
|---|
| 206 | return Buffer.from(hex, "hex").toString(
|
|---|
| 207 | /** @type {NodeJS.BufferEncoding} */ (encoding)
|
|---|
| 208 | );
|
|---|
| 209 | }
|
|---|
| 210 | }
|
|---|
| 211 |
|
|---|
| 212 | /**
|
|---|
| 213 | * Returns wasm hash.
|
|---|
| 214 | * @param {WebAssembly.Module} wasmModule wasm module
|
|---|
| 215 | * @param {WasmHash[]} instancesPool pool of instances
|
|---|
| 216 | * @param {number} chunkSize size of data chunks passed to wasm
|
|---|
| 217 | * @param {number} digestSize size of digest returned by wasm
|
|---|
| 218 | * @returns {WasmHash} wasm hash
|
|---|
| 219 | */
|
|---|
| 220 | const create = (wasmModule, instancesPool, chunkSize, digestSize) => {
|
|---|
| 221 | if (instancesPool.length > 0) {
|
|---|
| 222 | const old = /** @type {WasmHash} */ (instancesPool.pop());
|
|---|
| 223 | old.reset();
|
|---|
| 224 | return old;
|
|---|
| 225 | }
|
|---|
| 226 |
|
|---|
| 227 | return new WasmHash(
|
|---|
| 228 | new WebAssembly.Instance(wasmModule),
|
|---|
| 229 | instancesPool,
|
|---|
| 230 | chunkSize,
|
|---|
| 231 | digestSize
|
|---|
| 232 | );
|
|---|
| 233 | };
|
|---|
| 234 |
|
|---|
| 235 | create.MAX_SHORT_STRING = MAX_SHORT_STRING;
|
|---|
| 236 |
|
|---|
| 237 | module.exports = create;
|
|---|