Skip to main content

coven_foundation/
object_hash.rs

1//! The 32-byte content address every stored object, commit, and blob fact is
2//! identified by: SHA-256 over exact bytes, rendered as lowercase hex.
3
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use sha2::{Digest, Sha256};
9
10/// A string that is not the lowercase-hex rendering of a 32-byte digest.
11#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12#[error("invalid object hash: {0}")]
13pub struct InvalidObjectHash(pub String);
14
15#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct ObjectHash([u8; 32]);
17
18impl ObjectHash {
19    const ENCODED_LEN: usize = 64;
20
21    pub fn digest(bytes: &[u8]) -> Self {
22        Self(Sha256::digest(bytes).into())
23    }
24
25    pub fn from_digest(bytes: [u8; 32]) -> Self {
26        Self(bytes)
27    }
28
29    pub fn as_bytes(&self) -> &[u8; 32] {
30        &self.0
31    }
32
33    fn encoded(self) -> [u8; Self::ENCODED_LEN] {
34        const DIGITS: &[u8; 16] = b"0123456789abcdef";
35
36        let mut encoded = [0; Self::ENCODED_LEN];
37        for (index, byte) in self.0.into_iter().enumerate() {
38            encoded[index * 2] = DIGITS[usize::from(byte >> 4)];
39            encoded[index * 2 + 1] = DIGITS[usize::from(byte & 0x0f)];
40        }
41        encoded
42    }
43
44    fn decode_nibble(byte: u8) -> Option<u8> {
45        match byte {
46            b'0'..=b'9' => Some(byte - b'0'),
47            b'a'..=b'f' => Some(byte - b'a' + 10),
48            _ => None,
49        }
50    }
51}
52
53impl fmt::Debug for ObjectHash {
54    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55        fmt::Display::fmt(self, formatter)
56    }
57}
58
59impl fmt::Display for ObjectHash {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let encoded = self.encoded();
62        formatter.write_str(
63            std::str::from_utf8(&encoded).expect("ObjectHash lowercase hex must be UTF-8"),
64        )
65    }
66}
67
68impl FromStr for ObjectHash {
69    type Err = InvalidObjectHash;
70
71    fn from_str(value: &str) -> Result<Self, Self::Err> {
72        if value.len() != Self::ENCODED_LEN {
73            return Err(InvalidObjectHash(value.to_string()));
74        }
75        let mut bytes = [0_u8; 32];
76        for (output, pair) in bytes.iter_mut().zip(value.as_bytes().chunks_exact(2)) {
77            let Some(high) = Self::decode_nibble(pair[0]) else {
78                return Err(InvalidObjectHash(value.to_string()));
79            };
80            let Some(low) = Self::decode_nibble(pair[1]) else {
81                return Err(InvalidObjectHash(value.to_string()));
82            };
83            *output = (high << 4) | low;
84        }
85        Ok(Self(bytes))
86    }
87}
88
89impl Serialize for ObjectHash {
90    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
91    where
92        S: Serializer,
93    {
94        let encoded = self.encoded();
95        serializer.serialize_str(
96            std::str::from_utf8(&encoded).expect("ObjectHash lowercase hex must be UTF-8"),
97        )
98    }
99}
100
101struct ObjectHashVisitor;
102
103impl<'de> serde::de::Visitor<'de> for ObjectHashVisitor {
104    type Value = ObjectHash;
105
106    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107        formatter.write_str("a 64-character lowercase hexadecimal SHA-256 digest")
108    }
109
110    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
111    where
112        E: serde::de::Error,
113    {
114        value.parse().map_err(E::custom)
115    }
116}
117
118impl<'de> Deserialize<'de> for ObjectHash {
119    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120    where
121        D: Deserializer<'de>,
122    {
123        deserializer.deserialize_str(ObjectHashVisitor)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde::de::{Error as _, Visitor};
131
132    struct BorrowedHash<'a>(&'a str);
133
134    impl<'de> Deserializer<'de> for BorrowedHash<'de> {
135        type Error = serde::de::value::Error;
136
137        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
138        where
139            V: Visitor<'de>,
140        {
141            visitor.visit_borrowed_str(self.0)
142        }
143
144        fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
145        where
146            V: Visitor<'de>,
147        {
148            visitor.visit_borrowed_str(self.0)
149        }
150
151        fn deserialize_string<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
152        where
153            V: Visitor<'de>,
154        {
155            Err(Self::Error::custom(
156                "ObjectHash requested an owned string while deserializing",
157            ))
158        }
159
160        serde::forward_to_deserialize_any! {
161            bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char bytes byte_buf option unit
162            unit_struct newtype_struct seq tuple tuple_struct map struct enum identifier
163            ignored_any
164        }
165    }
166
167    #[test]
168    fn deserialization_accepts_a_borrowed_hash_without_requesting_an_owned_string() {
169        let encoded = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
170        let hash = ObjectHash::deserialize(BorrowedHash(encoded)).expect("deserialize hash");
171        assert_eq!(hash.to_string(), encoded);
172    }
173}