wasm-hash.js 6.5 KB

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