hash-digest.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. /** @typedef {import("../Hash")} Hash */
  7. /** @typedef {import("../../../declarations/WebpackOptions").HashDigest} Encoding */
  8. /** @typedef {"26" | "32" | "36" | "49" | "52" | "58" | "62"} Base */
  9. /* cSpell:disable */
  10. /** @type {Record<Base, string>} */
  11. const ENCODE_TABLE = Object.freeze({
  12. 26: "abcdefghijklmnopqrstuvwxyz",
  13. 32: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
  14. 36: "0123456789abcdefghijklmnopqrstuvwxyz",
  15. 49: "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ",
  16. 52: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
  17. 58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
  18. 62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  19. });
  20. /* cSpell:enable */
  21. const ZERO = BigInt("0");
  22. const EIGHT = BigInt("8");
  23. const FF = BigInt("0xff");
  24. /**
  25. * It encodes octet arrays by doing long divisions on all significant digits in the array, creating a representation of that number in the new base.
  26. * Then for every leading zero in the input (not significant as a number) it will encode as a single leader character.
  27. * This is the first in the alphabet and will decode as 8 bits. The other characters depend upon the base.
  28. * For example, a base58 alphabet packs roughly 5.858 bits per character.
  29. * This means the encoded string 000f (using a base16, 0-f alphabet) will actually decode to 4 bytes unlike a canonical hex encoding which uniformly packs 4 bits into each character.
  30. * While unusual, this does mean that no padding is required, and it works for bases like 43.
  31. * @param {Buffer} buffer buffer
  32. * @param {Base} base base
  33. * @returns {string} encoded buffer
  34. */
  35. const encode = (buffer, base) => {
  36. if (buffer.length === 0) return "";
  37. const bigIntBase = BigInt(ENCODE_TABLE[base].length);
  38. // Convert buffer to BigInt efficiently using bitwise operations
  39. let value = ZERO;
  40. for (let i = 0; i < buffer.length; i++) {
  41. value = (value << EIGHT) | BigInt(buffer[i]);
  42. }
  43. // Convert to baseX string efficiently using array
  44. /** @type {string[]} */
  45. const digits = [];
  46. if (value === ZERO) return ENCODE_TABLE[base][0];
  47. while (value > ZERO) {
  48. const remainder = Number(value % bigIntBase);
  49. digits.push(ENCODE_TABLE[base][remainder]);
  50. value /= bigIntBase;
  51. }
  52. return digits.reverse().join("");
  53. };
  54. /**
  55. * @param {string} data string
  56. * @param {Base} base base
  57. * @returns {Buffer} buffer
  58. */
  59. const decode = (data, base) => {
  60. if (data.length === 0) return Buffer.from("");
  61. const bigIntBase = BigInt(ENCODE_TABLE[base].length);
  62. // Convert the baseX string to a BigInt value
  63. let value = ZERO;
  64. for (let i = 0; i < data.length; i++) {
  65. const digit = ENCODE_TABLE[base].indexOf(data[i]);
  66. if (digit === -1) {
  67. throw new Error(`Invalid character at position ${i}: ${data[i]}`);
  68. }
  69. value = value * bigIntBase + BigInt(digit);
  70. }
  71. // If value is 0, return a single-byte buffer with value 0
  72. if (value === ZERO) {
  73. return Buffer.alloc(1);
  74. }
  75. // Determine buffer size efficiently by counting bytes
  76. let temp = value;
  77. let byteLength = 0;
  78. while (temp > ZERO) {
  79. temp >>= EIGHT;
  80. byteLength++;
  81. }
  82. // Create buffer and fill it from right to left
  83. const buffer = Buffer.alloc(byteLength);
  84. for (let i = byteLength - 1; i >= 0; i--) {
  85. buffer[i] = Number(value & FF);
  86. value >>= EIGHT;
  87. }
  88. return buffer;
  89. };
  90. // Compatibility with the old hash libraries, they can return different structures, so let's stringify them firstly
  91. /**
  92. * @param {string | { toString: (radix: number) => string }} value value
  93. * @param {string} encoding encoding
  94. * @returns {string} string
  95. */
  96. const toString = (value, encoding) =>
  97. typeof value === "string"
  98. ? value
  99. : Buffer.from(value.toString(16), "hex").toString(
  100. /** @type {NodeJS.BufferEncoding} */
  101. (encoding)
  102. );
  103. /**
  104. * @param {Buffer | { toString: (radix: number) => string }} value value
  105. * @returns {Buffer} buffer
  106. */
  107. const toBuffer = (value) =>
  108. Buffer.isBuffer(value) ? value : Buffer.from(value.toString(16), "hex");
  109. let isBase64URLSupported = false;
  110. try {
  111. isBase64URLSupported = Boolean(Buffer.from("", "base64url"));
  112. } catch (_err) {
  113. // Nothing
  114. }
  115. /**
  116. * @param {Hash} hash hash
  117. * @param {string | Buffer} data data
  118. * @param {Encoding=} encoding encoding of the return value
  119. * @returns {void}
  120. */
  121. const update = (hash, data, encoding) => {
  122. if (encoding === "base64url" && !isBase64URLSupported) {
  123. const base64String = /** @type {string} */ (data)
  124. .replace(/-/g, "+")
  125. .replace(/_/g, "/");
  126. const buf = Buffer.from(base64String, "base64");
  127. hash.update(buf);
  128. return;
  129. } else if (
  130. typeof data === "string" &&
  131. encoding &&
  132. typeof ENCODE_TABLE[/** @type {Base} */ (encoding.slice(4))] !== "undefined"
  133. ) {
  134. const buf = decode(data, /** @type {Base} */ (encoding.slice(4)));
  135. hash.update(buf);
  136. return;
  137. }
  138. if (encoding) {
  139. hash.update(/** @type {string} */ (data), encoding);
  140. } else {
  141. hash.update(data);
  142. }
  143. };
  144. /**
  145. * @overload
  146. * @param {Hash} hash hash
  147. * @returns {Buffer} digest
  148. */
  149. /**
  150. * @overload
  151. * @param {Hash} hash hash
  152. * @param {undefined} encoding encoding of the return value
  153. * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
  154. * @returns {Buffer} digest
  155. */
  156. /**
  157. * @overload
  158. * @param {Hash} hash hash
  159. * @param {Encoding} encoding encoding of the return value
  160. * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
  161. * @returns {string} digest
  162. */
  163. /**
  164. * @param {Hash} hash hash
  165. * @param {Encoding=} encoding encoding of the return value
  166. * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
  167. * @returns {string | Buffer} digest
  168. */
  169. const digest = (hash, encoding, isSafe) => {
  170. if (typeof encoding === "undefined") {
  171. return isSafe ? hash.digest() : toBuffer(hash.digest());
  172. }
  173. if (encoding === "base64url" && !isBase64URLSupported) {
  174. const digest = isSafe
  175. ? hash.digest("base64")
  176. : toString(hash.digest("base64"), "base64");
  177. return digest.replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]+$/, "");
  178. } else if (
  179. typeof ENCODE_TABLE[/** @type {Base} */ (encoding.slice(4))] !== "undefined"
  180. ) {
  181. const buf = isSafe ? hash.digest() : toBuffer(hash.digest());
  182. return encode(
  183. buf,
  184. /** @type {Base} */
  185. (encoding.slice(4))
  186. );
  187. }
  188. return isSafe
  189. ? hash.digest(encoding)
  190. : toString(hash.digest(encoding), encoding);
  191. };
  192. module.exports.decode = decode;
  193. module.exports.digest = digest;
  194. module.exports.encode = encode;
  195. module.exports.update = update;