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