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 "hd-{}",
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("hd-").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
161impl fmt::Debug for ChangeId {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 write!(f, "ChangeId({})", self.short())
164 }
165}
166
167impl fmt::Display for ChangeId {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 write!(f, "{}", self.short())
170 }
171}
172
173#[derive(Debug, Clone, thiserror::Error)]
175pub enum ChangeIdParseError {
176 #[error("invalid base32 encoding")]
177 InvalidBase32,
178 #[error("invalid length (expected 16 bytes)")]
179 InvalidLength,
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_content_hash_compute() {
188 let hash = ContentHash::compute(b"hello world");
189 assert_eq!(hash.to_hex().len(), 64);
190
191 let hash2 = ContentHash::compute(b"hello world");
192 assert_eq!(hash, hash2);
193
194 let hash3 = ContentHash::compute(b"hello world!");
195 assert_ne!(hash, hash3);
196 }
197
198 #[test]
199 fn test_content_hash_typed() {
200 let hash1 = ContentHash::compute_typed("blob", b"hello");
201 let hash2 = ContentHash::compute_typed("tree", b"hello");
202 assert_ne!(hash1, hash2);
203 }
204
205 #[test]
206 fn test_content_hash_hex_roundtrip() {
207 let hash = ContentHash::compute(b"test");
208 let hex = hash.to_hex();
209 let parsed = ContentHash::from_hex(&hex).unwrap();
210 assert_eq!(hash, parsed);
211 }
212
213 #[test]
214 fn test_change_id_generate() {
215 let id1 = ChangeId::generate();
216 let id2 = ChangeId::generate();
217 assert_ne!(id1, id2);
218 assert!(!id1.is_zero());
219 }
220
221 #[test]
222 fn test_change_id_roundtrip() {
223 let id = ChangeId::generate();
224 let s = id.to_string_full();
225 assert!(s.starts_with("hd-"));
226 let parsed = ChangeId::parse(&s).unwrap();
227 assert_eq!(id, parsed);
228 }
229
230 #[test]
231 fn test_change_id_short() {
232 let id = ChangeId::generate();
233 let short = id.short();
234 assert!(short.starts_with("hd-"));
235 assert!(short.len() <= 15);
236 }
237}