wasm-hash.js 6.7 KB

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