mdcs_merkle/
hash.rs

1//! Content-addressed hashing for Merkle nodes.
2//!
3//! Uses SHA-256 to generate Content Identifiers (CIDs) for nodes.
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::fmt;
8
9/// A 32-byte SHA-256 hash used as a Content Identifier (CID).
10#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
11pub struct Hash([u8; 32]);
12
13impl Hash {
14    /// Create a hash from raw bytes.
15    pub fn from_bytes(bytes: [u8; 32]) -> Self {
16        Hash(bytes)
17    }
18
19    /// Get the underlying bytes.
20    pub fn as_bytes(&self) -> &[u8; 32] {
21        &self.0
22    }
23
24    /// Create a zero hash (used for genesis nodes).
25    pub fn zero() -> Self {
26        Hash([0u8; 32])
27    }
28
29    /// Check if this is the zero hash.
30    pub fn is_zero(&self) -> bool {
31        self.0 == [0u8; 32]
32    }
33
34    /// Convert to hex string for display.
35    pub fn to_hex(&self) -> String {
36        self.0.iter().map(|b| format!("{:02x}", b)).collect()
37    }
38
39    /// Create from hex string.
40    pub fn from_hex(s: &str) -> Option<Self> {
41        if s.len() != 64 {
42            return None;
43        }
44        let mut bytes = [0u8; 32];
45        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
46            let hex_str = std::str::from_utf8(chunk).ok()?;
47            bytes[i] = u8::from_str_radix(hex_str, 16).ok()?;
48        }
49        Some(Hash(bytes))
50    }
51
52    /// Truncated display (first 8 chars).
53    pub fn short(&self) -> String {
54        self.to_hex()[..8].to_string()
55    }
56}
57
58impl fmt::Debug for Hash {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "Hash({}...)", &self.to_hex()[..8])
61    }
62}
63
64impl fmt::Display for Hash {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "{}", self.to_hex())
67    }
68}
69
70impl Default for Hash {
71    fn default() -> Self {
72        Hash::zero()
73    }
74}
75
76/// Hasher utility for computing content hashes.
77pub struct Hasher {
78    inner: Sha256,
79}
80
81impl Hasher {
82    /// Create a new hasher.
83    pub fn new() -> Self {
84        Hasher {
85            inner: Sha256::new(),
86        }
87    }
88
89    /// Update the hasher with data.
90    pub fn update(&mut self, data: &[u8]) {
91        self.inner.update(data);
92    }
93
94    /// Finalize and return the hash.
95    pub fn finalize(self) -> Hash {
96        let result = self.inner.finalize();
97        let mut bytes = [0u8; 32];
98        bytes.copy_from_slice(&result);
99        Hash(bytes)
100    }
101
102    /// Hash data directly.
103    pub fn hash(data: &[u8]) -> Hash {
104        let mut hasher = Self::new();
105        hasher.update(data);
106        hasher.finalize()
107    }
108
109    /// Hash multiple pieces of data.
110    pub fn hash_all(parts: &[&[u8]]) -> Hash {
111        let mut hasher = Self::new();
112        for part in parts {
113            hasher.update(part);
114        }
115        hasher.finalize()
116    }
117}
118
119impl Default for Hasher {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_hash_deterministic() {
131        let data = b"hello world";
132        let h1 = Hasher::hash(data);
133        let h2 = Hasher::hash(data);
134        assert_eq!(h1, h2);
135    }
136
137    #[test]
138    fn test_hash_different_data() {
139        let h1 = Hasher::hash(b"hello");
140        let h2 = Hasher::hash(b"world");
141        assert_ne!(h1, h2);
142    }
143
144    #[test]
145    fn test_hex_roundtrip() {
146        let h1 = Hasher::hash(b"test data");
147        let hex = h1.to_hex();
148        let h2 = Hash::from_hex(&hex).unwrap();
149        assert_eq!(h1, h2);
150    }
151
152    #[test]
153    fn test_zero_hash() {
154        let zero = Hash::zero();
155        assert!(zero.is_zero());
156        assert!(!Hasher::hash(b"test").is_zero());
157    }
158
159    #[test]
160    fn test_hash_all() {
161        let h1 = Hasher::hash_all(&[b"hello", b"world"]);
162
163        let mut hasher = Hasher::new();
164        hasher.update(b"hello");
165        hasher.update(b"world");
166        let h2 = hasher.finalize();
167
168        assert_eq!(h1, h2);
169    }
170}