lib_fnv.js

/**
 * FNV (Fowler/Noll/Vo) is a fast, 
 * non-cryptographic hash algorithm with good dispersion. 
 * FNV hashes are designed to be fast while maintaining a low collision
 * rate. The high dispersion of the FNV hashes makes them well suited
 * for hashing nearly identical strings such as URLs, hostnames,
 * filenames, text, IP addresses, etc. Their speed allows one to quickly
 * hash lots of data while maintaining a reasonably low collision rate.
 * However, they are generally not suitable for cryptographic use.
 * @see http://tools.ietf.org/html/draft-eastlake-fnv-04 
 * @module blossom/fnv
 */

'use strict';

const { Transform } = require('node:stream');


class FNV extends Transform {
  /**
   * Implements FNV hash function. 
   * @constructor
   * @extends {stream.Transform}
   * @param {object} [streamOptions] - {@link https://nodejs.org/api/stream.html#api-for-stream-consumers}
   * @example
   * const { fnv } = require('@yipsec/blossom');
   * const fnvHash = fnv.createHash();
   *
   * fnvHash.update('acab', 'hex');
   * console.log(fnvHash.digest('hex'));
   * @example
   * const { fnv } = require('@yipsec/blossom');
   * const fnvHash = new fnv.FNV();
   *
   * fnvHash.write('acab', 'hex');
   * fnvHash.pipe(process.stdout);
   */
  constructor(options) {
    super(options);
    this.hash = 0x811c9dc5; // offset basis 
  }

  _transform(data, encoding, callback) {
    try {
      this.update(data, encoding === 'buffer'
        ? undefined
        : encoding);
    } catch (e) {
      return callback(e);
    }

    callback(null);
  }

  _flush(callback) {
    callback(null, this.digest());
  }

  /**
   * Updates the hash function with the given data
   * @param {string|Buffer} data - Data to hash
   * @param {string} [encoding] - If data is a string, specify the encoding
   * @returns {FNV}
   */
  update(data, encoding) {
    if (typeof data === 'string') {
      data = Buffer.from(data, encoding);
    } else if (!Buffer.isBuffer(data)) {
      throw TypeError('Expected a String or Buffer');
    }
    
    for (let i = 0; i < data.length; i++) {
      this.hash = this.hash ^ data[i];

      // 32 bit FNV prime = 2**24 + 2**8 + 0x93
      this.hash += 
        (this.hash << 24) + 
        (this.hash << 8) + 
        (this.hash << 7) + 
        (this.hash << 4) + 
        (this.hash << 1);
    }

    return this;
  }

  /**
   * Returns the computed FNV hash
   * @param {string} [encoding] - Encoding type to return
   * @returns {string|Buffer}
   */
  digest(encoding) {
    const buf = Buffer.alloc(4);

    buf.writeInt32BE(this.hash & 0xffffffff, 0);
    
    if (encoding) {
      return buf.toString(encoding);
    } else {
      return buf;
    }
  }

  /**
   * Returns the current hash value
   * @returns {number}
   */ 
  value() {
    return this.hash & 0xffffffff;
  }

}

/**
 * @function createHash
 * @memberof module:blossom/fnv
 * @returns {FNV}
 */
function createHash() {
  return new FNV();
}

module.exports.FNV = FNV
module.exports.createHash = createHash;