Skip to main content

p2panda_core/
hash.rs

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