1use sha2::{Digest, Sha256};
8use std::{
9 fmt,
10 fs::File,
11 io::{self, Read},
12 path::Path,
13 str::FromStr,
14};
15
16mod gzip;
17#[cfg(unix)]
18mod no_follow;
19#[cfg(test)]
20mod tests;
21
22pub use gzip::{GzipError, decode_gzip};
23#[cfg(unix)]
24pub use no_follow::read_file_no_follow;
25
26#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28pub struct Sha256Digest([u8; 32]);
29
30impl Sha256Digest {
31 #[must_use]
33 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
34 Self(bytes)
35 }
36
37 #[must_use]
39 pub const fn as_bytes(&self) -> &[u8; 32] {
40 &self.0
41 }
42
43 #[must_use]
45 pub fn compute(bytes: &[u8]) -> Self {
46 Self(Sha256::digest(bytes).into())
47 }
48}
49
50impl fmt::Display for Sha256Digest {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 for byte in self.0 {
53 write!(f, "{byte:02x}")?;
54 }
55 Ok(())
56 }
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum DigestParseError {
62 Length {
64 actual: usize,
66 },
67 Digit {
69 offset: usize,
71 },
72}
73
74impl fmt::Display for DigestParseError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::Length { actual } => {
78 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
79 }
80 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
81 }
82 }
83}
84impl std::error::Error for DigestParseError {}
85
86impl FromStr for Sha256Digest {
87 type Err = DigestParseError;
88
89 fn from_str(text: &str) -> Result<Self, Self::Err> {
90 if text.len() != 64 {
91 return Err(DigestParseError::Length { actual: text.len() });
92 }
93 let mut bytes = [0; 32];
94 for (offset, digit) in text.bytes().enumerate() {
95 let nibble = match digit {
96 b'0'..=b'9' => digit - b'0',
97 b'a'..=b'f' => digit - b'a' + 10,
98 _ => return Err(DigestParseError::Digit { offset }),
99 };
100 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
101 }
102 Ok(Self(bytes))
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct ArtifactIdentity {
109 pub bytes: u64,
111 pub sha256: Sha256Digest,
113}
114
115#[derive(Debug)]
117pub enum ArtifactError {
118 Io(io::Error),
120 NotRegularFile,
122 LimitExceeded {
124 limit: u64,
126 },
127 DigestMismatch {
129 expected: Sha256Digest,
131 actual: ArtifactIdentity,
133 },
134 Allocation(std::collections::TryReserveError),
136}
137
138impl fmt::Display for ArtifactError {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::Io(source) => write!(f, "artifact read failed: {source}"),
142 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
143 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
144 Self::DigestMismatch { expected, actual } => write!(
145 f,
146 "artifact SHA-256 is {}, expected {expected}",
147 actual.sha256
148 ),
149 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
150 }
151 }
152}
153impl std::error::Error for ArtifactError {
154 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
155 match self {
156 Self::Io(source) => Some(source),
157 Self::Allocation(source) => Some(source),
158 _ => None,
159 }
160 }
161}
162impl From<io::Error> for ArtifactError {
163 fn from(source: io::Error) -> Self {
164 Self::Io(source)
165 }
166}
167
168pub fn hash_reader(
177 mut reader: impl Read,
178 max_bytes: u64,
179) -> Result<ArtifactIdentity, ArtifactError> {
180 let mut hasher = Sha256::new();
181 let bytes = visit_reader(&mut reader, max_bytes, |chunk| {
182 hasher.update(chunk);
183 Ok(())
184 })?;
185 Ok(ArtifactIdentity {
186 bytes,
187 sha256: Sha256Digest(hasher.finalize().into()),
188 })
189}
190
191pub fn verify_reader(
196 reader: impl Read,
197 max_bytes: u64,
198 expected: Sha256Digest,
199) -> Result<ArtifactIdentity, ArtifactError> {
200 let actual = hash_reader(reader, max_bytes)?;
201 if actual.sha256 != expected {
202 return Err(ArtifactError::DigestMismatch { expected, actual });
203 }
204 Ok(actual)
205}
206
207pub fn hash_file(path: &Path, max_bytes: u64) -> Result<ArtifactIdentity, ArtifactError> {
215 hash_reader(open_file(path, max_bytes)?, max_bytes)
216}
217
218pub fn read_file(path: &Path, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
226 read_reader(open_file(path, max_bytes as u64)?, max_bytes)
227}
228
229pub fn read_opened_file(file: File, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
245 check_metadata(&file.metadata()?, max_bytes as u64)?;
246 read_reader(file, max_bytes)
247}
248
249pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
257 let mut bytes = Vec::new();
258 visit_reader(&mut reader, max_bytes as u64, |chunk| {
259 bytes
260 .try_reserve_exact(chunk.len())
261 .map_err(ArtifactError::Allocation)?;
262 bytes.extend_from_slice(chunk);
263 Ok(())
264 })?;
265 Ok(bytes)
266}
267
268fn open_file(path: &Path, limit: u64) -> Result<File, ArtifactError> {
269 check_metadata(&std::fs::metadata(path)?, limit)?;
272 let file = File::open(path)?;
273 check_metadata(&file.metadata()?, limit)?;
274 Ok(file)
275}
276
277fn check_metadata(metadata: &std::fs::Metadata, limit: u64) -> Result<(), ArtifactError> {
278 if !metadata.is_file() {
279 return Err(ArtifactError::NotRegularFile);
280 }
281 if metadata.len() > limit {
282 return Err(ArtifactError::LimitExceeded { limit });
283 }
284 Ok(())
285}
286
287fn visit_reader(
288 reader: &mut impl Read,
289 limit: u64,
290 mut visit: impl FnMut(&[u8]) -> Result<(), ArtifactError>,
291) -> Result<u64, ArtifactError> {
292 let mut bytes = 0_u64;
293 let mut buffer = [0_u8; 16 * 1024];
294 loop {
295 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
296 let allowance = remaining.saturating_add(1).min(buffer.len());
297 let count = match reader.read(&mut buffer[..allowance]) {
298 Ok(count) => count,
299 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
300 Err(source) => return Err(source.into()),
301 };
302 if count == 0 {
303 return Ok(bytes);
304 }
305 if count as u64 > limit - bytes {
306 return Err(ArtifactError::LimitExceeded { limit });
307 }
308 visit(&buffer[..count])?;
309 bytes += count as u64;
310 }
311}