Skip to main content

weavatrix_worktree/
hash.rs

1use core::{fmt, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
4use sha2::{Digest, Sha256};
5
6/// Exact 32-byte SHA-256 digest represented as lowercase hexadecimal on wire.
7#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct Sha256Hash([u8; 32]);
9
10impl Sha256Hash {
11    #[must_use]
12    pub fn compute(bytes: &[u8]) -> Self {
13        Self(Sha256::digest(bytes).into())
14    }
15
16    pub fn parse(value: &str) -> Result<Self, ParseSha256Error> {
17        value.parse()
18    }
19
20    #[must_use]
21    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
22        Self(bytes)
23    }
24
25    #[must_use]
26    pub const fn as_bytes(&self) -> &[u8; 32] {
27        &self.0
28    }
29}
30
31impl FromStr for Sha256Hash {
32    type Err = ParseSha256Error;
33
34    fn from_str(value: &str) -> Result<Self, Self::Err> {
35        if value.len() != 64 {
36            return Err(ParseSha256Error);
37        }
38        let mut bytes = [0_u8; 32];
39        for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
40            bytes[index] = (nibble(pair[0])? << 4) | nibble(pair[1])?;
41        }
42        Ok(Self(bytes))
43    }
44}
45
46impl fmt::Display for Sha256Hash {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        for byte in self.0 {
49            write!(formatter, "{byte:02x}")?;
50        }
51        Ok(())
52    }
53}
54
55impl fmt::Debug for Sha256Hash {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(formatter, "Sha256Hash(\"{self}\")")
58    }
59}
60
61impl Serialize for Sha256Hash {
62    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63    where
64        S: Serializer,
65    {
66        serializer.collect_str(self)
67    }
68}
69
70impl<'de> Deserialize<'de> for Sha256Hash {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        deserializer.deserialize_str(Sha256Visitor)
76    }
77}
78
79struct Sha256Visitor;
80
81impl Visitor<'_> for Sha256Visitor {
82    type Value = Sha256Hash;
83
84    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str("64 lowercase hexadecimal SHA-256 characters")
86    }
87
88    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
89    where
90        E: serde::de::Error,
91    {
92        Sha256Hash::parse(value).map_err(E::custom)
93    }
94}
95
96/// A malformed lowercase SHA-256 hexadecimal value.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct ParseSha256Error;
99
100impl fmt::Display for ParseSha256Error {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        formatter.write_str("SHA-256 must contain 64 lowercase hexadecimal characters")
103    }
104}
105
106impl std::error::Error for ParseSha256Error {}
107
108fn nibble(byte: u8) -> Result<u8, ParseSha256Error> {
109    match byte {
110        b'0'..=b'9' => Ok(byte - b'0'),
111        b'a'..=b'f' => Ok(byte - b'a' + 10),
112        _ => Err(ParseSha256Error),
113    }
114}
115
116pub(crate) struct Sha256Hasher(Sha256);
117
118impl Sha256Hasher {
119    pub(crate) fn new() -> Self {
120        Self(Sha256::new())
121    }
122
123    pub(crate) fn update(&mut self, bytes: &[u8]) {
124        self.0.update(bytes);
125    }
126
127    pub(crate) fn finish(self) -> Sha256Hash {
128        Sha256Hash::from_bytes(self.0.finalize().into())
129    }
130}
131
132impl std::io::Write for Sha256Hasher {
133    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
134        self.update(bytes);
135        Ok(bytes.len())
136    }
137
138    fn flush(&mut self) -> std::io::Result<()> {
139        Ok(())
140    }
141}
142
143pub(crate) fn serialized_hash<T>(value: &T) -> Result<Sha256Hash, blazingly_json::Error>
144where
145    T: Serialize + ?Sized,
146{
147    let mut hasher = Sha256Hasher::new();
148    blazingly_json::to_writer(&mut hasher, value)?;
149    Ok(hasher.finish())
150}
151
152#[cfg(test)]
153mod tests {
154    use crate::hash::{Sha256Hash, Sha256Hasher, serialized_hash};
155
156    #[test]
157    fn matches_standard_vectors_and_incremental_updates() {
158        let expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
159        assert_eq!(Sha256Hash::compute(b"abc").to_string(), expected);
160
161        let mut hasher = Sha256Hasher::new();
162        hasher.update(b"a");
163        hasher.update(b"bc");
164        assert_eq!(hasher.finish().to_string(), expected);
165    }
166
167    #[test]
168    fn parsing_is_exact_and_lowercase() {
169        let digest = Sha256Hash::compute(b"");
170        assert_eq!(digest.to_string().parse(), Ok(digest));
171        assert!(
172            digest
173                .to_string()
174                .to_uppercase()
175                .parse::<Sha256Hash>()
176                .is_err()
177        );
178        assert!("00".parse::<Sha256Hash>().is_err());
179    }
180
181    #[test]
182    fn streaming_json_hash_matches_the_previous_buffered_contract() {
183        fn assert_same<T: serde::Serialize>(value: &T) {
184            let buffered = blazingly_json::to_vec(value).unwrap();
185            assert_eq!(
186                serialized_hash(value).unwrap(),
187                Sha256Hash::compute(&buffered)
188            );
189        }
190        assert_same(&weavatrix_refactor_plan::EditPlan::new(
191            "legacy",
192            Vec::new(),
193        ));
194    }
195}