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, derive_more::Display)]
109pub enum DigestError {
110 #[display("unsupported digest algorithm")]
111 UnsupportedAlgorithm,
112 #[display("invalid SHA-256 digest length")]
113 InvalidLength,
114 #[display("invalid SHA-256 digest encoding")]
115 InvalidEncoding,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
119pub enum VerifyError {
120 #[display("content size overflow")]
121 SizeOverflow,
122 #[display("size mismatch: expected {expected}, got {actual}")]
123 SizeMismatch { expected: u64, actual: u64 },
124 #[display("digest mismatch: expected {expected}, got {actual}")]
125 DigestMismatch { expected: Digest, actual: Digest },
126}
127
128fn hex(byte: u8) -> Result<u8, DigestError> {
129 match byte {
130 b'0'..=b'9' => Ok(byte - b'0'),
131 b'a'..=b'f' => Ok(byte - b'a' + 10),
132 _ => Err(DigestError::InvalidEncoding),
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use std::{format, string::ToString};
139
140 use super::{Digest, DigestError, Verifier, VerifyError};
141
142 #[test]
143 fn parses_and_formats_sha256() {
144 let text = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
145 assert_eq!(Digest::parse(text).unwrap().to_string(), text);
146 }
147
148 #[test]
149 fn reports_digest_parse_errors() {
150 assert_eq!(
151 Digest::parse(
152 "sha512:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
153 ),
154 Err(DigestError::UnsupportedAlgorithm)
155 );
156 assert_eq!(Digest::parse("sha256:00"), Err(DigestError::InvalidLength));
157 assert_eq!(
158 Digest::parse(
159 "sha256:ga7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
160 ),
161 Err(DigestError::InvalidEncoding)
162 );
163
164 assert_eq!(
165 DigestError::UnsupportedAlgorithm.to_string(),
166 "unsupported digest algorithm"
167 );
168 assert_eq!(
169 DigestError::InvalidLength.to_string(),
170 "invalid SHA-256 digest length"
171 );
172 assert_eq!(
173 DigestError::InvalidEncoding.to_string(),
174 "invalid SHA-256 digest encoding"
175 );
176 }
177
178 #[test]
179 fn verifies_fragmented_content() {
180 let digest = Digest::parse(
181 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
182 )
183 .unwrap();
184 let mut verifier = Verifier::new(digest, 3);
185 assert_eq!(verifier.actual_size(), 0);
186 verifier.update(b"a").unwrap();
187 assert_eq!(verifier.actual_size(), 1);
188 verifier.update(b"bc").unwrap();
189 assert_eq!(verifier.actual_size(), 3);
190 assert_eq!(verifier.finish(), Ok(()));
191
192 let mut verifier = Verifier::new(digest, 4);
193 verifier.update(b"abc").unwrap();
194 assert!(matches!(
195 verifier.finish(),
196 Err(VerifyError::SizeMismatch { .. })
197 ));
198 }
199
200 #[test]
201 fn formats_verification_errors() {
202 let expected = Digest::from_bytes([0x11; 32]);
203 let actual = Digest::from_bytes([0x22; 32]);
204
205 assert_eq!(
206 VerifyError::SizeOverflow.to_string(),
207 "content size overflow"
208 );
209 assert_eq!(
210 VerifyError::SizeMismatch {
211 expected: 7,
212 actual: 3,
213 }
214 .to_string(),
215 "size mismatch: expected 7, got 3"
216 );
217 assert_eq!(
218 VerifyError::DigestMismatch { expected, actual }.to_string(),
219 format!("digest mismatch: expected {expected}, got {actual}")
220 );
221 }
222}