Skip to main content

gpp_core/
hash.rs

1//! BLAKE3 content hash and its base32 display encoding.
2//!
3//! Per `docs/DATA_MODEL.md`: hashes are BLAKE3 (256-bit), displayed as
4//! lowercase base32 (RFC 4648 alphabet, unpadded) which is exactly 52
5//! characters for 32 bytes. Short form is the first 8 characters.
6
7use std::fmt;
8use std::str::FromStr;
9
10use serde::de::{self, Visitor};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use crate::error::Error;
14
15/// Length of a BLAKE3 digest in bytes.
16pub const HASH_LEN: usize = 32;
17
18/// Length of the base32 textual form of a [`Hash`].
19pub const HASH_STR_LEN: usize = 52;
20
21/// Number of leading characters used for the short display form.
22pub const SHORT_LEN: usize = 8;
23
24/// RFC 4648 base32 alphabet, lowercased (case-insensitive on decode).
25const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
26
27/// A 256-bit BLAKE3 content address.
28#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct ObjectHash([u8; HASH_LEN]);
30
31/// Public alias — most call sites just want `Hash`.
32pub type Hash = ObjectHash;
33
34impl ObjectHash {
35    /// Hash an arbitrary byte slice.
36    pub fn of(bytes: &[u8]) -> Self {
37        Self(blake3::hash(bytes).into())
38    }
39
40    /// Wrap a raw 32-byte digest.
41    pub fn from_raw(bytes: [u8; HASH_LEN]) -> Self {
42        Self(bytes)
43    }
44
45    /// The raw 32-byte digest.
46    pub fn as_bytes(&self) -> &[u8; HASH_LEN] {
47        &self.0
48    }
49
50    /// Full lowercase base32 form (52 chars).
51    pub fn to_base32(self) -> String {
52        base32_encode(&self.0)
53    }
54
55    /// First [`SHORT_LEN`] characters of the base32 form.
56    pub fn short(self) -> String {
57        let mut s = self.to_base32();
58        s.truncate(SHORT_LEN);
59        s
60    }
61
62    /// Parse a (case-insensitive) base32 hash string.
63    pub fn from_base32(s: &str) -> Result<Self, Error> {
64        let decoded = base32_decode(s)?;
65        let bytes: [u8; HASH_LEN] = decoded.as_slice().try_into().map_err(|_| {
66            Error::InvalidHash(format!("expected {HASH_LEN} bytes, got {}", decoded.len()))
67        })?;
68        Ok(Self(bytes))
69    }
70}
71
72impl fmt::Display for ObjectHash {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(&self.to_base32())
75    }
76}
77
78impl fmt::Debug for ObjectHash {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "Hash({})", self.short())
81    }
82}
83
84impl FromStr for ObjectHash {
85    type Err = Error;
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Self::from_base32(s)
88    }
89}
90
91impl Serialize for ObjectHash {
92    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
93        s.serialize_bytes(&self.0)
94    }
95}
96
97impl<'de> Deserialize<'de> for ObjectHash {
98    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
99        struct HashVisitor;
100        impl<'de> Visitor<'de> for HashVisitor {
101            type Value = ObjectHash;
102            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103                write!(f, "a 32-byte BLAKE3 digest")
104            }
105            fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<ObjectHash, E> {
106                let b: [u8; HASH_LEN] = v
107                    .try_into()
108                    .map_err(|_| E::invalid_length(v.len(), &self))?;
109                Ok(ObjectHash(b))
110            }
111            fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<ObjectHash, A::Error> {
112                let mut b = [0u8; HASH_LEN];
113                for (i, slot) in b.iter_mut().enumerate() {
114                    *slot = seq
115                        .next_element()?
116                        .ok_or_else(|| de::Error::invalid_length(i, &self))?;
117                }
118                Ok(ObjectHash(b))
119            }
120        }
121        d.deserialize_bytes(HashVisitor)
122    }
123}
124
125/// Encode bytes as lowercase, unpadded base32 (RFC 4648 alphabet).
126fn base32_encode(data: &[u8]) -> String {
127    let mut out = String::with_capacity(data.len().div_ceil(5) * 8);
128    let mut buf: u32 = 0;
129    let mut bits: u32 = 0;
130    for &byte in data {
131        buf = (buf << 8) | u32::from(byte);
132        bits += 8;
133        while bits >= 5 {
134            bits -= 5;
135            out.push(ALPHABET[((buf >> bits) & 0x1f) as usize] as char);
136        }
137    }
138    if bits > 0 {
139        out.push(ALPHABET[((buf << (5 - bits)) & 0x1f) as usize] as char);
140    }
141    out
142}
143
144/// Decode a (case-insensitive) base32 string. Rejects non-alphabet chars.
145fn base32_decode(s: &str) -> Result<Vec<u8>, Error> {
146    let mut out = Vec::with_capacity(s.len() * 5 / 8);
147    let mut buf: u32 = 0;
148    let mut bits: u32 = 0;
149    for ch in s.chars() {
150        let lc = ch.to_ascii_lowercase();
151        let val = match lc {
152            'a'..='z' => lc as u32 - 'a' as u32,
153            '2'..='7' => lc as u32 - '2' as u32 + 26,
154            _ => {
155                return Err(Error::InvalidHash(format!(
156                    "illegal base32 character {ch:?}"
157                )));
158            }
159        };
160        buf = (buf << 5) | val;
161        bits += 5;
162        if bits >= 8 {
163            bits -= 8;
164            out.push(((buf >> bits) & 0xff) as u8);
165        }
166    }
167    Ok(out)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn hash_string_is_52_chars_and_roundtrips() {
176        let h = Hash::of(b"hello gpp");
177        let s = h.to_base32();
178        assert_eq!(s.len(), HASH_STR_LEN);
179        assert_eq!(Hash::from_base32(&s).unwrap(), h);
180    }
181
182    #[test]
183    fn decode_is_case_insensitive() {
184        let h = Hash::of(b"case test");
185        let lower = h.to_base32();
186        let upper = lower.to_uppercase();
187        assert_eq!(Hash::from_base32(&upper).unwrap(), h);
188    }
189
190    #[test]
191    fn short_form_is_prefix() {
192        let h = Hash::of(b"short");
193        assert_eq!(h.short().len(), SHORT_LEN);
194        assert!(h.to_base32().starts_with(&h.short()));
195    }
196
197    #[test]
198    fn rejects_non_alphabet() {
199        assert!(Hash::from_base32("not valid!!").is_err());
200        assert!(Hash::from_base32("0189").is_err()); // 0,1,8,9 are not in the alphabet
201    }
202
203    #[test]
204    fn distinct_inputs_distinct_hashes() {
205        assert_ne!(Hash::of(b"a"), Hash::of(b"b"));
206    }
207}