1use crate::cid::{Cid, CidError};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use sha2::{Digest, Sha256};
4use std::convert::TryFrom;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7use thiserror::Error;
8
9const HASH_LENGTH: usize = 32;
10
11#[derive(Error, Debug)]
12pub enum ItemHashError {
13 #[error("Could not determine hash type: '{0}'")]
14 UnknownHashType(String),
15 #[error("Invalid IPFS CID: '{0}'")]
16 InvalidCid(#[from] CidError),
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum ItemHash {
22 Native(AlephItemHash),
23 Ipfs(Cid),
24}
25
26impl From<AlephItemHash> for ItemHash {
27 fn from(value: AlephItemHash) -> Self {
28 Self::Native(value)
29 }
30}
31
32impl From<[u8; HASH_LENGTH]> for ItemHash {
33 fn from(value: [u8; HASH_LENGTH]) -> Self {
34 Self::Native(AlephItemHash::new(value))
35 }
36}
37
38impl From<Cid> for ItemHash {
39 fn from(value: Cid) -> Self {
40 Self::Ipfs(value)
41 }
42}
43
44impl TryFrom<&str> for ItemHash {
45 type Error = ItemHashError;
46
47 fn try_from(value: &str) -> Result<Self, Self::Error> {
48 if let Ok(native_hash) = AlephItemHash::try_from(value) {
49 return Ok(Self::Native(native_hash));
50 }
51 if let Ok(cid) = Cid::try_from(value) {
52 return Ok(Self::Ipfs(cid));
53 }
54
55 Err(ItemHashError::UnknownHashType(value.to_string()))
56 }
57}
58
59impl FromStr for ItemHash {
60 type Err = ItemHashError;
61
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 Self::try_from(s)
64 }
65}
66
67impl Display for ItemHash {
68 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69 match self {
70 ItemHash::Native(hash) => write!(f, "{}", hash),
71 ItemHash::Ipfs(cid) => write!(f, "{}", cid),
72 }
73 }
74}
75
76#[macro_export]
88macro_rules! item_hash {
89 ($hash:expr) => {{ $crate::item_hash::ItemHash::try_from($hash).expect(concat!("Invalid ItemHash: ", $hash)) }};
90}
91
92#[derive(Clone, Copy, PartialEq, Eq, Hash)]
93pub struct AlephItemHash {
94 bytes: [u8; HASH_LENGTH],
95}
96
97impl AlephItemHash {
98 pub fn new(bytes: [u8; HASH_LENGTH]) -> Self {
99 Self { bytes }
100 }
101
102 pub fn from_bytes(bytes: &[u8]) -> Self {
103 let mut hasher = Sha256::new();
104 hasher.update(bytes);
105 let result = hasher.finalize();
106 let mut hash_bytes = [0u8; HASH_LENGTH];
107 hash_bytes.copy_from_slice(&result);
108 Self { bytes: hash_bytes }
109 }
110
111 pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] {
112 &self.bytes
113 }
114}
115
116#[derive(Error, Debug)]
117pub enum AlephItemHashError {
118 #[error("{0}: invalid hash length, expected 64 hex characters")]
119 InvalidLength(String),
120 #[error("invalid hex digit in hash string: {0}")]
121 InvalidHexDigit(String),
122}
123
124impl TryFrom<&str> for AlephItemHash {
125 type Error = AlephItemHashError;
126
127 fn try_from(hex: &str) -> Result<Self, Self::Error> {
128 if hex.len() != 2 * HASH_LENGTH {
129 return Err(AlephItemHashError::InvalidLength(hex.to_string()));
130 }
131 let mut bytes = [0u8; HASH_LENGTH];
132 for i in 0..HASH_LENGTH {
133 bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
134 .map_err(|_| AlephItemHashError::InvalidHexDigit(hex.to_string()))?;
135 }
136 Ok(Self { bytes })
137 }
138}
139
140impl FromStr for AlephItemHash {
141 type Err = AlephItemHashError; fn from_str(s: &str) -> Result<Self, Self::Err> {
144 AlephItemHash::try_from(s)
145 }
146}
147
148impl Display for AlephItemHash {
149 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150 for byte in self.bytes.iter() {
151 write!(f, "{:02x}", byte)?;
152 }
153 Ok(())
154 }
155}
156
157impl std::fmt::Debug for AlephItemHash {
159 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
160 std::fmt::Display::fmt(self, f)
161 }
162}
163
164impl Serialize for AlephItemHash {
165 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 serializer.serialize_str(&self.to_string())
170 }
171}
172
173impl<'de> Deserialize<'de> for AlephItemHash {
174 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175 where
176 D: Deserializer<'de>,
177 {
178 let s = String::deserialize(deserializer)?;
179 Self::try_from(s.as_str()).map_err(serde::de::Error::custom)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_new_aleph_item_hash() {
189 let bytes = [0u8; HASH_LENGTH];
190 let hash = AlephItemHash::new(bytes);
191 assert_eq!(hash.as_bytes(), &bytes);
192 }
193
194 #[test]
195 fn test_aleph_item_hash_from_bytes() {
196 let input = b"test data";
197 let hash = AlephItemHash::from_bytes(input);
198 assert_eq!(hash.as_bytes().len(), HASH_LENGTH);
199 }
200
201 #[test]
202 fn test_try_from_valid_hex() {
203 let hex = "3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878";
204 let hash = ItemHash::try_from(hex).unwrap();
205 assert_eq!(format!("{}", hash), hex);
206 }
207
208 #[test]
209 fn test_try_from_invalid() {
210 assert!(ItemHash::try_from("000").is_err());
212 assert!(
214 ItemHash::try_from("00000000000000000000000000000000000000000000000000000000000000zz")
215 .is_err()
216 );
217 }
218
219 #[test]
220 fn test_display() {
221 let bytes = [0xab; HASH_LENGTH];
222 let hash = ItemHash::from(bytes);
223 assert_eq!(
224 format!("{}", hash),
225 "abababababababababababababababababababababababababababababababab"
226 );
227 }
228
229 #[test]
230 fn test_convert_back_to_string() {
231 let item_hash_str = "3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878";
232 let item_hash =
233 ItemHash::try_from(item_hash_str).expect("failed to decode a valid item hash");
234 let converted_item_hash_str = item_hash.to_string();
235
236 assert_eq!(item_hash_str, converted_item_hash_str);
237 }
238
239 #[test]
240 fn test_serde() {
241 let item_hash_str = "8eb3e437b5d626da009dc6202617dbdd183ed073b6cad37c64b039b8d5127e2f";
242 let item_hash = ItemHash::try_from(item_hash_str).unwrap();
243
244 let json_item_hash = format!("\"{item_hash_str}\"");
245
246 let deserialized_item_hash: ItemHash = serde_json::from_str(&json_item_hash).unwrap();
247 assert_eq!(item_hash, deserialized_item_hash);
248
249 let serialized_item_hash = serde_json::to_string(&deserialized_item_hash).unwrap();
250 assert_eq!(json_item_hash, serialized_item_hash);
251 }
252}