Skip to main content

zub/
hash.rs

1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::fmt;
4
5use crate::types::Xattr;
6use crate::Error;
7
8/// SHA-256 hash used for content addressing
9#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct Hash([u8; 32]);
11
12impl Hash {
13    /// zero hash (useful as sentinel)
14    pub const ZERO: Hash = Hash([0u8; 32]);
15
16    /// create from raw bytes
17    pub fn from_bytes(bytes: [u8; 32]) -> Self {
18        Self(bytes)
19    }
20
21    /// parse from hex string
22    pub fn from_hex(s: &str) -> crate::Result<Self> {
23        let bytes = hex::decode(s).map_err(|_| Error::InvalidHashHex(s.to_string()))?;
24        if bytes.len() != 32 {
25            return Err(Error::InvalidHashHex(s.to_string()));
26        }
27        let mut arr = [0u8; 32];
28        arr.copy_from_slice(&bytes);
29        Ok(Self(arr))
30    }
31
32    /// get raw bytes
33    pub fn as_bytes(&self) -> &[u8; 32] {
34        &self.0
35    }
36
37    /// convert to hex string
38    pub fn to_hex(&self) -> String {
39        hex::encode(self.0)
40    }
41
42    /// split into path components for object store
43    /// returns (first 2 hex chars, remaining 62 hex chars)
44    pub fn to_path_components(&self) -> (String, String) {
45        let hex = self.to_hex();
46        (hex[..2].to_string(), hex[2..].to_string())
47    }
48}
49
50impl fmt::Display for Hash {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{}", self.to_hex())
53    }
54}
55
56impl fmt::Debug for Hash {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "Hash({})", &self.to_hex()[..12])
59    }
60}
61
62impl Serialize for Hash {
63    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64    where
65        S: serde::Serializer,
66    {
67        serializer.serialize_str(&self.to_hex())
68    }
69}
70
71impl<'de> Deserialize<'de> for Hash {
72    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73    where
74        D: serde::Deserializer<'de>,
75    {
76        let s = String::deserialize(deserializer)?;
77        Self::from_hex(&s).map_err(serde::de::Error::custom)
78    }
79}
80
81/// symlink mode constant for deterministic hashing
82pub const SYMLINK_MODE: u32 = 0o120777;
83
84/// compute blob hash over (uid, gid, mode, xattrs, content)
85///
86/// uid/gid are INSIDE namespace values (logical), NOT on-disk values.
87/// xattrs are sorted by name for determinism.
88/// format:
89///   uid: 4 bytes LE
90///   gid: 4 bytes LE
91///   mode: 4 bytes LE
92///   xattr_count: 4 bytes LE
93///   for each xattr (sorted by name):
94///     name_len: 4 bytes LE
95///     name: bytes
96///     value_len: 4 bytes LE
97///     value: bytes
98///   content: bytes
99pub fn compute_blob_hash(
100    inside_uid: u32,
101    inside_gid: u32,
102    mode: u32,
103    xattrs: &[Xattr],
104    content: &[u8],
105) -> Hash {
106    let mut hasher = Sha256::new();
107
108    // fixed header
109    hasher.update(&inside_uid.to_le_bytes());
110    hasher.update(&inside_gid.to_le_bytes());
111    hasher.update(&mode.to_le_bytes());
112
113    // xattrs: count + sorted entries
114    let mut sorted: Vec<_> = xattrs.iter().collect();
115    sorted.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
116
117    hasher.update(&(sorted.len() as u32).to_le_bytes());
118    for xattr in sorted {
119        hasher.update(&(xattr.name.len() as u32).to_le_bytes());
120        hasher.update(xattr.name.as_bytes());
121        hasher.update(&(xattr.value.len() as u32).to_le_bytes());
122        hasher.update(&xattr.value);
123    }
124
125    // content
126    hasher.update(content);
127
128    Hash(hasher.finalize().into())
129}
130
131/// compute hash for symlink (target is the "content")
132/// always uses SYMLINK_MODE for determinism
133pub fn compute_symlink_hash(
134    inside_uid: u32,
135    inside_gid: u32,
136    xattrs: &[Xattr],
137    target: &str,
138) -> Hash {
139    compute_blob_hash(
140        inside_uid,
141        inside_gid,
142        SYMLINK_MODE,
143        xattrs,
144        target.as_bytes(),
145    )
146}
147
148/// compute hash of compressed bytes (for trees and commits)
149#[allow(dead_code)]
150pub fn compute_compressed_hash(compressed: &[u8]) -> Hash {
151    let digest = Sha256::digest(compressed);
152    Hash(digest.into())
153}
154
155/// streaming blob hasher for large files
156#[allow(dead_code)]
157pub struct BlobHasher {
158    hasher: Sha256,
159}
160
161impl BlobHasher {
162    /// create new hasher, writing header and xattrs immediately
163    pub fn new(inside_uid: u32, inside_gid: u32, mode: u32, xattrs: &[Xattr]) -> Self {
164        let mut hasher = Sha256::new();
165
166        hasher.update(&inside_uid.to_le_bytes());
167        hasher.update(&inside_gid.to_le_bytes());
168        hasher.update(&mode.to_le_bytes());
169
170        let mut sorted: Vec<_> = xattrs.iter().collect();
171        sorted.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
172
173        hasher.update(&(sorted.len() as u32).to_le_bytes());
174        for xattr in sorted {
175            hasher.update(&(xattr.name.len() as u32).to_le_bytes());
176            hasher.update(xattr.name.as_bytes());
177            hasher.update(&(xattr.value.len() as u32).to_le_bytes());
178            hasher.update(&xattr.value);
179        }
180
181        Self { hasher }
182    }
183
184    /// feed content bytes
185    pub fn update(&mut self, data: &[u8]) {
186        self.hasher.update(data);
187    }
188
189    /// finalize and return hash
190    pub fn finalize(self) -> Hash {
191        Hash(self.hasher.finalize().into())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_hash_hex_roundtrip() {
201        let original =
202            Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
203                .unwrap();
204        let hex = original.to_hex();
205        let parsed = Hash::from_hex(&hex).unwrap();
206        assert_eq!(original, parsed);
207    }
208
209    #[test]
210    fn test_hash_invalid_hex() {
211        assert!(Hash::from_hex("not valid hex").is_err());
212        assert!(Hash::from_hex("abcd").is_err()); // too short
213        assert!(Hash::from_hex(
214            "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ff"
215        )
216        .is_err()); // too long
217    }
218
219    #[test]
220    fn test_hash_path_components() {
221        let h = Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
222            .unwrap();
223        let (dir, file) = h.to_path_components();
224        assert_eq!(dir, "ab");
225        assert_eq!(
226            file,
227            "cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
228        );
229    }
230
231    #[test]
232    fn test_hash_ordering() {
233        let h1 = Hash::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
234            .unwrap();
235        let h2 = Hash::from_hex("0000000000000000000000000000000000000000000000000000000000000002")
236            .unwrap();
237        assert!(h1 < h2);
238    }
239
240    #[test]
241    fn test_blob_hash_determinism() {
242        let h1 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
243        let h2 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
244        assert_eq!(h1, h2);
245    }
246
247    #[test]
248    fn test_blob_hash_different_uid() {
249        let h1 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
250        let h2 = compute_blob_hash(1, 0, 0o644, &[], b"hello");
251        assert_ne!(h1, h2);
252    }
253
254    #[test]
255    fn test_blob_hash_different_gid() {
256        let h1 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
257        let h2 = compute_blob_hash(0, 1, 0o644, &[], b"hello");
258        assert_ne!(h1, h2);
259    }
260
261    #[test]
262    fn test_blob_hash_different_mode() {
263        let h1 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
264        let h2 = compute_blob_hash(0, 0, 0o755, &[], b"hello");
265        assert_ne!(h1, h2);
266    }
267
268    #[test]
269    fn test_blob_hash_different_content() {
270        let h1 = compute_blob_hash(0, 0, 0o644, &[], b"hello");
271        let h2 = compute_blob_hash(0, 0, 0o644, &[], b"world");
272        assert_ne!(h1, h2);
273    }
274
275    #[test]
276    fn test_blob_hash_different_xattr() {
277        let x1 = vec![Xattr::new("user.test", vec![1, 2, 3])];
278        let x2 = vec![Xattr::new("user.test", vec![4, 5, 6])];
279
280        let h1 = compute_blob_hash(0, 0, 0o644, &x1, b"hello");
281        let h2 = compute_blob_hash(0, 0, 0o644, &x2, b"hello");
282        assert_ne!(h1, h2);
283    }
284
285    #[test]
286    fn test_blob_hash_xattr_ordering() {
287        // different order should produce same hash
288        let x1 = vec![Xattr::new("user.a", vec![1]), Xattr::new("user.b", vec![2])];
289        let x2 = vec![Xattr::new("user.b", vec![2]), Xattr::new("user.a", vec![1])];
290
291        let h1 = compute_blob_hash(0, 0, 0o644, &x1, b"hello");
292        let h2 = compute_blob_hash(0, 0, 0o644, &x2, b"hello");
293        assert_eq!(h1, h2);
294    }
295
296    #[test]
297    fn test_blob_hash_empty_content() {
298        let h = compute_blob_hash(0, 0, 0o644, &[], b"");
299        assert_ne!(h, Hash::ZERO);
300    }
301
302    #[test]
303    fn test_blob_hash_empty_xattr_value() {
304        let x = vec![Xattr::new("user.empty", vec![])];
305        let h = compute_blob_hash(0, 0, 0o644, &x, b"hello");
306        assert_ne!(h, Hash::ZERO);
307    }
308
309    #[test]
310    fn test_symlink_hash() {
311        let h1 = compute_symlink_hash(0, 0, &[], "/target/path");
312        let h2 = compute_symlink_hash(0, 0, &[], "/target/path");
313        assert_eq!(h1, h2);
314
315        // different target = different hash
316        let h3 = compute_symlink_hash(0, 0, &[], "/other/path");
317        assert_ne!(h1, h3);
318    }
319
320    #[test]
321    fn test_streaming_hasher() {
322        let direct = compute_blob_hash(0, 0, 0o644, &[], b"helloworld");
323
324        let mut streaming = BlobHasher::new(0, 0, 0o644, &[]);
325        streaming.update(b"hello");
326        streaming.update(b"world");
327        let streamed = streaming.finalize();
328
329        assert_eq!(direct, streamed);
330    }
331
332    #[test]
333    fn test_hash_serde_json() {
334        let h = Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
335            .unwrap();
336        let json = serde_json::to_string(&h).unwrap();
337        assert!(json.contains("abcdef"));
338        let parsed: Hash = serde_json::from_str(&json).unwrap();
339        assert_eq!(h, parsed);
340    }
341}