Skip to main content

schema_core/config/
content_hash.rs

1use std::hash::{Hash, Hasher};
2
3const FNV_OFFSET: u32 = 2_166_136_261;
4const FNV_PRIME: u32 = 16_777_619;
5
6#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
7pub struct ContentHash(u32);
8
9impl ContentHash {
10    pub fn new(value: u32) -> Self {
11        Self(value)
12    }
13
14    /// The content hash of any [`Hash`] value, via FNV-1a. Deterministic for a
15    /// given structure — the same parsed value always hashes the same — which
16    /// is what makes it usable as a stable, structure-derived identifier.
17    pub fn of<T: Hash>(value: &T) -> Self {
18        let mut hasher = Fnv1aHasher::default();
19        value.hash(&mut hasher);
20        Self(hasher.0)
21    }
22}
23
24impl std::fmt::Display for ContentHash {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{:08x}", self.0)
27    }
28}
29
30/// Serializes as the eight-hex-digit string (its [`Display`](std::fmt::Display)) —
31/// the same form that suffixes a physical index name — rather than the raw `u32`.
32impl serde::Serialize for ContentHash {
33    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
34        serializer.collect_str(self)
35    }
36}
37
38/// An FNV-1a [`Hasher`], so any [`Hash`] value yields a stable [`ContentHash`]
39/// independent of the platform's default (randomized) hasher.
40struct Fnv1aHasher(u32);
41
42impl Default for Fnv1aHasher {
43    fn default() -> Self {
44        Self(FNV_OFFSET)
45    }
46}
47
48impl Hasher for Fnv1aHasher {
49    fn finish(&self) -> u64 {
50        u64::from(self.0)
51    }
52
53    fn write(&mut self, bytes: &[u8]) {
54        for byte in bytes {
55            self.0 ^= u32::from(*byte);
56            self.0 = self.0.wrapping_mul(FNV_PRIME);
57        }
58    }
59}
60
61#[cfg(test)]
62#[allow(clippy::unwrap_used)]
63mod tests;