p2panda_core/
hash.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! BLAKE3 hashes over arbitrary bytes.
4//!
5//! ## Example
6//!
7//! ```
8//! use p2panda_core::Hash;
9//!
10//! let bytes: &[u8] = b"A very important message.";
11//! let hash = Hash::new(bytes);
12//!
13//! assert_eq!(
14//!     "8d3ca6d66651182cd6a9c1fc5dad0260a0ee29fe9ed494734e60d259430ae8a4",
15//!     hash.to_hex()
16//! )
17//! ```
18use std::fmt;
19use std::str::FromStr;
20
21#[cfg(feature = "arbitrary")]
22use arbitrary::Arbitrary;
23use thiserror::Error;
24
25/// The length of a BLAKE3 hash in bytes.
26pub const HASH_LEN: usize = blake3::KEY_LEN;
27
28/// 32-byte BLAKE3 hash.
29#[derive(Clone, Copy, PartialEq, Eq, Hash)]
30pub struct Hash(blake3::Hash);
31
32impl Hash {
33    /// Calculate the hash of the provided bytes.
34    pub fn new(buf: impl AsRef<[u8]>) -> Self {
35        Self(blake3::hash(buf.as_ref()))
36    }
37
38    /// Create a `Hash` from its raw bytes representation.
39    pub const fn from_bytes(bytes: [u8; HASH_LEN]) -> Self {
40        Self(blake3::Hash::from_bytes(bytes))
41    }
42
43    /// Bytes of the hash.
44    pub fn as_bytes(&self) -> &[u8; HASH_LEN] {
45        self.0.as_bytes()
46    }
47
48    /// Convert the hash to a hex string.
49    pub fn to_hex(&self) -> String {
50        self.0.to_hex().to_string()
51    }
52}
53
54impl AsRef<[u8]> for Hash {
55    fn as_ref(&self) -> &[u8] {
56        self.0.as_bytes()
57    }
58}
59
60impl From<Hash> for blake3::Hash {
61    fn from(value: Hash) -> Self {
62        value.0
63    }
64}
65
66impl From<blake3::Hash> for Hash {
67    fn from(value: blake3::Hash) -> Self {
68        Self(value)
69    }
70}
71
72impl From<[u8; HASH_LEN]> for Hash {
73    fn from(value: [u8; HASH_LEN]) -> Self {
74        Self(blake3::Hash::from(value))
75    }
76}
77
78impl From<Hash> for [u8; HASH_LEN] {
79    fn from(value: Hash) -> Self {
80        *value.as_bytes()
81    }
82}
83
84impl From<&[u8; HASH_LEN]> for Hash {
85    fn from(value: &[u8; HASH_LEN]) -> Self {
86        Self(blake3::Hash::from(*value))
87    }
88}
89
90impl TryFrom<&[u8]> for Hash {
91    type Error = HashError;
92
93    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
94        let value_len = value.len();
95
96        let checked_value: [u8; HASH_LEN] = value
97            .try_into()
98            .map_err(|_| HashError::InvalidLength(value_len, HASH_LEN))?;
99
100        Ok(Self(blake3::Hash::from(checked_value)))
101    }
102}
103
104impl From<&Hash> for [u8; 32] {
105    fn from(value: &Hash) -> Self {
106        *value.as_bytes()
107    }
108}
109
110impl FromStr for Hash {
111    type Err = HashError;
112
113    fn from_str(value: &str) -> Result<Self, Self::Err> {
114        Self::try_from(hex::decode(value)?.as_slice())
115    }
116}
117
118impl PartialOrd for Hash {
119    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
120        Some(self.0.as_bytes().cmp(other.0.as_bytes()))
121    }
122}
123
124impl Ord for Hash {
125    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
126        self.0.as_bytes().cmp(other.0.as_bytes())
127    }
128}
129
130impl fmt::Display for Hash {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{}", self.to_hex())
133    }
134}
135
136impl fmt::Debug for Hash {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.debug_tuple("Hash").field(self.0.as_bytes()).finish()
139    }
140}
141
142#[cfg(feature = "arbitrary")]
143impl<'a> Arbitrary<'a> for Hash {
144    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
145        let bytes = <[u8; HASH_LEN] as Arbitrary>::arbitrary(u)?;
146        Ok(Hash::from_bytes(bytes))
147    }
148}
149
150/// Error types for `Hash` struct.
151#[derive(Error, Debug)]
152pub enum HashError {
153    /// Hash string has an invalid length.
154    #[error("invalid hash length {0} bytes, expected {1} bytes")]
155    InvalidLength(usize, usize),
156
157    /// Hash string contains invalid hexadecimal characters.
158    #[error("invalid hex encoding in hash string")]
159    InvalidHexEncoding(#[from] hex::FromHexError),
160}
161
162#[cfg(test)]
163mod tests {
164    use super::{Hash, HashError};
165
166    #[test]
167    fn hashing() {
168        let hash = Hash::new([1, 2, 3]);
169
170        assert_eq!(
171            hash.as_bytes(),
172            &[
173                177, 119, 236, 27, 242, 109, 251, 59, 112, 16, 212, 115, 230, 212, 71, 19, 178,
174                155, 118, 91, 153, 198, 230, 14, 203, 250, 231, 66, 222, 73, 101, 67
175            ]
176        );
177    }
178
179    #[test]
180    fn invalid_length() {
181        let bytes = vec![254, 100, 4, 7];
182        let result: Result<Hash, HashError> = bytes.as_slice().try_into();
183        matches!(result, Err(HashError::InvalidLength(4, 32)));
184    }
185
186    #[test]
187    fn invalid_hex_encoding() {
188        let hex = "notreallyahexstring";
189        let result: Result<Hash, HashError> = hex.parse();
190        matches!(
191            result,
192            Err(HashError::InvalidHexEncoding(
193                hex::FromHexError::InvalidHexCharacter { c: 'n', index: 0 }
194            ))
195        );
196    }
197}