1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use anyhow::{Result, anyhow};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub struct Hash(pub [u8; 32]);
9
10impl Hash {
11 pub fn zero() -> Self {
12 Self([0u8; 32])
13 }
14
15 pub fn as_bytes(&self) -> &[u8; 32] {
16 &self.0
17 }
18
19 pub fn from_bytes(bytes: [u8; 32]) -> Self {
20 Self(bytes)
21 }
22}
23
24impl Display for Hash {
25 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26 for b in self.0 {
27 write!(f, "{b:02x}")?;
28 }
29 Ok(())
30 }
31}
32
33impl FromStr for Hash {
34 type Err = anyhow::Error;
35
36 fn from_str(s: &str) -> Result<Self> {
37 if s.len() != 64 {
38 return Err(anyhow!("expected 64-char hex hash, got {}", s.len()));
39 }
40
41 let mut out = [0u8; 32];
42 for (idx, chunk) in s.as_bytes().chunks(2).enumerate() {
43 let chunk_str = std::str::from_utf8(chunk)?;
44 out[idx] = u8::from_str_radix(chunk_str, 16)
45 .map_err(|e| anyhow!("invalid hex at byte {idx}: {e}"))?;
46 }
47 Ok(Self(out))
48 }
49}
50
51impl Serialize for Hash {
52 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53 where
54 S: Serializer,
55 {
56 serializer.serialize_str(&self.to_string())
57 }
58}
59
60impl<'de> Deserialize<'de> for Hash {
61 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62 where
63 D: Deserializer<'de>,
64 {
65 let s = String::deserialize(deserializer)?;
66 Hash::from_str(&s).map_err(serde::de::Error::custom)
67 }
68}
69
70pub fn hash_typed(tag: &[u8], bytes: &[u8]) -> Hash {
71 let mut hasher = blake3::Hasher::new();
72 hasher.update(tag);
73 hasher.update(bytes);
74 let out = hasher.finalize();
75 Hash(*out.as_bytes())
76}
77
78pub fn hash_blob(bytes: &[u8]) -> Hash {
79 hash_typed(b"blob:", bytes)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn hash_parse_roundtrip() {
88 let h = hash_blob(b"hello");
89 let parsed = Hash::from_str(&h.to_string()).unwrap();
90 assert_eq!(h, parsed);
91 }
92
93 #[test]
94 fn hash_parse_rejects_invalid_len() {
95 assert!(Hash::from_str("abcd").is_err());
96 }
97
98 #[test]
99 fn hash_parse_rejects_invalid_hex() {
100 let bad = "g".repeat(64);
101 assert!(Hash::from_str(&bad).is_err());
102 }
103
104 #[test]
105 fn hash_blob_is_deterministic() {
106 assert_eq!(hash_blob(b"abc"), hash_blob(b"abc"));
107 }
108
109 #[test]
110 fn hash_typed_domain_separates() {
111 let a = hash_typed(b"blob:", b"abc");
112 let b = hash_typed(b"commit:", b"abc");
113 assert_ne!(a, b);
114 }
115}