const FX_SEED64: u64 = 0x517c_c1b7_2722_0a95;
const FX_ROTATE: u32 = 5;
#[must_use]
pub(crate) fn hash(bytes: &[u8]) -> u64 {
let mut state = 0_u64;
let mut cursor = 0_usize;
while cursor + 8 <= bytes.len() {
let mut word = [0_u8; 8];
word.copy_from_slice(&bytes[cursor..cursor + 8]);
state = mix(state, u64::from_le_bytes(word));
cursor += 8;
}
while cursor < bytes.len() {
state = mix(state, u64::from(bytes[cursor]));
cursor += 1;
}
state
}
#[inline]
const fn mix(state: u64, word: u64) -> u64 {
(state.rotate_left(FX_ROTATE) ^ word).wrapping_mul(FX_SEED64)
}
#[cfg(test)]
mod tests {
use super::hash;
#[test]
fn empty_input_hashes_to_zero() {
assert_eq!(hash(&[]), 0);
}
#[test]
fn hash_is_deterministic() {
let key = b"the quick brown fox";
assert_eq!(hash(key), hash(key));
}
#[test]
fn different_inputs_produce_different_hashes() {
let mut seen = std::collections::HashSet::new();
for i in 0_u32..1024 {
let key = format!("key-{i}");
let h = hash(key.as_bytes());
assert!(seen.insert(h), "duplicate hash for {key}");
}
}
#[test]
fn distribution_across_low_bits_is_balanced() {
let mut buckets = [0_u32; 32];
for i in 0_u32..10_000 {
let key = format!("k{i}");
buckets[(hash(key.as_bytes()) & 31) as usize] += 1;
}
for count in buckets {
assert!(count > 0, "shard distribution missed a bucket");
}
}
#[test]
fn matches_documented_seed_constant() {
assert_eq!(hash(&[1]), 0x517c_c1b7_2722_0a95);
assert_eq!(hash(&[0]), 0);
assert_eq!(hash(&[0; 16]), 0);
}
#[test]
fn long_input_completes_in_eight_byte_strides_then_tail() {
let key = b"thirteen-byte";
assert_eq!(key.len(), 13);
let h = hash(key);
assert_ne!(h, 0);
let key2 = b"thirteen-bytefoo";
assert_ne!(h, hash(key2));
}
}