ic_host_artifacts/artifact/
mod.rs1use sha2::{Digest, Sha256};
9use std::{
10 fmt,
11 io::{self, Read},
12 str::FromStr,
13};
14
15mod chunks;
16mod copy;
17#[cfg(feature = "gzip")]
18mod gzip;
19mod hashing;
20mod matching;
21#[cfg(test)]
22mod test_support;
23#[cfg(test)]
24mod tests;
25mod writer;
26
27pub use chunks::{ChunkDigestError, chunk_digests};
28pub use copy::{CopyError, copy_reader};
29#[cfg(feature = "gzip")]
30pub use gzip::{GzipError, decode_gzip, encode_gzip, gzip_matches, hash_gzip, hash_gzip_or_raw};
31pub use hashing::HashingWriter;
32pub use matching::MatchingWriter;
33pub use writer::{BoundedWriter, WriterError};
34
35#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37pub struct Sha256Digest([u8; 32]);
38
39impl Sha256Digest {
40 #[must_use]
42 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
43 Self(bytes)
44 }
45
46 #[must_use]
48 pub const fn as_bytes(&self) -> &[u8; 32] {
49 &self.0
50 }
51
52 #[must_use]
54 pub fn compute(bytes: &[u8]) -> Self {
55 Self(Sha256::digest(bytes).into())
56 }
57}
58
59impl fmt::Display for Sha256Digest {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 for byte in self.0 {
62 write!(f, "{byte:02x}")?;
63 }
64 Ok(())
65 }
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum DigestParseError {
71 Length {
73 actual: usize,
75 },
76 Digit {
78 offset: usize,
80 },
81}
82
83impl fmt::Display for DigestParseError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 Self::Length { actual } => {
87 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
88 }
89 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
90 }
91 }
92}
93impl std::error::Error for DigestParseError {}
94
95impl FromStr for Sha256Digest {
96 type Err = DigestParseError;
97
98 fn from_str(text: &str) -> Result<Self, Self::Err> {
99 if text.len() != 64 {
100 return Err(DigestParseError::Length { actual: text.len() });
101 }
102 let mut bytes = [0; 32];
103 for (offset, digit) in text.bytes().enumerate() {
104 let nibble = match digit {
105 b'0'..=b'9' => digit - b'0',
106 b'a'..=b'f' => digit - b'a' + 10,
107 _ => return Err(DigestParseError::Digit { offset }),
108 };
109 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
110 }
111 Ok(Self(bytes))
112 }
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct ArtifactIdentity {
118 pub bytes: u64,
120 pub sha256: Sha256Digest,
122}
123
124impl ArtifactIdentity {
125 pub fn verify(self, expected: Sha256Digest) -> Result<Self, ArtifactError> {
133 if self.sha256 != expected {
134 return Err(ArtifactError::DigestMismatch {
135 expected,
136 actual: self,
137 });
138 }
139 Ok(self)
140 }
141}
142
143#[derive(Debug)]
145pub enum ArtifactError {
146 Io(io::Error),
148 NotRegularFile,
150 LimitExceeded {
152 limit: u64,
154 },
155 LengthMismatch {
157 expected: u64,
159 actual: u64,
161 },
162 DigestMismatch {
164 expected: Sha256Digest,
166 actual: ArtifactIdentity,
168 },
169 Allocation(std::collections::TryReserveError),
171}
172
173impl fmt::Display for ArtifactError {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 Self::Io(source) => write!(f, "artifact read failed: {source}"),
177 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
178 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
179 Self::LengthMismatch { expected, actual } => {
180 write!(f, "artifact has {actual} bytes, expected {expected}")
181 }
182 Self::DigestMismatch { expected, actual } => write!(
183 f,
184 "artifact SHA-256 is {}, expected {expected}",
185 actual.sha256
186 ),
187 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
188 }
189 }
190}
191impl std::error::Error for ArtifactError {
192 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
193 match self {
194 Self::Io(source) => Some(source),
195 Self::Allocation(source) => Some(source),
196 _ => None,
197 }
198 }
199}
200impl From<io::Error> for ArtifactError {
201 fn from(source: io::Error) -> Self {
202 Self::Io(source)
203 }
204}
205
206impl From<ArtifactError> for io::Error {
208 fn from(error: ArtifactError) -> Self {
209 match error {
210 ArtifactError::Io(source) => source,
211 other => Self::other(other),
212 }
213 }
214}
215
216pub fn hash_reader(
226 mut reader: impl Read,
227 max_bytes: u64,
228) -> Result<ArtifactIdentity, ArtifactError> {
229 let mut hasher = Sha256::new();
230 let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
231 hasher.update(chunk);
232 Ok(())
233 })?;
234 Ok(ArtifactIdentity {
235 bytes,
236 sha256: Sha256Digest(hasher.finalize().into()),
237 })
238}
239
240pub fn hash_exact_reader(
248 reader: impl Read,
249 expected_bytes: u64,
250) -> Result<ArtifactIdentity, ArtifactError> {
251 let identity = hash_reader(reader, expected_bytes)?;
252 if identity.bytes != expected_bytes {
253 return Err(ArtifactError::LengthMismatch {
254 expected: expected_bytes,
255 actual: identity.bytes,
256 });
257 }
258 Ok(identity)
259}
260
261pub fn verify_reader(
266 reader: impl Read,
267 max_bytes: u64,
268 expected: Sha256Digest,
269) -> Result<ArtifactIdentity, ArtifactError> {
270 hash_reader(reader, max_bytes)?.verify(expected)
271}
272
273pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
282 let mut bytes = Vec::new();
283 visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
284 reserve_bounded(&mut bytes, chunk.len(), max_bytes).map_err(ArtifactError::Allocation)?;
285 bytes.extend_from_slice(chunk);
286 Ok(())
287 })?;
288 Ok(bytes)
289}
290
291pub fn read_exact_reader(
299 reader: impl Read,
300 expected_bytes: usize,
301) -> Result<Vec<u8>, ArtifactError> {
302 let bytes = read_reader(reader, expected_bytes)?;
303 if bytes.len() != expected_bytes {
304 return Err(ArtifactError::LengthMismatch {
305 expected: expected_bytes as u64,
306 actual: bytes.len() as u64,
307 });
308 }
309 Ok(bytes)
310}
311
312fn reserve_bounded<T>(
315 items: &mut Vec<T>,
316 additional: usize,
317 limit: usize,
318) -> Result<(), std::collections::TryReserveError> {
319 let required = items.len() + additional;
320 if required > items.capacity() {
321 let capacity = items.capacity().saturating_mul(2).max(required).min(limit);
322 items.try_reserve_exact(capacity - items.len())?;
323 }
324 Ok(())
325}
326
327fn visit_reader<E: From<ArtifactError>>(
328 reader: &mut impl Read,
329 limit: u64,
330 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
331) -> Result<u64, E> {
332 let mut bytes = 0_u64;
333 let mut buffer = [0_u8; 16 * 1024];
334 loop {
335 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
336 let allowance = remaining.saturating_add(1).min(buffer.len());
337 let count = match reader.read(&mut buffer[..allowance]) {
338 Ok(count) => count,
339 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
340 Err(source) => return Err(ArtifactError::Io(source).into()),
341 };
342 if count > allowance {
343 return Err(ArtifactError::Io(io::Error::new(
344 io::ErrorKind::InvalidData,
345 "reader returned more bytes than its buffer can hold",
346 ))
347 .into());
348 }
349 if count == 0 {
350 return Ok(bytes);
351 }
352 if count as u64 > limit - bytes {
353 return Err(ArtifactError::LimitExceeded { limit }.into());
354 }
355 visit(&buffer[..count])?;
356 bytes += count as u64;
357 }
358}