1use sha2::{Digest, Sha256};
9use std::{
10 fmt,
11 fs::File,
12 io::{self, Read},
13 path::Path,
14 str::FromStr,
15};
16
17mod copy;
18mod gzip;
19#[cfg(unix)]
20mod no_follow;
21#[cfg(test)]
22mod tests;
23mod writer;
24
25pub use copy::{CopyError, copy_reader};
26pub use gzip::{GzipError, decode_gzip};
27#[cfg(unix)]
28pub use no_follow::read_file_no_follow;
29pub use writer::{BoundedWriter, WriterError};
30
31#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
33pub struct Sha256Digest([u8; 32]);
34
35impl Sha256Digest {
36 #[must_use]
38 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
39 Self(bytes)
40 }
41
42 #[must_use]
44 pub const fn as_bytes(&self) -> &[u8; 32] {
45 &self.0
46 }
47
48 #[must_use]
50 pub fn compute(bytes: &[u8]) -> Self {
51 Self(Sha256::digest(bytes).into())
52 }
53}
54
55impl fmt::Display for Sha256Digest {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 for byte in self.0 {
58 write!(f, "{byte:02x}")?;
59 }
60 Ok(())
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum DigestParseError {
67 Length {
69 actual: usize,
71 },
72 Digit {
74 offset: usize,
76 },
77}
78
79impl fmt::Display for DigestParseError {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::Length { actual } => {
83 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
84 }
85 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
86 }
87 }
88}
89impl std::error::Error for DigestParseError {}
90
91impl FromStr for Sha256Digest {
92 type Err = DigestParseError;
93
94 fn from_str(text: &str) -> Result<Self, Self::Err> {
95 if text.len() != 64 {
96 return Err(DigestParseError::Length { actual: text.len() });
97 }
98 let mut bytes = [0; 32];
99 for (offset, digit) in text.bytes().enumerate() {
100 let nibble = match digit {
101 b'0'..=b'9' => digit - b'0',
102 b'a'..=b'f' => digit - b'a' + 10,
103 _ => return Err(DigestParseError::Digit { offset }),
104 };
105 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
106 }
107 Ok(Self(bytes))
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct ArtifactIdentity {
114 pub bytes: u64,
116 pub sha256: Sha256Digest,
118}
119
120#[derive(Debug)]
122pub enum ArtifactError {
123 Io(io::Error),
125 NotRegularFile,
127 LimitExceeded {
129 limit: u64,
131 },
132 DigestMismatch {
134 expected: Sha256Digest,
136 actual: ArtifactIdentity,
138 },
139 Allocation(std::collections::TryReserveError),
141}
142
143impl fmt::Display for ArtifactError {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 Self::Io(source) => write!(f, "artifact read failed: {source}"),
147 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
148 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
149 Self::DigestMismatch { expected, actual } => write!(
150 f,
151 "artifact SHA-256 is {}, expected {expected}",
152 actual.sha256
153 ),
154 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
155 }
156 }
157}
158impl std::error::Error for ArtifactError {
159 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
160 match self {
161 Self::Io(source) => Some(source),
162 Self::Allocation(source) => Some(source),
163 _ => None,
164 }
165 }
166}
167impl From<io::Error> for ArtifactError {
168 fn from(source: io::Error) -> Self {
169 Self::Io(source)
170 }
171}
172
173pub fn hash_reader(
183 mut reader: impl Read,
184 max_bytes: u64,
185) -> Result<ArtifactIdentity, ArtifactError> {
186 let mut hasher = Sha256::new();
187 let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
188 hasher.update(chunk);
189 Ok(())
190 })?;
191 Ok(ArtifactIdentity {
192 bytes,
193 sha256: Sha256Digest(hasher.finalize().into()),
194 })
195}
196
197pub fn verify_reader(
202 reader: impl Read,
203 max_bytes: u64,
204 expected: Sha256Digest,
205) -> Result<ArtifactIdentity, ArtifactError> {
206 let actual = hash_reader(reader, max_bytes)?;
207 if actual.sha256 != expected {
208 return Err(ArtifactError::DigestMismatch { expected, actual });
209 }
210 Ok(actual)
211}
212
213pub fn hash_file(path: &Path, max_bytes: u64) -> Result<ArtifactIdentity, ArtifactError> {
221 hash_reader(open_file(path, max_bytes)?, max_bytes)
222}
223
224pub fn read_file(path: &Path, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
232 read_reader(open_file(path, max_bytes as u64)?, max_bytes)
233}
234
235pub fn read_opened_file(file: File, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
251 check_metadata(&file.metadata()?, max_bytes as u64)?;
252 read_reader(file, max_bytes)
253}
254
255pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
264 let mut bytes = Vec::new();
265 visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
266 bytes
267 .try_reserve_exact(chunk.len())
268 .map_err(ArtifactError::Allocation)?;
269 bytes.extend_from_slice(chunk);
270 Ok(())
271 })?;
272 Ok(bytes)
273}
274
275fn open_file(path: &Path, limit: u64) -> Result<File, ArtifactError> {
276 check_metadata(&std::fs::metadata(path)?, limit)?;
279 let file = File::open(path)?;
280 check_metadata(&file.metadata()?, limit)?;
281 Ok(file)
282}
283
284fn check_metadata(metadata: &std::fs::Metadata, limit: u64) -> Result<(), ArtifactError> {
285 if !metadata.is_file() {
286 return Err(ArtifactError::NotRegularFile);
287 }
288 if metadata.len() > limit {
289 return Err(ArtifactError::LimitExceeded { limit });
290 }
291 Ok(())
292}
293
294fn visit_reader<E: From<ArtifactError>>(
295 reader: &mut impl Read,
296 limit: u64,
297 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
298) -> Result<u64, E> {
299 let mut bytes = 0_u64;
300 let mut buffer = [0_u8; 16 * 1024];
301 loop {
302 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
303 let allowance = remaining.saturating_add(1).min(buffer.len());
304 let count = match reader.read(&mut buffer[..allowance]) {
305 Ok(count) => count,
306 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
307 Err(source) => return Err(ArtifactError::Io(source).into()),
308 };
309 if count > allowance {
310 return Err(ArtifactError::Io(io::Error::new(
311 io::ErrorKind::InvalidData,
312 "reader returned more bytes than its buffer can hold",
313 ))
314 .into());
315 }
316 if count == 0 {
317 return Ok(bytes);
318 }
319 if count as u64 > limit - bytes {
320 return Err(ArtifactError::LimitExceeded { limit }.into());
321 }
322 visit(&buffer[..count])?;
323 bytes += count as u64;
324 }
325}