Skip to main content

neleus_db/
hash.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use anyhow::{Result, anyhow};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub struct Hash(pub [u8; 32]);
9
10impl Hash {
11    pub fn zero() -> Self {
12        Self([0u8; 32])
13    }
14
15    pub fn as_bytes(&self) -> &[u8; 32] {
16        &self.0
17    }
18
19    pub fn from_bytes(bytes: [u8; 32]) -> Self {
20        Self(bytes)
21    }
22}
23
24impl Display for Hash {
25    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26        // Single-pass hex into a stack buffer beats 32 separate `write!`
27        // formatter calls measurably — `path_for` runs on every CAS I/O.
28        const HEX: &[u8; 16] = b"0123456789abcdef";
29        let mut buf = [0u8; 64];
30        for (i, b) in self.0.iter().enumerate() {
31            buf[i * 2] = HEX[(b >> 4) as usize];
32            buf[i * 2 + 1] = HEX[(b & 0x0f) as usize];
33        }
34        // All bytes are ASCII hex digits by construction.
35        f.write_str(std::str::from_utf8(&buf).expect("hex buffer is ASCII"))
36    }
37}
38
39impl FromStr for Hash {
40    type Err = anyhow::Error;
41
42    fn from_str(s: &str) -> Result<Self> {
43        if s.len() != 64 {
44            return Err(anyhow!("expected 64-char hex hash, got {}", s.len()));
45        }
46
47        let mut out = [0u8; 32];
48        for (idx, chunk) in s.as_bytes().chunks(2).enumerate() {
49            let chunk_str = std::str::from_utf8(chunk)?;
50            out[idx] = u8::from_str_radix(chunk_str, 16)
51                .map_err(|e| anyhow!("invalid hex at byte {idx}: {e}"))?;
52        }
53        Ok(Self(out))
54    }
55}
56
57impl Serialize for Hash {
58    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
59    where
60        S: Serializer,
61    {
62        serializer.serialize_str(&self.to_string())
63    }
64}
65
66impl<'de> Deserialize<'de> for Hash {
67    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68    where
69        D: Deserializer<'de>,
70    {
71        let s = String::deserialize(deserializer)?;
72        Hash::from_str(&s).map_err(serde::de::Error::custom)
73    }
74}
75
76pub fn hash_typed(tag: &[u8], bytes: &[u8]) -> Hash {
77    let mut hasher = blake3::Hasher::new();
78    hasher.update(tag);
79    hasher.update(bytes);
80    let out = hasher.finalize();
81    Hash(*out.as_bytes())
82}
83
84pub fn hash_blob(bytes: &[u8]) -> Hash {
85    hash_typed(b"blob:", bytes)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn hash_parse_roundtrip() {
94        let h = hash_blob(b"hello");
95        let parsed = Hash::from_str(&h.to_string()).unwrap();
96        assert_eq!(h, parsed);
97    }
98
99    #[test]
100    fn hash_parse_rejects_invalid_len() {
101        assert!(Hash::from_str("abcd").is_err());
102    }
103
104    #[test]
105    fn hash_parse_rejects_invalid_hex() {
106        let bad = "g".repeat(64);
107        assert!(Hash::from_str(&bad).is_err());
108    }
109
110    #[test]
111    fn hash_blob_is_deterministic() {
112        assert_eq!(hash_blob(b"abc"), hash_blob(b"abc"));
113    }
114
115    #[test]
116    fn hash_typed_domain_separates() {
117        let a = hash_typed(b"blob:", b"abc");
118        let b = hash_typed(b"commit:", b"abc");
119        assert_ne!(a, b);
120    }
121}