wasm-hash.js 4.9 KB

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