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