jamhash 0.1.3

Fast non-cryptographic hashing for canonical packed DNA kmers
Documentation
use jamhash::{JamHasher, jamhash_bytes, jamhash_u64};
use std::hash::{BuildHasher, BuildHasherDefault, Hasher};

#[cfg(target_endian = "little")]
const HELLO_BYTES: u64 = 0x128b_7e0f_70cd_886b;
#[cfg(target_endian = "big")]
const HELLO_BYTES: u64 = 0xa917_0142_0231_0b04;
#[cfg(target_endian = "little")]
const ONE_WRITE: u64 = 0x9370_1e7f_4c96_14e1;
#[cfg(target_endian = "big")]
const ONE_WRITE: u64 = 0xa1ea_ee6f_3bbb_e049;
#[cfg(target_endian = "little")]
const SPLIT_WRITE: u64 = 0xf088_8fb6_ac3a_0380;
#[cfg(target_endian = "big")]
const SPLIT_WRITE: u64 = 0x9e1e_5217_2841_bcaa;

fn hash_frames(frames: &[&[u8]]) -> u64 {
    let mut hasher = JamHasher::new();
    for frame in frames {
        hasher.write(frame);
    }
    hasher.finish()
}

#[test]
fn documented_u64_vectors_are_stable() {
    assert_eq!(jamhash_u64(0), 0);
    assert_eq!(jamhash_u64(1), 0x2d67_3a90_c3c1_4c02);
    assert_eq!(jamhash_u64(0x0000_006f_93b4_b1bc), 0xaf47_a961_8481_4793);
    assert_eq!(jamhash_u64(u64::MAX), u64::MAX);
}

#[test]
fn empty_input_and_empty_writes_are_noops() {
    assert_eq!(jamhash_bytes(b""), 0);
    assert_eq!(hash_frames(&[]), 0);
    assert_eq!(hash_frames(&[b""]), 0);
    assert_eq!(hash_frames(&[b"", b"hello world", b""]), ONE_WRITE);
}

#[test]
fn one_and_split_write_contract_is_stable() {
    assert_eq!(jamhash_bytes(b"hello world"), HELLO_BYTES);
    assert_eq!(hash_frames(&[b"hello world"]), ONE_WRITE);
    assert_eq!(hash_frames(&[b"hello ", b"world"]), SPLIT_WRITE);
    assert_ne!(ONE_WRITE, SPLIT_WRITE);
    assert_ne!(HELLO_BYTES, ONE_WRITE);
}

#[test]
fn build_hasher_default_uses_the_default_call_sequence_hasher() {
    let builder = BuildHasherDefault::<JamHasher>::default();
    let mut built = builder.build_hasher();
    built.write(b"hello ");
    built.write(b"world");

    assert_eq!(built.finish(), SPLIT_WRITE);
    assert_eq!(built.finish(), hash_frames(&[b"hello ", b"world"]));
}

#[test]
fn byte_inputs_use_native_endian_words() {
    const LENGTH_MIXER: u64 = 0xa409_3822_299f_31d0;
    let bytes = [0, 1, 2, 3, 4, 5, 6, 7];
    let native_word = u64::from_ne_bytes(bytes);
    let expected = jamhash_u64(native_word ^ 8u64.wrapping_mul(LENGTH_MIXER));

    assert_eq!(jamhash_bytes(&bytes), expected);
}

#[test]
fn unsafe_byte_read_boundaries() {
    const LENGTHS: &[usize] = &[
        0, 1, 3, 4, 7, 8, 9, 16, 17, 18, 31, 32, 33, 63, 64, 65, 127, 128, 129, 130, 191, 192, 193,
        4095, 4096, 4097,
    ];

    for &len in LENGTHS {
        let expected_input: Vec<u8> = (0..len)
            .map(|index| (index as u8).wrapping_mul(37).wrapping_add(11))
            .collect();
        let expected_hash = jamhash_bytes(&expected_input);

        for offset in 0..8 {
            let mut storage = vec![0xa5; offset + len + 8];
            storage[offset..offset + len].copy_from_slice(&expected_input);
            let input = &storage[offset..offset + len];

            assert_eq!(
                jamhash_bytes(input),
                expected_hash,
                "len={len}, offset={offset}"
            );

            let mut hasher = JamHasher::new();
            hasher.write(input);
            assert_eq!(
                hasher.finish(),
                hash_frames(&[&expected_input]),
                "JamHasher len={len}, offset={offset}"
            );
        }
    }
}