asset-hash
Version:
Very fast asset hashing function for using e.g. during front-end deployments.
240 lines (204 loc) • 6.58 kB
JavaScript
/*! asset-hash v4.1.0 by Sebastian Werner <s.werner@sebastian-software.de> */
import 'core-js/modules/es.error.cause.js';
import { createReadStream } from 'fs';
import { extname } from 'path';
import { createHash } from 'crypto';
import xxhash from 'xxhash-wasm';
import { fingerprint32, fingerprint64 } from 'farmhash';
function baseEncodeFactory(charset) {
const radix = BigInt(charset.length);
const decode = str => {
let result = BigInt(0);
for (const chr of str) {
const index = BigInt(charset.indexOf(chr));
result = radix * result + index;
}
return result;
};
const encode = (number, maxLength = Infinity) => {
let result = "";
while (number > 0) {
const mod = number % radix;
result = charset[Number(mod)] + result;
number = (number - mod) / radix;
if (result.length === maxLength) {
return result;
}
}
return result || `0`;
};
return {
decode,
encode
};
}
const baseEncoder = {
base26: baseEncodeFactory("abcdefghijklmnopqrstuvwxyz"),
base32: baseEncodeFactory("123456789abcdefghjkmnpqrstuvwxyz"),
// no 0lio
base36: baseEncodeFactory("0123456789abcdefghijklmnopqrstuvwxyz"),
base49: baseEncodeFactory("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"),
// no lIO
base52: baseEncodeFactory("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"),
base58: baseEncodeFactory("123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"),
// no 0lIO
base62: baseEncodeFactory("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
};
function computeDigest(rawDigest, options = {}) {
let output = null;
const {
encoding,
maxLength
} = options;
const rawIsNumber = typeof rawDigest === "number" || typeof rawDigest === "bigint";
const rawIsBuffer = rawDigest instanceof Buffer; // Fast-path for number => hex
if (rawIsNumber && encoding === "hex") {
output = rawDigest.toString(16);
} else {
if (encoding === "base26" || encoding === "base32" || encoding === "base36" || encoding === "base49" || encoding === "base52" || encoding === "base58" || encoding === "base62") {
const valueAsBigInt = rawDigest instanceof Buffer ? BigInt(`0x${rawDigest.toString("hex")}`) : typeof rawDigest === "number" ? BigInt(rawDigest) : BigInt(rawDigest);
return baseEncoder[encoding].encode(valueAsBigInt, maxLength);
}
const valueAsBuffer = rawIsBuffer ? rawDigest : Buffer.from(rawDigest.toString());
output = valueAsBuffer.toString(encoding);
}
return maxLength != null ? output.slice(0, maxLength) : output;
}
let xxHashInstance;
let hasherReady = false;
async function initHashClasses() {
if (hasherReady) {
return;
} // Allow failing initialization for platforms < Node v16
try {
xxHashInstance = await xxhash();
} catch {}
hasherReady = true;
}
/**
* Make Node.js crypto hash signature compatible to Hash
*/
function cryptoEnvelope(hash) {
const envelopeHash = {
update: input => {
hash.update(input);
return envelopeHash;
},
digest: () => hash.digest()
};
return envelopeHash;
}
function farmhashEnvelope(hash) {
const data = [];
const envelopeHash = {
update: input => {
data.push(input);
return envelopeHash;
},
digest: () => {
const stringOrNumber = hash(Buffer.concat(data));
return BigInt(stringOrNumber);
}
};
return envelopeHash;
}
/**
* Creates hasher instance
*/
function createHasher(algorithm) {
if (!hasherReady) {
throw new Error("Hasher was not correctly initialized. Call `await initHashClasses()` first.");
}
let hasher;
if (algorithm === "xxhash32" || algorithm === "xxhash64") {
if (!xxHashInstance) {
throw new Error("Invalid xxhash WASM instance!");
}
hasher = algorithm === "xxhash32" ? xxHashInstance.create32() : xxHashInstance.create64();
} else if (algorithm === "farmhash32" || algorithm === "farmhash64") {
hasher = farmhashEnvelope(algorithm === "farmhash32" ? fingerprint32 : fingerprint64);
} else {
hasher = cryptoEnvelope(createHash(algorithm));
}
return hasher;
}
const NODE_MAJOR_VERSION = parseInt(process.versions.node.split('.')[0], 10);
const NODE_SUPPORTS_BIGINT_SINCE = 16;
const DEFAULT_ALGORITHM = NODE_MAJOR_VERSION < NODE_SUPPORTS_BIGINT_SINCE ? "farmhash64" : "xxhash64";
const DEFAULT_ENCODING = "base52";
const DEFAULT_MAX_LENGTH = 8;
class Hasher {
algorithm = DEFAULT_ALGORITHM;
encoding = DEFAULT_ENCODING;
maxLength = Infinity;
constructor(options = {}) {
if (options.algorithm) {
this.algorithm = options.algorithm;
}
if (options.encoding) {
this.encoding = options.encoding;
}
if (options.maxLength) {
this.maxLength = options.maxLength;
}
this.hasher = createHasher(this.algorithm);
}
update(data) {
const buffer = data instanceof Buffer ? data : Buffer.from(data.toString(), "utf-8");
this.hasher.update(buffer);
return this;
}
digest(encoding, maxLength) {
return computeDigest(this.hasher.digest(), {
encoding: encoding || this.encoding,
maxLength: maxLength || this.maxLength
});
}
}
/**
* Get hash from file
*
* @param fileName Filename of file to hash
*/
function getHash(fileName, options) {
const {
algorithm,
encoding,
maxLength
} = options || {};
return new Promise(async (resolve, reject) => {
try {
await initHashClasses();
const hasher = createHasher(algorithm || DEFAULT_ALGORITHM);
createReadStream(fileName).on("data", data => {
hasher.update(data);
}).on("error", error => {
reject(error);
}).on("end", () => {
try {
const digest = computeDigest(hasher.digest(), {
encoding: encoding || DEFAULT_ENCODING,
maxLength: maxLength || DEFAULT_MAX_LENGTH
});
digest ? resolve(digest) : reject(new Error("Unexpected error while generating digest!"));
} catch (error) {
reject(error);
}
});
} catch (error) {
reject(error);
}
});
}
/**
* Get new filename based upon hash and extension of file
*
* @param fileName Filename of file to hash
*/
async function getHashedName(fileName, options) {
const hashed = await getHash(fileName, options);
const extension = extname(fileName);
return hashed + extension;
}
export { DEFAULT_ALGORITHM, DEFAULT_ENCODING, DEFAULT_MAX_LENGTH, Hasher, getHash, getHashedName, initHashClasses };
//# sourceMappingURL=index.esm.js.map