Skip to main content

aleph_types/
item_hash.rs

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