aleph_types/
item_hash.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use sha2::{Digest, Sha256};
3use std::convert::TryFrom;
4use std::fmt::{Display, Formatter};
5use std::str::FromStr;
6use thiserror::Error;
7
8const HASH_LENGTH: usize = 32;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ItemHash {
12    bytes: [u8; HASH_LENGTH],
13}
14
15impl ItemHash {
16    pub fn new(bytes: [u8; HASH_LENGTH]) -> Self {
17        Self { bytes }
18    }
19
20    pub fn from_bytes(bytes: &[u8]) -> Self {
21        let mut hasher = Sha256::new();
22        hasher.update(bytes);
23        let result = hasher.finalize();
24        let mut hash_bytes = [0u8; HASH_LENGTH];
25        hash_bytes.copy_from_slice(&result);
26        Self { bytes: hash_bytes }
27    }
28
29    pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] {
30        &self.bytes
31    }
32}
33
34#[derive(Error, Debug)]
35pub enum ItemHashError {
36    #[error("invalid hash length, expected 64 hex characters")]
37    InvalidLength,
38    #[error("invalid hex digit in hash string")]
39    InvalidHexDigit,
40}
41
42impl TryFrom<&str> for ItemHash {
43    type Error = ItemHashError;
44
45    fn try_from(hex: &str) -> Result<Self, Self::Error> {
46        if hex.len() != 2 * HASH_LENGTH {
47            return Err(ItemHashError::InvalidLength);
48        }
49        let mut bytes = [0u8; HASH_LENGTH];
50        for i in 0..HASH_LENGTH {
51            bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
52                .map_err(|_| ItemHashError::InvalidHexDigit)?;
53        }
54        Ok(Self { bytes })
55    }
56}
57
58impl FromStr for ItemHash {
59    type Err = ItemHashError; // whatever TryFrom<String> returns
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        ItemHash::try_from(s)
63    }
64}
65
66impl Display for ItemHash {
67    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68        for byte in self.bytes.iter() {
69            write!(f, "{:02x}", byte)?;
70        }
71        Ok(())
72    }
73}
74
75impl Serialize for ItemHash {
76    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
77    where
78        S: Serializer,
79    {
80        serializer.serialize_str(&self.to_string())
81    }
82}
83
84impl<'de> Deserialize<'de> for ItemHash {
85    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86    where
87        D: Deserializer<'de>,
88    {
89        let s = String::deserialize(deserializer)?;
90        ItemHash::try_from(s.as_str()).map_err(serde::de::Error::custom)
91    }
92}
93
94/// Macro for creating ItemHash instances from hex string literals.
95///
96/// This macro simplifies creating ItemHash instances in tests and other code
97/// by panicking on invalid input (similar to `vec!` or `format!`).
98///
99/// # Example
100///
101/// ```
102/// use aleph_types::item_hash;
103/// let hash = item_hash!("3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878");
104/// ```
105#[macro_export]
106macro_rules! item_hash {
107    ($hex:expr) => {{ $crate::item_hash::ItemHash::try_from($hex).expect(concat!("Invalid ItemHash: ", $hex)) }};
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_new() {
116        let bytes = [0u8; HASH_LENGTH];
117        let hash = ItemHash::new(bytes);
118        assert_eq!(hash.as_bytes(), &bytes);
119    }
120
121    #[test]
122    fn test_from_bytes() {
123        let input = b"test data";
124        let hash = ItemHash::from_bytes(input);
125        assert_eq!(hash.as_bytes().len(), HASH_LENGTH);
126    }
127
128    #[test]
129    fn test_try_from_valid() {
130        let hex = "3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878";
131        let hash = ItemHash::try_from(hex).unwrap();
132        assert_eq!(format!("{}", hash), hex);
133    }
134
135    #[test]
136    fn test_try_from_invalid() {
137        // Test invalid length
138        assert!(ItemHash::try_from("000").is_err());
139        // Test invalid hex digits
140        assert!(
141            ItemHash::try_from("00000000000000000000000000000000000000000000000000000000000000zz")
142                .is_err()
143        );
144    }
145
146    #[test]
147    fn test_display() {
148        let bytes = [0xab; HASH_LENGTH];
149        let hash = ItemHash::new(bytes);
150        assert_eq!(
151            format!("{}", hash),
152            "abababababababababababababababababababababababababababababababab"
153        );
154    }
155
156    #[test]
157    fn test_convert_back_to_string() {
158        let item_hash_str = "3c5b05761c8f94a7b8fe6d0d43e5fb91f9689c53c078a870e5e300c7da8a1878";
159        let item_hash =
160            ItemHash::try_from(item_hash_str).expect("failed to decode a valid item hash");
161        let converted_item_hash_str = item_hash.to_string();
162
163        assert_eq!(item_hash_str, converted_item_hash_str);
164    }
165
166    #[test]
167    fn test_serde() {
168        let item_hash_str = "8eb3e437b5d626da009dc6202617dbdd183ed073b6cad37c64b039b8d5127e2f";
169        let item_hash = ItemHash::try_from(item_hash_str).unwrap();
170
171        let json_item_hash = format!("\"{item_hash_str}\"");
172
173        let deserialized_item_hash: ItemHash = serde_json::from_str(&json_item_hash).unwrap();
174        assert_eq!(item_hash, deserialized_item_hash);
175
176        let serialized_item_hash = serde_json::to_string(&deserialized_item_hash).unwrap();
177        assert_eq!(json_item_hash, serialized_item_hash);
178    }
179}