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