Skip to main content

csv_adapter_core/
hash.rs

1//! A 32-byte cryptographic hash used throughout the CSV protocol.
2//!
3//! This type wraps a fixed-size `[u8; 32]` array and provides safe conversion
4//! between byte slices, hex strings, and the internal representation. All
5//! hashing in CSV uses SHA-256 with domain separation to prevent cross-protocol
6//! replay attacks (see `crate::tagged_hash`).
7//!
8//! # Examples
9//!
10//! ```
11//! use csv_adapter_core::Hash;
12//!
13//! // Create from bytes
14//! let h = Hash::new([0xAB; 32]);
15//!
16//! // Convert to hex
17//! let hex = h.to_hex();
18//! assert!(hex.starts_with("abab"));
19//!
20//! // Parse from hex
21//! let parsed = Hash::from_hex(&hex).unwrap();
22//! assert_eq!(h, parsed);
23//! ```
24
25use std::fmt;
26use std::str::FromStr;
27
28use serde::{Deserialize, Serialize};
29
30/// A 32-byte hash value.
31///
32/// This is the fundamental building block for commitments, right IDs,
33/// seal references, and all cryptographic operations in CSV.
34#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
35pub struct Hash([u8; 32]);
36
37impl Hash {
38    /// Creates a new [`struct@Hash`] from exactly 32 bytes.
39    ///
40    /// # Panics
41    /// This method does not panic — it accepts any `[u8; 32]`. For fallible
42    /// construction from a slice, use [`Hash::try_from`].
43    #[inline]
44    pub const fn new(bytes: [u8; 32]) -> Self {
45        Self(bytes)
46    }
47
48    /// Returns a hash of all zeros. Useful as a sentinel value.
49    #[inline]
50    pub const fn zero() -> Self {
51        Self([0u8; 32])
52    }
53
54    /// Returns a reference to the underlying 32-byte array.
55    #[inline]
56    pub const fn as_bytes(&self) -> &[u8; 32] {
57        &self.0
58    }
59
60    /// Returns a mutable reference to the underlying 32-byte array.
61    #[inline]
62    pub fn as_bytes_mut(&mut self) -> &mut [u8; 32] {
63        &mut self.0
64    }
65
66    /// Returns the hash as a byte slice.
67    #[inline]
68    pub fn as_slice(&self) -> &[u8] {
69        &self.0
70    }
71
72    /// Consumes the hash and returns the inner byte array.
73    #[inline]
74    pub fn into_inner(self) -> [u8; 32] {
75        self.0
76    }
77
78    /// Returns a new [`Vec<u8>`] containing the hash bytes.
79    ///
80    /// This allocates. For a borrowed slice, use [`Self::as_slice`].
81    #[inline]
82    pub fn to_vec(&self) -> Vec<u8> {
83        self.0.to_vec()
84    }
85
86    /// Returns the hash as a lowercase hex string without the `0x` prefix.
87    ///
88    /// The returned string is always 64 characters long.
89    pub fn to_hex(&self) -> String {
90        hex::encode(self.0)
91    }
92
93    /// Parses a [`struct@Hash`] from a hex string.
94    ///
95    /// The input may optionally start with `0x` or `0X`. The remaining
96    /// characters must be valid hex digits representing exactly 32 bytes.
97    ///
98    /// # Errors
99    /// Returns [`HashParseError`] if the input is not valid hex or does not
100    /// represent exactly 32 bytes.
101    pub fn from_hex(s: &str) -> Result<Self, HashParseError> {
102        let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
103        let bytes = hex::decode(s).map_err(|e| HashParseError::InvalidHex(e.to_string()))?;
104        if bytes.len() != 32 {
105            return Err(HashParseError::WrongLength {
106                expected: 32,
107                got: bytes.len(),
108            });
109        }
110        let mut arr = [0u8; 32];
111        arr.copy_from_slice(&bytes);
112        Ok(Self(arr))
113    }
114}
115
116// ============================================================================
117// Trait Implementations
118// ============================================================================
119
120impl AsRef<[u8]> for Hash {
121    #[inline]
122    fn as_ref(&self) -> &[u8] {
123        &self.0
124    }
125}
126
127impl AsRef<[u8; 32]> for Hash {
128    #[inline]
129    fn as_ref(&self) -> &[u8; 32] {
130        &self.0
131    }
132}
133
134impl From<[u8; 32]> for Hash {
135    #[inline]
136    fn from(bytes: [u8; 32]) -> Self {
137        Self(bytes)
138    }
139}
140
141impl From<&[u8; 32]> for Hash {
142    #[inline]
143    fn from(bytes: &[u8; 32]) -> Self {
144        Self(*bytes)
145    }
146}
147
148impl TryFrom<&[u8]> for Hash {
149    type Error = HashParseError;
150
151    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
152        if bytes.len() != 32 {
153            return Err(HashParseError::WrongLength {
154                expected: 32,
155                got: bytes.len(),
156            });
157        }
158        let mut arr = [0u8; 32];
159        arr.copy_from_slice(bytes);
160        Ok(Self(arr))
161    }
162}
163
164impl FromStr for Hash {
165    type Err = HashParseError;
166
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        Self::from_hex(s)
169    }
170}
171
172impl fmt::Display for Hash {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        // Show first 8 hex chars + ellipsis for compact display
175        if f.alternate() {
176            write!(f, "0x{}", self.to_hex())
177        } else {
178            write!(f, "0x{}…", &self.to_hex()[..8])
179        }
180    }
181}
182
183impl fmt::Debug for Hash {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "Hash(0x{})", self.to_hex())
186    }
187}
188
189impl Default for Hash {
190    #[inline]
191    fn default() -> Self {
192        Self::zero()
193    }
194}
195
196// ============================================================================
197// Error Types
198// ============================================================================
199
200/// Errors that can occur when parsing a [`struct@Hash`] from a string or byte slice.
201#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
202#[allow(missing_docs)]
203pub enum HashParseError {
204    /// The input string is not valid hexadecimal.
205    #[error("invalid hex: {0}")]
206    InvalidHex(String),
207
208    /// The decoded bytes are not exactly 32 bytes.
209    #[error("expected 32 bytes, got {got}")]
210    WrongLength { expected: usize, got: usize },
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_hash_new() {
219        let h = Hash::new([1u8; 32]);
220        assert_eq!(h.as_bytes(), &[1u8; 32]);
221    }
222
223    #[test]
224    fn test_hash_zero() {
225        let h = Hash::zero();
226        assert_eq!(h.as_bytes(), &[0u8; 32]);
227    }
228
229    #[test]
230    fn test_hash_hex_roundtrip() {
231        let h = Hash::new([0xAB; 32]);
232        let hex = h.to_hex();
233        let parsed = Hash::from_hex(&hex).unwrap();
234        assert_eq!(h, parsed);
235    }
236
237    #[test]
238    fn test_hash_from_hex_with_prefix() {
239        let h = Hash::from_hex("0xabcdef").unwrap_err();
240        assert!(matches!(h, HashParseError::WrongLength { .. }));
241    }
242
243    #[test]
244    fn test_hash_display() {
245        let h = Hash::new([0xAB; 32]);
246        let display = format!("{}", h);
247        assert!(display.starts_with("0x"));
248        assert!(display.contains("…"));
249    }
250
251    #[test]
252    fn test_hash_display_altern() {
253        let h = Hash::new([0xAB; 32]);
254        let display = format!("{:#}", h);
255        assert_eq!(display.len(), 66); // "0x" + 64 hex chars
256    }
257
258    #[test]
259    fn test_hash_debug() {
260        let h = Hash::new([0xAB; 32]);
261        let debug = format!("{:?}", h);
262        assert!(debug.starts_with("Hash(0x"));
263    }
264
265    #[test]
266    fn test_hash_from_str() {
267        let h: Hash = "abababababababababababababababababababababababababababababababab"
268            .parse()
269            .unwrap();
270        assert_eq!(h.to_hex(), "abababababababababababababababababababababababababababababababab");
271    }
272}