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; 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_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 assert!(ItemHash::try_from("000").is_err());
139 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}