triplets_core/hash.rs
1use siphasher::sip::SipHasher;
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4
5/// Run `f` against a fresh [`SipHasher`] and return the resulting digest.
6///
7/// Uses the pinned `siphasher` crate (SipHash-2-4) instead of
8/// `std::collections::hash_map::DefaultHasher`, guaranteeing stable output
9/// across Rust compiler versions and platforms.
10pub fn stable_hash_with(f: impl FnOnce(&mut SipHasher)) -> u64 {
11 let mut hasher = SipHasher::new();
12 f(&mut hasher);
13 hasher.finish()
14}
15
16/// Return a deterministic u64 hash of `value` mixed with `seed`.
17///
18/// Suitable for deriving per-record RNG seeds where `seed` is a global base
19/// (e.g. a shuffle seed constant) and `value` is a unique identifier such as
20/// a ticker symbol or record id.
21pub fn stable_hash_str(seed: u64, value: &str) -> u64 {
22 stable_hash_with(|hasher| {
23 seed.hash(hasher);
24 value.hash(hasher);
25 })
26}
27
28/// Return a deterministic u64 hash of the string form of `path` mixed with `seed`.
29pub fn stable_hash_path(seed: u64, path: &Path) -> u64 {
30 stable_hash_with(|hasher| {
31 seed.hash(hasher);
32 path.to_string_lossy().hash(hasher);
33 })
34}
35
36/// Derive a per-epoch seed by mixing an epoch counter (or constant offset) into a base seed.
37///
38/// All seed derivations that incorporate an epoch value must go through this function so the
39/// derivation strategy can be changed in one place. Both the source-shuffling path
40/// (`base_seed ^ epoch`) and the epoch-tracker initialisation path
41/// (`base_seed ^ EPOCH_SEED_OFFSET`) are expressed as `derive_epoch_seed(base_seed, epoch)`.
42pub fn derive_epoch_seed(base_seed: u64, epoch: u64) -> u64 {
43 base_seed ^ epoch
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 /// Ensures the pinned SipHash-2-4 produces a known digest for a fixed input.
51 /// If this test fails, the hasher has changed — update the expected value.
52 #[test]
53 fn stable_hash_str_is_deterministic() {
54 let result = stable_hash_str(42, "hello");
55 assert_eq!(result, 16678829552985060110);
56 }
57
58 #[test]
59 fn stable_hash_with_produces_consistent_results() {
60 let a = stable_hash_str(0, "test");
61 let b = stable_hash_str(0, "test");
62 assert_eq!(a, b);
63 }
64}