1use sha2::{Digest, Sha256};
8use std::{
9 fmt,
10 fs::File,
11 io::{self, Read},
12 path::Path,
13 str::FromStr,
14};
15
16#[cfg(test)]
17mod tests;
18
19#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21pub struct Sha256Digest([u8; 32]);
22
23impl Sha256Digest {
24 #[must_use]
26 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
27 Self(bytes)
28 }
29
30 #[must_use]
32 pub const fn as_bytes(&self) -> &[u8; 32] {
33 &self.0
34 }
35
36 #[must_use]
38 pub fn compute(bytes: &[u8]) -> Self {
39 Self(Sha256::digest(bytes).into())
40 }
41}
42
43impl fmt::Display for Sha256Digest {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 for byte in self.0 {
46 write!(f, "{byte:02x}")?;
47 }
48 Ok(())
49 }
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum DigestParseError {
55 Length {
57 actual: usize,
59 },
60 Digit {
62 offset: usize,
64 },
65}
66
67impl fmt::Display for DigestParseError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::Length { actual } => {
71 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
72 }
73 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
74 }
75 }
76}
77impl std::error::Error for DigestParseError {}
78
79impl FromStr for Sha256Digest {
80 type Err = DigestParseError;
81
82 fn from_str(text: &str) -> Result<Self, Self::Err> {
83 if text.len() != 64 {
84 return Err(DigestParseError::Length { actual: text.len() });
85 }
86 let mut bytes = [0; 32];
87 for (offset, digit) in text.bytes().enumerate() {
88 let nibble = match digit {
89 b'0'..=b'9' => digit - b'0',
90 b'a'..=b'f' => digit - b'a' + 10,
91 _ => return Err(DigestParseError::Digit { offset }),
92 };
93 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
94 }
95 Ok(Self(bytes))
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct ArtifactIdentity {
102 pub bytes: u64,
104 pub sha256: Sha256Digest,
106}
107
108#[derive(Debug)]
110pub enum ArtifactError {
111 Io(io::Error),
113 NotRegularFile,
115 LimitExceeded {
117 limit: u64,
119 },
120 DigestMismatch {
122 expected: Sha256Digest,
124 actual: ArtifactIdentity,
126 },
127 Allocation(std::collections::TryReserveError),
129}
130
131impl fmt::Display for ArtifactError {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::Io(source) => write!(f, "artifact read failed: {source}"),
135 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
136 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
137 Self::DigestMismatch { expected, actual } => write!(
138 f,
139 "artifact SHA-256 is {}, expected {expected}",
140 actual.sha256
141 ),
142 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
143 }
144 }
145}
146impl std::error::Error for ArtifactError {
147 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148 match self {
149 Self::Io(source) => Some(source),
150 Self::Allocation(source) => Some(source),
151 _ => None,
152 }
153 }
154}
155impl From<io::Error> for ArtifactError {
156 fn from(source: io::Error) -> Self {
157 Self::Io(source)
158 }
159}
160
161pub fn hash_reader(
170 mut reader: impl Read,
171 max_bytes: u64,
172) -> Result<ArtifactIdentity, ArtifactError> {
173 let mut hasher = Sha256::new();
174 let bytes = visit_reader(&mut reader, max_bytes, |chunk| {
175 hasher.update(chunk);
176 Ok(())
177 })?;
178 Ok(ArtifactIdentity {
179 bytes,
180 sha256: Sha256Digest(hasher.finalize().into()),
181 })
182}
183
184pub fn verify_reader(
189 reader: impl Read,
190 max_bytes: u64,
191 expected: Sha256Digest,
192) -> Result<ArtifactIdentity, ArtifactError> {
193 let actual = hash_reader(reader, max_bytes)?;
194 if actual.sha256 != expected {
195 return Err(ArtifactError::DigestMismatch { expected, actual });
196 }
197 Ok(actual)
198}
199
200pub fn hash_file(path: &Path, max_bytes: u64) -> Result<ArtifactIdentity, ArtifactError> {
208 hash_reader(open_file(path, max_bytes)?, max_bytes)
209}
210
211pub fn read_file(path: &Path, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
219 read_reader(open_file(path, max_bytes as u64)?, max_bytes)
220}
221
222pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
230 let mut bytes = Vec::new();
231 visit_reader(&mut reader, max_bytes as u64, |chunk| {
232 bytes
233 .try_reserve_exact(chunk.len())
234 .map_err(ArtifactError::Allocation)?;
235 bytes.extend_from_slice(chunk);
236 Ok(())
237 })?;
238 Ok(bytes)
239}
240
241fn open_file(path: &Path, limit: u64) -> Result<File, ArtifactError> {
242 check_metadata(&std::fs::metadata(path)?, limit)?;
245 let file = File::open(path)?;
246 check_metadata(&file.metadata()?, limit)?;
247 Ok(file)
248}
249
250fn check_metadata(metadata: &std::fs::Metadata, limit: u64) -> Result<(), ArtifactError> {
251 if !metadata.is_file() {
252 return Err(ArtifactError::NotRegularFile);
253 }
254 if metadata.len() > limit {
255 return Err(ArtifactError::LimitExceeded { limit });
256 }
257 Ok(())
258}
259
260fn visit_reader(
261 reader: &mut impl Read,
262 limit: u64,
263 mut visit: impl FnMut(&[u8]) -> Result<(), ArtifactError>,
264) -> Result<u64, ArtifactError> {
265 let mut bytes = 0_u64;
266 let mut buffer = [0_u8; 16 * 1024];
267 loop {
268 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
269 let allowance = remaining.saturating_add(1).min(buffer.len());
270 let count = match reader.read(&mut buffer[..allowance]) {
271 Ok(count) => count,
272 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
273 Err(source) => return Err(source.into()),
274 };
275 if count == 0 {
276 return Ok(bytes);
277 }
278 if count as u64 > limit - bytes {
279 return Err(ArtifactError::LimitExceeded { limit });
280 }
281 visit(&buffer[..count])?;
282 bytes += count as u64;
283 }
284}