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