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::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
118#[allow(clippy::non_canonical_partial_ord_impl)]
119impl PartialOrd for Hash {
120    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
121        Some(self.0.as_bytes().cmp(other.0.as_bytes()))
122    }
123}
124
125impl Ord for Hash {
126    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
127        self.0.as_bytes().cmp(other.0.as_bytes())
128    }
129}
130
131impl fmt::Display for Hash {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}", self.to_hex())
134    }
135}
136
137impl fmt::Debug for Hash {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.debug_tuple("Hash").field(self.0.as_bytes()).finish()
140    }
141}
142
143#[cfg(feature = "arbitrary")]
144impl<'a> Arbitrary<'a> for Hash {
145    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
146        let bytes = <[u8; HASH_LEN] as Arbitrary>::arbitrary(u)?;
147        Ok(Hash::from_bytes(bytes))
148    }
149}
150
151/// Error types for `Hash` struct.
152#[derive(Error, Debug)]
153pub enum HashError {
154    /// Hash string has an invalid length.
155    #[error("invalid hash length {0} bytes, expected {1} bytes")]
156    InvalidLength(usize, usize),
157
158    /// Hash string contains invalid hexadecimal characters.
159    #[error("invalid hex encoding in hash string")]
160    InvalidHexEncoding(#[from] hex::FromHexError),
161}
162
163#[cfg(test)]
164mod tests {
165    use super::{Hash, HashError};
166
167    #[test]
168    fn hashing() {
169        let hash = Hash::new([1, 2, 3]);
170
171        assert_eq!(
172            hash.as_bytes(),
173            &[
174                177, 119, 236, 27, 242, 109, 251, 59, 112, 16, 212, 115, 230, 212, 71, 19, 178,
175                155, 118, 91, 153, 198, 230, 14, 203, 250, 231, 66, 222, 73, 101, 67
176            ]
177        );
178    }
179
180    #[test]
181    fn invalid_length() {
182        let bytes = vec![254, 100, 4, 7];
183        let result: Result<Hash, HashError> = bytes.as_slice().try_into();
184        matches!(result, Err(HashError::InvalidLength(4, 32)));
185    }
186
187    #[test]
188    fn invalid_hex_encoding() {
189        let hex = "notreallyahexstring";
190        let result: Result<Hash, HashError> = hex.parse();
191        matches!(
192            result,
193            Err(HashError::InvalidHexEncoding(
194                hex::FromHexError::InvalidHexCharacter { c: 'n', index: 0 }
195            ))
196        );
197    }
198}