ic_host_artifacts/artifact/
mod.rs1use sha2::{Digest, Sha256};
9use std::{
10 fmt,
11 io::{self, Read},
12 str::FromStr,
13};
14
15pub mod buffer;
16mod chunks;
17mod copy;
18#[cfg(feature = "gzip")]
19mod gzip;
20mod hashing;
21mod matching;
22#[cfg(test)]
23mod tests;
24pub mod tree;
25mod writer;
26
27pub use chunks::{ChunkDigestError, chunk_digests};
28pub use copy::{CopyError, copy_reader};
29#[cfg(feature = "gzip")]
30pub use gzip::{
31 GzipError, GzipReadError, copy_gzip, decode_gzip, encode_gzip, gzip_matches, hash_gzip,
32 with_gzip_reader,
33};
34pub use hashing::HashingWriter;
35pub use matching::MatchingWriter;
36pub use writer::{BoundedWriter, WriterError};
37
38use buffer::reserve_bounded;
39
40#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub struct Sha256Digest([u8; 32]);
43
44impl Sha256Digest {
45 #[must_use]
47 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
48 Self(bytes)
49 }
50
51 #[must_use]
53 pub const fn as_bytes(&self) -> &[u8; 32] {
54 &self.0
55 }
56
57 #[must_use]
62 pub const fn hex_bytes(self) -> [u8; 64] {
63 const HEX: &[u8; 16] = b"0123456789abcdef";
64 let mut encoded = [0; 64];
65 let mut index = 0;
66 while index < self.0.len() {
67 let byte = self.0[index];
68 encoded[index * 2] = HEX[(byte >> 4) as usize];
69 encoded[index * 2 + 1] = HEX[(byte & 0x0f) as usize];
70 index += 1;
71 }
72 encoded
73 }
74
75 #[must_use]
77 pub fn compute(bytes: &[u8]) -> Self {
78 Self(Sha256::digest(bytes).into())
79 }
80}
81
82impl fmt::Display for Sha256Digest {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.write_str(std::str::from_utf8(&self.hex_bytes()).expect("hex encoding is ASCII"))
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum DigestParseError {
91 Length {
93 actual: usize,
95 },
96 Digit {
98 offset: usize,
100 },
101}
102
103impl fmt::Display for DigestParseError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Length { actual } => {
107 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
108 }
109 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
110 }
111 }
112}
113impl std::error::Error for DigestParseError {}
114
115impl FromStr for Sha256Digest {
116 type Err = DigestParseError;
117
118 fn from_str(text: &str) -> Result<Self, Self::Err> {
119 if text.len() != 64 {
120 return Err(DigestParseError::Length { actual: text.len() });
121 }
122 let mut bytes = [0; 32];
123 for (offset, digit) in text.bytes().enumerate() {
124 let nibble = match digit {
125 b'0'..=b'9' => digit - b'0',
126 b'a'..=b'f' => digit - b'a' + 10,
127 _ => return Err(DigestParseError::Digit { offset }),
128 };
129 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
130 }
131 Ok(Self(bytes))
132 }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub struct ArtifactIdentity {
138 pub bytes: u64,
140 pub sha256: Sha256Digest,
142}
143
144impl ArtifactIdentity {
145 pub fn verify(self, expected: Sha256Digest) -> Result<Self, ArtifactError> {
153 if self.sha256 != expected {
154 return Err(ArtifactError::DigestMismatch {
155 expected,
156 actual: self,
157 });
158 }
159 Ok(self)
160 }
161}
162
163#[derive(Debug)]
165pub enum ArtifactError {
166 Io(io::Error),
168 NotRegularFile,
170 LimitExceeded {
172 limit: u64,
174 },
175 LengthMismatch {
177 expected: u64,
179 actual: u64,
181 },
182 DigestMismatch {
184 expected: Sha256Digest,
186 actual: ArtifactIdentity,
188 },
189 Allocation(std::collections::TryReserveError),
191}
192
193impl fmt::Display for ArtifactError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 Self::Io(source) => write!(f, "artifact read failed: {source}"),
197 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
198 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
199 Self::LengthMismatch { expected, actual } => {
200 write!(f, "artifact has {actual} bytes, expected {expected}")
201 }
202 Self::DigestMismatch { expected, actual } => write!(
203 f,
204 "artifact SHA-256 is {}, expected {expected}",
205 actual.sha256
206 ),
207 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
208 }
209 }
210}
211impl std::error::Error for ArtifactError {
212 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
213 match self {
214 Self::Io(source) => Some(source),
215 Self::Allocation(source) => Some(source),
216 _ => None,
217 }
218 }
219}
220impl From<io::Error> for ArtifactError {
221 fn from(source: io::Error) -> Self {
222 Self::Io(source)
223 }
224}
225
226impl From<ArtifactError> for io::Error {
228 fn from(error: ArtifactError) -> Self {
229 match error {
230 ArtifactError::Io(source) => source,
231 other => Self::other(other),
232 }
233 }
234}
235
236pub fn hash_reader(
247 mut reader: impl Read,
248 max_bytes: u64,
249) -> Result<ArtifactIdentity, ArtifactError> {
250 let mut hasher = Sha256::new();
251 let bytes =
252 visit_reader::<ArtifactError>(&mut reader, &mut [0; 16 * 1024], max_bytes, |chunk| {
253 hasher.update(chunk);
254 Ok(())
255 })?;
256 Ok(ArtifactIdentity {
257 bytes,
258 sha256: Sha256Digest(hasher.finalize().into()),
259 })
260}
261
262pub fn hash_exact_reader(
270 reader: impl Read,
271 expected_bytes: u64,
272) -> Result<ArtifactIdentity, ArtifactError> {
273 let identity = hash_reader(reader, expected_bytes)?;
274 if identity.bytes != expected_bytes {
275 return Err(ArtifactError::LengthMismatch {
276 expected: expected_bytes,
277 actual: identity.bytes,
278 });
279 }
280 Ok(identity)
281}
282
283pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
292 let mut bytes = Vec::new();
293 visit_reader::<ArtifactError>(
294 &mut reader,
295 &mut [0; 16 * 1024],
296 max_bytes as u64,
297 |chunk| {
298 reserve_bounded(&mut bytes, chunk.len(), max_bytes)
299 .map_err(ArtifactError::Allocation)?;
300 bytes.extend_from_slice(chunk);
301 Ok(())
302 },
303 )?;
304 Ok(bytes)
305}
306
307pub fn read_exact_reader(
315 reader: impl Read,
316 expected_bytes: usize,
317) -> Result<Vec<u8>, ArtifactError> {
318 let bytes = read_reader(reader, expected_bytes)?;
319 if bytes.len() != expected_bytes {
320 return Err(ArtifactError::LengthMismatch {
321 expected: expected_bytes as u64,
322 actual: bytes.len() as u64,
323 });
324 }
325 Ok(bytes)
326}
327
328pub fn visit_reader<E: From<ArtifactError>>(
341 reader: &mut impl Read,
342 buffer: &mut [u8],
343 limit: u64,
344 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
345) -> Result<u64, E> {
346 let mut bytes = 0_u64;
347 if buffer.is_empty() {
348 return Err(ArtifactError::Io(io::Error::new(
349 io::ErrorKind::InvalidInput,
350 "stream scratch buffer must not be empty",
351 ))
352 .into());
353 }
354 loop {
355 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
356 let allowance = remaining.saturating_add(1).min(buffer.len());
357 let count = match reader.read(&mut buffer[..allowance]) {
358 Ok(count) => count,
359 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
360 Err(source) => return Err(ArtifactError::Io(source).into()),
361 };
362 if count > allowance {
363 return Err(ArtifactError::Io(io::Error::new(
364 io::ErrorKind::InvalidData,
365 "reader returned more bytes than its buffer can hold",
366 ))
367 .into());
368 }
369 if count == 0 {
370 return Ok(bytes);
371 }
372 if count as u64 > limit - bytes {
373 return Err(ArtifactError::LimitExceeded { limit }.into());
374 }
375 visit(&buffer[..count])?;
376 bytes += count as u64;
377 }
378}