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, ShortFormat};
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 ShortFormat for Hash {
154    fn fmt_short(&self) -> String {
155        self.to_hex()[0..5].to_string()
156    }
157}
158
159impl OperationId for Hash {}
160
161/// Error types for `Hash` struct.
162#[derive(Error, Debug)]
163pub enum HashError {
164    /// Hash string has an invalid length.
165    #[error("invalid hash length {0} bytes, expected {1} bytes")]
166    InvalidLength(usize, usize),
167
168    /// Hash string contains invalid hexadecimal characters.
169    #[error("invalid hex encoding in hash string")]
170    InvalidHexEncoding(#[from] hex::FromHexError),
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{Hash, HashError};
176
177    #[test]
178    fn hashing() {
179        let hash = Hash::digest([1, 2, 3]);
180
181        assert_eq!(
182            hash.as_bytes(),
183            &[
184                177, 119, 236, 27, 242, 109, 251, 59, 112, 16, 212, 115, 230, 212, 71, 19, 178,
185                155, 118, 91, 153, 198, 230, 14, 203, 250, 231, 66, 222, 73, 101, 67
186            ]
187        );
188    }
189
190    #[test]
191    fn invalid_length() {
192        let bytes = vec![254, 100, 4, 7];
193        let result: Result<Hash, HashError> = bytes.as_slice().try_into();
194        matches!(result, Err(HashError::InvalidLength(4, 32)));
195    }
196
197    #[test]
198    fn invalid_hex_encoding() {
199        let hex = "notreallyahexstring";
200        let result: Result<Hash, HashError> = hex.parse();
201        matches!(
202            result,
203            Err(HashError::InvalidHexEncoding(
204                hex::FromHexError::InvalidHexCharacter { c: 'n', index: 0 }
205            ))
206        );
207    }
208}