@hugoalh/fnv
Version:
A module to get the non-cryptographic hash of the data with algorithm Fowler-Noll-Vo (FNV).
162 lines (161 loc) • 5.58 kB
JavaScript
const variants = [
"0",
"1",
"1a"
];
const bitsParameters = new Map([
[32, {
offset: 2166136261n,
prime: 16777619n
}],
[64, {
offset: 14695981039346656037n,
prime: 1099511628211n
}],
[128, {
offset: 144066263297769815596495629667062367629n,
prime: 309485009821345068724781371n
}],
[256, {
offset: 100029257958052580907070968620625704837092796014241193945225284501741471925557n,
prime: 374144419156711147060143317175368453031918731002211n
}],
[512, {
offset: 9659303129496669498009435400716310466090418745672637896108374329434462657994582932197716438449813051892206539805784495328239340083876191928701583869517785n,
prime: 35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759n
}],
[1024, {
offset: 14197795064947621068722070641403218320880622795441933960878474914617582723252296732303717722150864096521202355549365628174669108571814760471015076148029755969804077320157692458563003215304957150157403644460363550505412711285966361610267868082893823963790439336411086884584107735010676915n,
prime: 5016456510113118655434598811035278955030765345404790744303017523831112055108147451509157692220295382716162651878526895249385292291816524375083746691371804094271873160484737966720260389217684476157468082573n
}]
]);
/**
* Get the non-cryptographic hash of the data with algorithm Fowler-Noll-Vo (FNV).
*/
export class FNV {
get [Symbol.toStringTag]() {
return `FNV-${this.#variant}-${this.#size}`;
}
#freezed = false;
#hashHex = null;
#hashUint8Array = null;
#prime;
#size;
#variant;
#bin = 0n;
/**
* Initialize.
* @param {FNVVariant} variant Variant of the FNV.
* @param {FNVBitsSize} size Bits size of the FNV.
* @param {FNVAcceptDataType} [data] Data. Can append later via the method {@linkcode FNV.update} and {@linkcode FNV.updateFromStream}.
*/
constructor(variant, size, data) {
if (!variants.includes(variant)) {
throw new RangeError(`\`${variant}\` is not a valid FNV variant! Only accept these values: ${variants.join(", ")}`);
}
this.#variant = variant;
const parameter = bitsParameters.get(size);
if (typeof parameter === "undefined") {
throw new RangeError(`\`${size}\` is not a valid FNV hash bits size! Only accept these values: ${Array.from(bitsParameters.keys()).join(", ")}`);
}
const { offset, prime } = parameter;
if (this.#variant !== "0") {
this.#bin = offset;
}
this.#prime = prime;
this.#size = size;
if (typeof data !== "undefined") {
this.update(data);
}
}
/**
* Whether the instance is freezed.
* @returns {boolean}
*/
get freezed() {
return this.#freezed;
}
/**
* Bits size of the FNV.
* @returns {FNVBitsSize}
*/
get size() {
return this.#size;
}
/**
* Variant of the FNV.
* @returns {FNVVariant}
*/
get variant() {
return this.#variant;
}
/**
* Freeze the instance to prevent any further update.
* @returns {this}
*/
freeze() {
this.#freezed = true;
return this;
}
/**
* Get the non-cryptographic hash of the data, in Uint8Array.
* @returns {Uint8Array}
*/
hash() {
if (this.#hashUint8Array === null) {
const hex = this.hashHex();
const bytes = [];
for (let index = 0; index < hex.length; index += 2) {
bytes.push(hex.slice(index, index + 2));
}
this.#hashUint8Array = Uint8Array.from(bytes.map((byte) => {
return Number.parseInt(byte, 16);
}));
}
return Uint8Array.from(this.#hashUint8Array);
}
/**
* Get the non-cryptographic hash of the data, in hexadecimal with padding.
* @returns {string}
*/
hashHex() {
if (this.#hashHex === null) {
this.#hashHex = this.#bin.toString(16).toUpperCase().padStart(this.#size / 4, "0");
if (this.#hashHex.length !== this.#size / 4) {
throw new Error(`Unexpected hash hex result \`${this.#hashHex}\`! Please submit a bug report.`);
}
}
return this.#hashHex;
}
/**
* Append data.
* @param {FNVAcceptDataType} data Data.
* @returns {this}
*/
update(data) {
if (this.#freezed) {
throw new Error(`Instance is freezed!`);
}
this.#hashHex = null;
this.#hashUint8Array = null;
const dataFmt = (typeof data === "string") ? new TextEncoder().encode(data) : Uint8Array.from(data);
for (const byte of dataFmt) {
this.#bin = (this.#variant === "1a")
? BigInt.asUintN(this.#size, (this.#bin ^ BigInt(byte)) * this.#prime)
: BigInt.asUintN(this.#size, (this.#bin * this.#prime) ^ BigInt(byte));
}
return this;
}
/**
* Append data from the readable stream.
* @param {ReadableStream<FNVAcceptDataType>} stream Data from the readable stream.
* @returns {Promise<this>}
*/
async updateFromStream(stream) {
for await (const chunk of stream) {
this.update(chunk);
}
return this;
}
}
export default FNV;