1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! Content hashes and non-cryptographic change-detection fingerprints.
use core::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Compute the 64-bit FNV-1a fingerprint of every byte in `data`.
///
/// Uses the standard offset basis and wrapping multiplication. This is a
/// non-cryptographic fingerprint; it is not an integrity or security boundary.
pub fn fnv1a_64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
/// SHA-256 of optional parsed text and an exact raw record.
///
/// The frame is a one-byte presence marker, followed, when text is present,
/// by its big-endian `u64` UTF-8 byte length and bytes. The raw record always
/// follows as its big-endian `u64` byte length and bytes. No normalization is
/// performed. This is the persisted session-mirror content-hash format; absent
/// text, empty text, and delimiter-containing values remain distinct.
///
/// Requires the `sha2` feature and does not allocate.
#[cfg(feature = "sha2")]
pub fn framed_text_sha256(text: Option<&str>, raw: &str) -> Hash32 {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
match text {
Some(value) => {
hash.update([1]);
hash.update((value.len() as u64).to_be_bytes());
hash.update(value.as_bytes());
}
None => hash.update([0]),
}
hash.update((raw.len() as u64).to_be_bytes());
hash.update(raw.as_bytes());
Hash32::from_bytes(hash.finalize().into())
}
/// 256-bit (32-byte) content hash.
///
/// Used as a content-addressed identifier for HNSW checkpoints and other
/// snapshot artifacts. The underlying algorithm is caller-defined; the type
/// carries the raw bytes without encoding assumptions.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Hash32([u8; 32]);
impl Hash32 {
/// Zero hash (nil value).
pub const ZERO: Self = Self([0u8; 32]);
/// Construct from raw bytes.
#[inline]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
/// Return the raw byte representation.
#[inline]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Compute a BLAKE3 hash over the given byte slice.
///
/// Requires the `blake3` feature.
#[cfg(feature = "blake3")]
#[inline]
pub fn from_blake3(data: &[u8]) -> Self {
let hash = blake3::hash(data);
Self(*hash.as_bytes())
}
/// Constant-time equality check.
///
/// Accumulates XOR over all 32 bytes without early exit so the comparison
/// takes the same number of iterations regardless of where bytes differ.
/// Suitable for integrity comparisons where timing side-channels are a
/// concern. The `#[inline(never)]` attribute discourages the compiler from
/// inlining and optimising away the full-loop traversal.
#[inline(never)]
pub fn eq_ct(&self, other: &Self) -> bool {
let diff = self
.0
.iter()
.zip(other.0.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b));
diff == 0
}
}
impl From<[u8; 32]> for Hash32 {
#[inline]
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl fmt::Debug for Hash32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash32(")?;
for b in &self.0 {
write!(f, "{b:02x}")?;
}
write!(f, ")")
}
}
impl fmt::Display for Hash32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0 {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
#[cfg(test)]
#[path = "hash_tests.rs"]
mod tests;