gruel_cache/
fingerprint.rs1use std::fmt;
9
10pub fn blake3_bytes(bytes: &[u8]) -> CacheKey {
14 let mut h = Hasher::new();
15 h.update(bytes);
16 h.finalize()
17}
18
19#[derive(Default)]
26pub struct Hasher {
27 inner: blake3::Hasher,
28}
29
30impl Hasher {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn update(&mut self, bytes: &[u8]) -> &mut Self {
36 self.inner.update(bytes);
37 self
38 }
39
40 pub fn update_u32(&mut self, value: u32) -> &mut Self {
44 self.inner.update(&value.to_le_bytes());
45 self
46 }
47
48 pub fn update_u64(&mut self, value: u64) -> &mut Self {
50 self.inner.update(&value.to_le_bytes());
51 self
52 }
53
54 pub fn update_str(&mut self, s: &str) -> &mut Self {
58 self.update_u64(s.len() as u64);
59 self.inner.update(s.as_bytes());
60 self
61 }
62
63 pub fn finalize(self) -> CacheKey {
64 let hash = self.inner.finalize();
65 CacheKey {
66 bytes: *hash.as_bytes(),
67 }
68 }
69}
70
71#[derive(Clone, Copy, PartialEq, Eq, Hash)]
76pub struct CacheKey {
77 bytes: [u8; 32],
78}
79
80impl CacheKey {
81 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
85 Self { bytes }
86 }
87
88 pub const fn as_bytes(&self) -> &[u8; 32] {
89 &self.bytes
90 }
91
92 pub fn hex(&self) -> String {
94 let mut s = String::with_capacity(64);
95 for byte in &self.bytes {
96 s.push_str(&format!("{:02x}", byte));
97 }
98 s
99 }
100}
101
102impl fmt::Debug for CacheKey {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 let hex = self.hex();
106 write!(f, "CacheKey({}…)", &hex[..8])
107 }
108}
109
110impl fmt::Display for CacheKey {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(&self.hex())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn empty_input_has_stable_hash() {
122 let k = blake3_bytes(b"");
123 assert_eq!(
125 k.hex(),
126 "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
127 );
128 }
129
130 #[test]
131 fn update_str_is_length_prefixed() {
132 let mut h1 = Hasher::new();
135 h1.update_str("ab").update_str("c");
136 let mut h2 = Hasher::new();
137 h2.update_str("a").update_str("bc");
138 assert_ne!(h1.finalize(), h2.finalize());
139 }
140
141 #[test]
142 fn raw_update_is_not_length_prefixed() {
143 let mut h1 = Hasher::new();
148 h1.update(b"abc");
149 let mut h2 = Hasher::new();
150 h2.update(b"a").update(b"bc");
151 assert_eq!(h1.finalize(), h2.finalize());
152 }
153
154 #[test]
155 fn cache_key_hex_is_64_chars() {
156 let k = blake3_bytes(b"some content");
157 assert_eq!(k.hex().len(), 64);
158 assert!(k.hex().chars().all(|c| c.is_ascii_hexdigit()));
159 }
160
161 #[test]
162 fn debug_truncates() {
163 let k = blake3_bytes(b"x");
164 let dbg = format!("{:?}", k);
165 assert!(dbg.starts_with("CacheKey("));
166 assert!(dbg.ends_with("…)"));
167 }
168}