1use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
13pub struct ContentHash([u8; 32]);
14
15impl ContentHash {
16 pub fn from_bytes(bytes: [u8; 32]) -> Self {
18 Self(bytes)
19 }
20
21 pub fn compute(content: &[u8]) -> Self {
23 Self(blake3::hash(content).into())
24 }
25
26 pub fn compute_typed(type_prefix: &str, content: &[u8]) -> Self {
28 let mut hasher = Self::typed_hasher(type_prefix, content.len() as u64);
29 hasher.update(content);
30 Self(hasher.finalize().into())
31 }
32
33 pub fn typed_hasher(type_prefix: &str, content_len: u64) -> blake3::Hasher {
35 let mut hasher = blake3::Hasher::new();
36 hasher.update(type_prefix.as_bytes());
37 hasher.update(&content_len.to_le_bytes());
38 hasher.update(&[0]);
39 hasher
40 }
41
42 pub fn compute_typed_with_len(
44 type_prefix: &str,
45 content_len: u64,
46 update: impl FnOnce(&mut blake3::Hasher),
47 ) -> Self {
48 let mut hasher = Self::typed_hasher(type_prefix, content_len);
49 update(&mut hasher);
50 Self(hasher.finalize().into())
51 }
52
53 pub fn as_bytes(&self) -> &[u8; 32] {
55 &self.0
56 }
57
58 pub fn to_hex(&self) -> String {
60 hex::encode(self.0)
61 }
62
63 pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
65 let bytes = hex::decode(s)?;
66 if bytes.len() != 32 {
67 return Err(hex::FromHexError::InvalidStringLength);
68 }
69 let mut arr = [0u8; 32];
70 arr.copy_from_slice(&bytes);
71 Ok(Self(arr))
72 }
73
74 pub fn short(&self) -> String {
76 self.to_hex()[..8].to_string()
77 }
78
79 pub fn matches_prefix(&self, prefix: &str) -> bool {
81 self.to_hex().starts_with(prefix)
82 }
83}
84
85impl fmt::Debug for ContentHash {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "ContentHash({})", self.short())
88 }
89}
90
91impl fmt::Display for ContentHash {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "{}", self.to_hex())
94 }
95}
96
97#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
99pub struct ChangeId([u8; 16]);
100
101impl ChangeId {
102 pub fn generate() -> Self {
104 Self(rand::random())
105 }
106
107 pub fn from_bytes(bytes: [u8; 16]) -> Self {
109 Self(bytes)
110 }
111
112 pub fn try_from_slice(bytes: &[u8]) -> Result<Self, ChangeIdParseError> {
115 if bytes.len() != 16 {
116 return Err(ChangeIdParseError::InvalidLength);
117 }
118 let mut arr = [0u8; 16];
119 arr.copy_from_slice(bytes);
120 Ok(Self(arr))
121 }
122
123 pub fn as_bytes(&self) -> &[u8; 16] {
125 &self.0
126 }
127
128 pub fn to_string_full(&self) -> String {
130 format!(
131 "hc-{}",
132 base32::encode(base32::Alphabet::Crockford, &self.0).to_lowercase()
133 )
134 }
135
136 pub fn short(&self) -> String {
138 let full = self.to_string_full();
139 full[..15.min(full.len())].to_string()
140 }
141
142 pub fn parse(s: &str) -> Result<Self, ChangeIdParseError> {
144 let s = s.strip_prefix("hc-").unwrap_or(s);
145 let bytes = base32::decode(base32::Alphabet::Crockford, &s.to_uppercase())
146 .ok_or(ChangeIdParseError::InvalidBase32)?;
147 if bytes.len() != 16 {
148 return Err(ChangeIdParseError::InvalidLength);
149 }
150 let mut arr = [0u8; 16];
151 arr.copy_from_slice(&bytes);
152 Ok(Self(arr))
153 }
154
155 pub fn is_zero(&self) -> bool {
157 self.0 == [0u8; 16]
158 }
159}
160
161#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, Default)]
163pub struct StateId([u8; 32]);
164
165impl StateId {
166 pub fn from_bytes(bytes: [u8; 32]) -> Self {
167 Self(bytes)
168 }
169
170 pub fn from_content_hash(hash: ContentHash) -> Self {
171 Self(*hash.as_bytes())
172 }
173
174 pub fn try_from_slice(bytes: &[u8]) -> Result<Self, StateIdParseError> {
175 if bytes.len() != 32 {
176 return Err(StateIdParseError::InvalidLength);
177 }
178 let mut value = [0; 32];
179 value.copy_from_slice(bytes);
180 Ok(Self(value))
181 }
182
183 pub fn as_bytes(&self) -> &[u8; 32] {
184 &self.0
185 }
186
187 pub fn as_content_hash(&self) -> ContentHash {
188 ContentHash::from_bytes(self.0)
189 }
190
191 pub fn to_string_full(&self) -> String {
192 format!(
193 "hs-{}",
194 base32::encode(base32::Alphabet::Crockford, &self.0).to_lowercase()
195 )
196 }
197
198 pub fn short(&self) -> String {
199 let full = self.to_string_full();
200 full[..18.min(full.len())].to_string()
201 }
202
203 pub fn parse(s: &str) -> Result<Self, StateIdParseError> {
204 let encoded = s.strip_prefix("hs-").unwrap_or(s);
205 let bytes = base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase())
206 .ok_or(StateIdParseError::InvalidBase32)?;
207 Self::try_from_slice(&bytes)
208 }
209
210 pub fn is_zero(&self) -> bool {
211 self.0 == [0; 32]
212 }
213}
214
215impl fmt::Debug for StateId {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 write!(f, "StateId({})", self.short())
218 }
219}
220
221impl fmt::Display for StateId {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 f.write_str(&self.short())
224 }
225}
226
227#[derive(Debug, Clone, thiserror::Error)]
228pub enum StateIdParseError {
229 #[error("invalid base32 encoding")]
230 InvalidBase32,
231 #[error("invalid length (expected 32 bytes)")]
232 InvalidLength,
233}
234
235impl fmt::Debug for ChangeId {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 write!(f, "ChangeId({})", self.short())
238 }
239}
240
241impl fmt::Display for ChangeId {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(f, "{}", self.short())
244 }
245}
246
247#[derive(Debug, Clone, thiserror::Error)]
249pub enum ChangeIdParseError {
250 #[error("invalid base32 encoding")]
251 InvalidBase32,
252 #[error("invalid length (expected 16 bytes)")]
253 InvalidLength,
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_content_hash_compute() {
262 let hash = ContentHash::compute(b"hello world");
263 assert_eq!(hash.to_hex().len(), 64);
264
265 let hash2 = ContentHash::compute(b"hello world");
266 assert_eq!(hash, hash2);
267
268 let hash3 = ContentHash::compute(b"hello world!");
269 assert_ne!(hash, hash3);
270 }
271
272 #[test]
273 fn test_content_hash_typed() {
274 let hash1 = ContentHash::compute_typed("blob", b"hello");
275 let hash2 = ContentHash::compute_typed("tree", b"hello");
276 assert_ne!(hash1, hash2);
277 }
278
279 #[test]
280 fn test_content_hash_hex_roundtrip() {
281 let hash = ContentHash::compute(b"test");
282 let hex = hash.to_hex();
283 let parsed = ContentHash::from_hex(&hex).unwrap();
284 assert_eq!(hash, parsed);
285 }
286
287 #[test]
288 fn test_change_id_generate() {
289 let id1 = ChangeId::generate();
290 let id2 = ChangeId::generate();
291 assert_ne!(id1, id2);
292 assert!(!id1.is_zero());
293 }
294
295 #[test]
296 fn test_change_id_roundtrip() {
297 let id = ChangeId::generate();
298 let s = id.to_string_full();
299 assert!(s.starts_with("hc-"));
300 let parsed = ChangeId::parse(&s).unwrap();
301 assert_eq!(id, parsed);
302 }
303
304 #[test]
305 fn test_change_id_short() {
306 let id = ChangeId::generate();
307 let short = id.short();
308 assert!(short.starts_with("hc-"));
309 assert!(short.len() <= 15);
310 }
311
312 #[test]
313 fn test_state_id_roundtrip() {
314 let id = StateId::from_content_hash(ContentHash::compute(b"state"));
315 let encoded = id.to_string_full();
316
317 assert!(encoded.starts_with("hs-"));
318 assert_eq!(StateId::parse(&encoded).unwrap(), id);
319 assert_eq!(StateId::try_from_slice(id.as_bytes()).unwrap(), id);
320 }
321}