coven_foundation/
object_hash.rs1use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use sha2::{Digest, Sha256};
9
10#[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}