p2panda_rs/hash/
hash.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::convert::TryFrom;
4use std::fmt;
5use std::hash::Hash as StdHash;
6use std::str::FromStr;
7
8use arrayvec::ArrayVec;
9use bamboo_rs_core_ed25519_yasmf::yasmf_hash::new_blake3;
10use serde::{Deserialize, Serialize};
11use serde_bytes::ByteBuf;
12use yasmf_hash::{YasmfHash, BLAKE3_HASH_SIZE, MAX_YAMF_HASH_SIZE};
13
14use crate::hash::error::HashError;
15use crate::hash::HashId;
16use crate::serde::deserialize_hex;
17use crate::{Human, Validate};
18
19/// Size of p2panda entries' hashes.
20pub const HASH_SIZE: usize = BLAKE3_HASH_SIZE;
21
22/// Type used for `bamboo-rs-core-ed25519-yasmf` entries that own their bytes.
23pub type Blake3ArrayVec = ArrayVec<[u8; HASH_SIZE]>;
24
25/// Hash of `Entry` or `Operation` encoded as hex string.
26///
27/// This uses the BLAKE3 algorithm wrapped in [`YASMF`] "Yet-Another-Smol-Multi-Format" according
28/// to the Bamboo specification.
29///
30/// [`YASMF`]: https://github.com/bamboo-rs/yasmf-hash
31#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, StdHash)]
32pub struct Hash(String);
33
34impl Hash {
35    /// Validates and wraps encoded hash string into new `Hash` instance.
36    pub fn new(value: &str) -> Result<Self, HashError> {
37        let hash = Self(String::from(value));
38        hash.validate()?;
39        Ok(hash)
40    }
41
42    /// Hashes byte data and returns it as `Hash` instance.
43    pub fn new_from_bytes(value: &[u8]) -> Self {
44        // Generate Blake3 hash
45        let blake3_hash = new_blake3(value);
46
47        // Wrap hash in YASMF container format
48        let mut bytes = Vec::new();
49        blake3_hash
50            .encode_write(&mut bytes)
51            // Unwrap here as this will only fail on critical system failures
52            .unwrap();
53
54        // Encode bytes as hex string
55        let hex_str = hex::encode(&bytes);
56
57        Self(hex_str)
58    }
59
60    /// Returns hash as bytes.
61    pub fn to_bytes(&self) -> Vec<u8> {
62        // Unwrap as we already validated the hash
63        hex::decode(&self.0).unwrap()
64    }
65
66    /// Returns hash as `&str`.
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70}
71
72impl Validate for Hash {
73    type Error = HashError;
74
75    fn validate(&self) -> Result<(), Self::Error> {
76        // Check if hash is a hex string
77        match hex::decode(&self.0) {
78            Ok(bytes) => {
79                // Check if length is correct
80                if bytes.len() != HASH_SIZE + 2 {
81                    return Err(HashError::InvalidLength(bytes.len(), HASH_SIZE + 2));
82                }
83
84                // Check if YASMF BLAKE3 hash is valid
85                match YasmfHash::<&[u8]>::decode(&bytes) {
86                    Ok((YasmfHash::Blake3(_), _)) => Ok(()),
87                    _ => Err(HashError::DecodingFailed),
88                }
89            }
90            Err(_) => Err(HashError::InvalidHexEncoding),
91        }
92    }
93}
94
95impl fmt::Display for Hash {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "{}", self.as_str())
98    }
99}
100
101impl Human for Hash {
102    /// Return a shortened six character representation.
103    ///
104    /// ## Example
105    ///
106    /// ```
107    /// # use p2panda_rs::hash::Hash;
108    /// # use p2panda_rs::Human;
109    /// let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
110    /// let hash: Hash = hash_str.parse().unwrap();
111    /// assert_eq!(hash.display(), "<Hash 6ec805>");
112    /// ```
113    fn display(&self) -> String {
114        let offset = MAX_YAMF_HASH_SIZE * 2 - 6;
115        format!("<Hash {}>", &self.as_str()[offset..])
116    }
117}
118
119impl Serialize for Hash {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: serde::Serializer,
123    {
124        if serializer.is_human_readable() {
125            self.as_str().serialize(serializer)
126        } else {
127            ByteBuf::from(self.to_bytes()).serialize(serializer)
128        }
129    }
130}
131
132impl<'de> Deserialize<'de> for Hash {
133    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134    where
135        D: serde::Deserializer<'de>,
136    {
137        // Deserialize hash bytes.
138        let hash_bytes = deserialize_hex(deserializer)?;
139
140        // Convert and validate format
141        let hash_str = hex::encode(hash_bytes);
142        Hash::new(&hash_str).map_err(|err: HashError| serde::de::Error::custom(err.to_string()))
143    }
144}
145
146/// Converts YASMF hash from `yasmf-hash` crate to p2panda `Hash` instance.
147impl<T: core::borrow::Borrow<[u8]> + Clone> From<&YasmfHash<T>> for Hash {
148    fn from(yasmf_hash: &YasmfHash<T>) -> Self {
149        let mut out = [0u8; MAX_YAMF_HASH_SIZE];
150        // Unwrap here as this will only fail on a critical system error
151        let _ = yasmf_hash.encode(&mut out).unwrap();
152        // Unwrap because we know it is a valid hex string
153        Self::new(&hex::encode(out)).unwrap()
154    }
155}
156
157/// Returns Yet-Another-Smol-Multiformat Hash struct from the `yasmf-hash` crate.
158///
159/// This comes in handy when interacting with the `bamboo-rs` crate.
160impl From<&Hash> for YasmfHash<Blake3ArrayVec> {
161    fn from(hash: &Hash) -> YasmfHash<Blake3ArrayVec> {
162        let bytes = hash.to_bytes();
163        let yasmf_hash = YasmfHash::<Blake3ArrayVec>::decode_owned(&bytes).unwrap();
164        yasmf_hash.0
165    }
166}
167
168/// Convert any hex-encoded string representation of a hash into a `Hash` instance.
169impl TryFrom<&str> for Hash {
170    type Error = HashError;
171
172    fn try_from(str: &str) -> Result<Self, Self::Error> {
173        Self::new(str)
174    }
175}
176
177/// Convert any borrowed string representation into a `Hash` instance.
178impl FromStr for Hash {
179    type Err = HashError;
180
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        Self::new(s)
183    }
184}
185
186/// Convert any owned string representation into a `Hash` instance.
187impl TryFrom<String> for Hash {
188    type Error = HashError;
189
190    fn try_from(str: String) -> Result<Self, Self::Error> {
191        Self::new(&str)
192    }
193}
194
195impl<T: HashId> From<T> for Hash {
196    fn from(hash_id: T) -> Hash {
197        hash_id.as_hash().to_owned()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::collections::HashMap;
204    use std::convert::{TryFrom, TryInto};
205
206    use ciborium::cbor;
207    use serde_bytes::ByteBuf;
208    use yasmf_hash::YasmfHash;
209
210    use crate::serde::{deserialize_into, serialize_from, serialize_value};
211    use crate::Human;
212
213    use super::{Blake3ArrayVec, Hash};
214
215    #[test]
216    fn validate() {
217        assert!(Hash::new("abcdefg").is_err());
218        assert!(Hash::new("112233445566ff").is_err());
219        assert!(
220            Hash::new("01234567812345678123456781234567812345678123456781234567812345678").is_err()
221        );
222        assert!(
223            Hash::new("0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543")
224                .is_ok()
225        );
226    }
227
228    #[test]
229    fn new_from_bytes() {
230        assert_eq!(
231            Hash::new_from_bytes(&[1, 2, 3]),
232            Hash::new("0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543")
233                .unwrap()
234        );
235    }
236
237    #[test]
238    fn convert_yasmf() {
239        let hash = Hash::new_from_bytes(&[1, 2, 3]);
240        let yasmf_hash = Into::<YasmfHash<Blake3ArrayVec>>::into(&hash);
241        let hash_restored = Into::<Hash>::into(&yasmf_hash);
242        assert_eq!(hash, hash_restored);
243    }
244
245    #[test]
246    fn it_hashes() {
247        let hash = Hash::new_from_bytes(&[1, 2, 3]);
248        let mut hash_map = HashMap::new();
249        let key_value = "Value identified by a hash".to_string();
250        hash_map.insert(&hash, key_value.clone());
251        let key_value_retrieved = hash_map.get(&hash).unwrap().to_owned();
252        assert_eq!(key_value, key_value_retrieved)
253    }
254
255    #[test]
256    fn from_string() {
257        let hash_str = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543";
258
259        // Using TryFrom<&str>
260        let hash_from_str: Hash = hash_str.try_into().unwrap();
261        assert_eq!(hash_str, hash_from_str.as_str());
262
263        // Using FromStr
264        let hash_from_parse: Hash = hash_str.parse().unwrap();
265        assert_eq!(hash_str, hash_from_parse.as_str());
266
267        // Using TryFrom<String>
268        let hash_from_string = Hash::try_from(String::from(hash_str)).unwrap();
269        assert_eq!(hash_str, hash_from_string.as_str());
270    }
271
272    #[test]
273    fn string_representation() {
274        let hash_str = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543";
275        let hash = Hash::new(hash_str).unwrap();
276
277        assert_eq!(hash_str, hash.as_str());
278        assert_eq!(hash_str, hash.to_string());
279        assert_eq!(hash_str, format!("{}", hash));
280    }
281
282    #[test]
283    fn short_representation() {
284        let hash_str = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543";
285        let hash = Hash::new(hash_str).unwrap();
286
287        assert_eq!(hash.display(), "<Hash 496543>");
288    }
289
290    #[test]
291    fn serialize() {
292        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
293        let hash = Hash::new(hash_str).unwrap();
294        let bytes = serialize_from(hash.clone());
295        assert_eq!(
296            bytes,
297            vec![
298                88, 34, 0, 32, 207, 176, 250, 55, 243, 109, 8, 47, 170, 211, 136, 106, 159, 251,
299                204, 40, 19, 183, 175, 233, 15, 6, 9, 165, 86, 212, 37, 241, 167, 110, 200, 5
300            ]
301        );
302
303        // The `cbor` macro serializes to human readable formats, in this case, hex encoded bytes.
304        let human_readable_cbor = cbor!(hash).unwrap();
305        assert!(human_readable_cbor.is_text());
306        assert_eq!(human_readable_cbor.as_text().unwrap(), hash_str)
307    }
308
309    #[test]
310    fn deserialize() {
311        // Deserialize from non human-readable CBOR bytes
312        let hash_bytes = [
313            0, 32, 207, 176, 250, 55, 243, 109, 8, 47, 170, 211, 136, 106, 159, 251, 204, 40, 19,
314            183, 175, 233, 15, 6, 9, 165, 86, 212, 37, 241, 167, 110, 200, 5,
315        ];
316        let hash: Hash =
317            deserialize_into(&serialize_value(cbor!(ByteBuf::from(hash_bytes)))).unwrap();
318        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
319        assert_eq!(Hash::new(hash_str).unwrap(), hash);
320
321        // Invalid hashes
322        let invalid_hash = deserialize_into::<Hash>(&serialize_value(cbor!("1234")));
323        assert!(invalid_hash.is_err());
324        let invalid_hash = deserialize_into::<Hash>(&serialize_value(cbor!("xyz".as_bytes())));
325        assert!(invalid_hash.is_err(), "{:#?}", invalid_hash);
326    }
327
328    #[test]
329    fn deserialize_human_readable() {
330        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
331
332        #[derive(serde::Deserialize, Debug, PartialEq)]
333        struct Test {
334            hash: Hash,
335        }
336
337        // Deserialize from human-readable (hex-encoded) JSON string
338        let json = format!(
339            r#"
340            {{
341                "hash": "{hash_str}"
342            }}
343        "#
344        );
345
346        let result: Test = serde_json::from_str(&json).unwrap();
347        assert_eq!(
348            Test {
349                hash: Hash::new(hash_str).unwrap()
350            },
351            result
352        );
353    }
354}