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
103            .strip_prefix("0x")
104            .or_else(|| s.strip_prefix("0X"))
105            .unwrap_or(s);
106        let bytes = hex::decode(s).map_err(|e| HashParseError::InvalidHex(e.to_string()))?;
107        if bytes.len() != 32 {
108            return Err(HashParseError::WrongLength {
109                expected: 32,
110                got: bytes.len(),
111            });
112        }
113        let mut arr = [0u8; 32];
114        arr.copy_from_slice(&bytes);
115        Ok(Self(arr))
116    }
117}
118
119// ============================================================================
120// Trait Implementations
121// ============================================================================
122
123impl AsRef<[u8]> for Hash {
124    #[inline]
125    fn as_ref(&self) -> &[u8] {
126        &self.0
127    }
128}
129
130impl AsRef<[u8; 32]> for Hash {
131    #[inline]
132    fn as_ref(&self) -> &[u8; 32] {
133        &self.0
134    }
135}
136
137impl From<[u8; 32]> for Hash {
138    #[inline]
139    fn from(bytes: [u8; 32]) -> Self {
140        Self(bytes)
141    }
142}
143
144impl From<&[u8; 32]> for Hash {
145    #[inline]
146    fn from(bytes: &[u8; 32]) -> Self {
147        Self(*bytes)
148    }
149}
150
151impl TryFrom<&[u8]> for Hash {
152    type Error = HashParseError;
153
154    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
155        if bytes.len() != 32 {
156            return Err(HashParseError::WrongLength {
157                expected: 32,
158                got: bytes.len(),
159            });
160        }
161        let mut arr = [0u8; 32];
162        arr.copy_from_slice(bytes);
163        Ok(Self(arr))
164    }
165}
166
167impl FromStr for Hash {
168    type Err = HashParseError;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Self::from_hex(s)
172    }
173}
174
175impl fmt::Display for Hash {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        // Show first 8 hex chars + ellipsis for compact display
178        if f.alternate() {
179            write!(f, "0x{}", self.to_hex())
180        } else {
181            write!(f, "0x{}…", &self.to_hex()[..8])
182        }
183    }
184}
185
186impl fmt::Debug for Hash {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(f, "Hash(0x{})", self.to_hex())
189    }
190}
191
192impl Default for Hash {
193    #[inline]
194    fn default() -> Self {
195        Self::zero()
196    }
197}
198
199// ============================================================================
200// Error Types
201// ============================================================================
202
203/// Errors that can occur when parsing a [`struct@Hash`] from a string or byte slice.
204#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
205#[allow(missing_docs)]
206pub enum HashParseError {
207    /// The input string is not valid hexadecimal.
208    #[error("invalid hex: {0}")]
209    InvalidHex(String),
210
211    /// The decoded bytes are not exactly 32 bytes.
212    #[error("expected 32 bytes, got {got}")]
213    WrongLength { expected: usize, got: usize },
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_hash_new() {
222        let h = Hash::new([1u8; 32]);
223        assert_eq!(h.as_bytes(), &[1u8; 32]);
224    }
225
226    #[test]
227    fn test_hash_zero() {
228        let h = Hash::zero();
229        assert_eq!(h.as_bytes(), &[0u8; 32]);
230    }
231
232    #[test]
233    fn test_hash_hex_roundtrip() {
234        let h = Hash::new([0xAB; 32]);
235        let hex = h.to_hex();
236        let parsed = Hash::from_hex(&hex).unwrap();
237        assert_eq!(h, parsed);
238    }
239
240    #[test]
241    fn test_hash_from_hex_with_prefix() {
242        let h = Hash::from_hex("0xabcdef").unwrap_err();
243        assert!(matches!(h, HashParseError::WrongLength { .. }));
244    }
245
246    #[test]
247    fn test_hash_display() {
248        let h = Hash::new([0xAB; 32]);
249        let display = format!("{}", h);
250        assert!(display.starts_with("0x"));
251        assert!(display.contains("…"));
252    }
253
254    #[test]
255    fn test_hash_display_altern() {
256        let h = Hash::new([0xAB; 32]);
257        let display = format!("{:#}", h);
258        assert_eq!(display.len(), 66); // "0x" + 64 hex chars
259    }
260
261    #[test]
262    fn test_hash_debug() {
263        let h = Hash::new([0xAB; 32]);
264        let debug = format!("{:?}", h);
265        assert!(debug.starts_with("Hash(0x"));
266    }
267
268    #[test]
269    fn test_hash_from_str() {
270        let h: Hash = "abababababababababababababababababababababababababababababababab"
271            .parse()
272            .unwrap();
273        assert_eq!(
274            h.to_hex(),
275            "abababababababababababababababababababababababababababababababab"
276        );
277    }
278}