Skip to main content

aleph_types/
item_hash.rs

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 for creating ItemHash instances from hex string literals.
75///
76/// This macro simplifies creating ItemHash instances in tests and other code
77/// by panicking on invalid input (similar to `vec!` or `format!`).
78///
79/// # Example
80///
81/// ```
82/// use aleph_types::item_hash;
83/// let hash = item_hash!("3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878");
84/// ```
85#[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; // whatever TryFrom<String> returns
140
141    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
155// Use the Display implementation for debug formatting
156impl 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        // Test invalid length
209        assert!(ItemHash::try_from("000").is_err());
210        // Test invalid hex digits
211        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}