weavatrix_worktree/
hash.rs1use core::{fmt, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
4use sha2::{Digest, Sha256};
5
6#[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#[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}