hash-digest.js 7.2 KB

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