1use core::fmt;
4
5use sha2::{Digest as _, Sha256};
6
7pub const SHA256_SIZE: usize = 32;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct Digest([u8; SHA256_SIZE]);
13
14impl Digest {
15 pub fn parse(value: &str) -> Result<Self, DigestError> {
17 let encoded = value
18 .strip_prefix("sha256:")
19 .ok_or(DigestError::UnsupportedAlgorithm)?;
20 if encoded.len() != SHA256_SIZE * 2 {
21 return Err(DigestError::InvalidLength);
22 }
23 let mut bytes = [0; SHA256_SIZE];
24 for (index, pair) in encoded.as_bytes().chunks_exact(2).enumerate() {
25 bytes[index] = (hex(pair[0])? << 4) | hex(pair[1])?;
26 }
27 Ok(Self(bytes))
28 }
29
30 pub const fn from_bytes(bytes: [u8; SHA256_SIZE]) -> Self {
31 Self(bytes)
32 }
33
34 pub const fn as_bytes(&self) -> &[u8; SHA256_SIZE] {
35 &self.0
36 }
37}
38
39impl fmt::Display for Digest {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str("sha256:")?;
42 for byte in self.0 {
43 write!(formatter, "{byte:02x}")?;
44 }
45 Ok(())
46 }
47}
48
49pub struct Verifier {
51 expected_digest: Digest,
52 expected_size: Option<u64>,
53 actual_size: u64,
54 hash: Sha256,
55}
56
57impl Verifier {
58 pub fn new(expected_digest: Digest, expected_size: u64) -> Self {
59 Self {
60 expected_digest,
61 expected_size: Some(expected_size),
62 actual_size: 0,
63 hash: Sha256::new(),
64 }
65 }
66
67 pub fn digest_only(expected_digest: Digest) -> Self {
69 Self {
70 expected_digest,
71 expected_size: None,
72 actual_size: 0,
73 hash: Sha256::new(),
74 }
75 }
76
77 pub fn update(&mut self, bytes: &[u8]) -> Result<(), VerifyError> {
78 self.actual_size = self
79 .actual_size
80 .checked_add(bytes.len() as u64)
81 .ok_or(VerifyError::SizeOverflow)?;
82 self.hash.update(bytes);
83 Ok(())
84 }
85
86 pub const fn actual_size(&self) -> u64 {
87 self.actual_size
88 }
89
90 pub fn finish(&self) -> Result<(), VerifyError> {
91 if let Some(expected_size) = self.expected_size.filter(|size| *size != self.actual_size) {
92 return Err(VerifyError::SizeMismatch {
93 expected: expected_size,
94 actual: self.actual_size,
95 });
96 }
97 let actual = Digest::from_bytes(self.hash.clone().finalize().into());
98 if actual != self.expected_digest {
99 return Err(VerifyError::DigestMismatch {
100 expected: self.expected_digest,
101 actual,
102 });
103 }
104 Ok(())
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum DigestError {
110 UnsupportedAlgorithm,
111 InvalidLength,
112 InvalidEncoding,
113}
114
115impl fmt::Display for DigestError {
116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117 formatter.write_str(match self {
118 Self::UnsupportedAlgorithm => "unsupported digest algorithm",
119 Self::InvalidLength => "invalid SHA-256 digest length",
120 Self::InvalidEncoding => "invalid SHA-256 digest encoding",
121 })
122 }
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum VerifyError {
127 SizeOverflow,
128 SizeMismatch { expected: u64, actual: u64 },
129 DigestMismatch { expected: Digest, actual: Digest },
130}
131
132impl fmt::Display for VerifyError {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 Self::SizeOverflow => formatter.write_str("content size overflow"),
136 Self::SizeMismatch { expected, actual } => {
137 write!(
138 formatter,
139 "size mismatch: expected {expected}, got {actual}"
140 )
141 }
142 Self::DigestMismatch { expected, actual } => {
143 write!(
144 formatter,
145 "digest mismatch: expected {expected}, got {actual}"
146 )
147 }
148 }
149 }
150}
151
152fn hex(byte: u8) -> Result<u8, DigestError> {
153 match byte {
154 b'0'..=b'9' => Ok(byte - b'0'),
155 b'a'..=b'f' => Ok(byte - b'a' + 10),
156 _ => Err(DigestError::InvalidEncoding),
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use std::{format, string::ToString};
163
164 use super::{Digest, DigestError, Verifier, VerifyError};
165
166 #[test]
167 fn parses_and_formats_sha256() {
168 let text = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
169 assert_eq!(Digest::parse(text).unwrap().to_string(), text);
170 }
171
172 #[test]
173 fn reports_digest_parse_errors() {
174 assert_eq!(
175 Digest::parse(
176 "sha512:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
177 ),
178 Err(DigestError::UnsupportedAlgorithm)
179 );
180 assert_eq!(Digest::parse("sha256:00"), Err(DigestError::InvalidLength));
181 assert_eq!(
182 Digest::parse(
183 "sha256:ga7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
184 ),
185 Err(DigestError::InvalidEncoding)
186 );
187
188 assert_eq!(
189 DigestError::UnsupportedAlgorithm.to_string(),
190 "unsupported digest algorithm"
191 );
192 assert_eq!(
193 DigestError::InvalidLength.to_string(),
194 "invalid SHA-256 digest length"
195 );
196 assert_eq!(
197 DigestError::InvalidEncoding.to_string(),
198 "invalid SHA-256 digest encoding"
199 );
200 }
201
202 #[test]
203 fn verifies_fragmented_content() {
204 let digest = Digest::parse(
205 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
206 )
207 .unwrap();
208 let mut verifier = Verifier::new(digest, 3);
209 assert_eq!(verifier.actual_size(), 0);
210 verifier.update(b"a").unwrap();
211 assert_eq!(verifier.actual_size(), 1);
212 verifier.update(b"bc").unwrap();
213 assert_eq!(verifier.actual_size(), 3);
214 assert_eq!(verifier.finish(), Ok(()));
215
216 let mut verifier = Verifier::new(digest, 4);
217 verifier.update(b"abc").unwrap();
218 assert!(matches!(
219 verifier.finish(),
220 Err(VerifyError::SizeMismatch { .. })
221 ));
222 }
223
224 #[test]
225 fn formats_verification_errors() {
226 let expected = Digest::from_bytes([0x11; 32]);
227 let actual = Digest::from_bytes([0x22; 32]);
228
229 assert_eq!(
230 VerifyError::SizeOverflow.to_string(),
231 "content size overflow"
232 );
233 assert_eq!(
234 VerifyError::SizeMismatch {
235 expected: 7,
236 actual: 3,
237 }
238 .to_string(),
239 "size mismatch: expected 7, got 3"
240 );
241 assert_eq!(
242 VerifyError::DigestMismatch { expected, actual }.to_string(),
243 format!("digest mismatch: expected {expected}, got {actual}")
244 );
245 }
246}