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 tests;
23mod writer;
24
25pub use chunks::{ChunkDigestError, chunk_digests};
26pub use copy::{CopyError, copy_reader};
27#[cfg(feature = "gzip")]
28pub use gzip::{GzipError, decode_gzip, encode_gzip, gzip_matches, hash_gzip, hash_gzip_or_raw};
29pub use hashing::HashingWriter;
30pub use matching::MatchingWriter;
31pub use writer::{BoundedWriter, WriterError};
32
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35pub struct Sha256Digest([u8; 32]);
36
37impl Sha256Digest {
38 #[must_use]
40 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
41 Self(bytes)
42 }
43
44 #[must_use]
46 pub const fn as_bytes(&self) -> &[u8; 32] {
47 &self.0
48 }
49
50 #[must_use]
52 pub fn compute(bytes: &[u8]) -> Self {
53 Self(Sha256::digest(bytes).into())
54 }
55}
56
57impl fmt::Display for Sha256Digest {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 for byte in self.0 {
60 write!(f, "{byte:02x}")?;
61 }
62 Ok(())
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum DigestParseError {
69 Length {
71 actual: usize,
73 },
74 Digit {
76 offset: usize,
78 },
79}
80
81impl fmt::Display for DigestParseError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Length { actual } => {
85 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
86 }
87 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
88 }
89 }
90}
91impl std::error::Error for DigestParseError {}
92
93impl FromStr for Sha256Digest {
94 type Err = DigestParseError;
95
96 fn from_str(text: &str) -> Result<Self, Self::Err> {
97 if text.len() != 64 {
98 return Err(DigestParseError::Length { actual: text.len() });
99 }
100 let mut bytes = [0; 32];
101 for (offset, digit) in text.bytes().enumerate() {
102 let nibble = match digit {
103 b'0'..=b'9' => digit - b'0',
104 b'a'..=b'f' => digit - b'a' + 10,
105 _ => return Err(DigestParseError::Digit { offset }),
106 };
107 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
108 }
109 Ok(Self(bytes))
110 }
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct ArtifactIdentity {
116 pub bytes: u64,
118 pub sha256: Sha256Digest,
120}
121
122impl ArtifactIdentity {
123 pub fn verify(self, expected: Sha256Digest) -> Result<Self, ArtifactError> {
131 if self.sha256 != expected {
132 return Err(ArtifactError::DigestMismatch {
133 expected,
134 actual: self,
135 });
136 }
137 Ok(self)
138 }
139}
140
141#[derive(Debug)]
143pub enum ArtifactError {
144 Io(io::Error),
146 NotRegularFile,
148 LimitExceeded {
150 limit: u64,
152 },
153 DigestMismatch {
155 expected: Sha256Digest,
157 actual: ArtifactIdentity,
159 },
160 Allocation(std::collections::TryReserveError),
162}
163
164impl fmt::Display for ArtifactError {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 Self::Io(source) => write!(f, "artifact read failed: {source}"),
168 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
169 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
170 Self::DigestMismatch { expected, actual } => write!(
171 f,
172 "artifact SHA-256 is {}, expected {expected}",
173 actual.sha256
174 ),
175 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
176 }
177 }
178}
179impl std::error::Error for ArtifactError {
180 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
181 match self {
182 Self::Io(source) => Some(source),
183 Self::Allocation(source) => Some(source),
184 _ => None,
185 }
186 }
187}
188impl From<io::Error> for ArtifactError {
189 fn from(source: io::Error) -> Self {
190 Self::Io(source)
191 }
192}
193
194impl From<ArtifactError> for io::Error {
196 fn from(error: ArtifactError) -> Self {
197 match error {
198 ArtifactError::Io(source) => source,
199 other => Self::other(other),
200 }
201 }
202}
203
204pub fn hash_reader(
214 mut reader: impl Read,
215 max_bytes: u64,
216) -> Result<ArtifactIdentity, ArtifactError> {
217 let mut hasher = Sha256::new();
218 let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
219 hasher.update(chunk);
220 Ok(())
221 })?;
222 Ok(ArtifactIdentity {
223 bytes,
224 sha256: Sha256Digest(hasher.finalize().into()),
225 })
226}
227
228pub fn verify_reader(
233 reader: impl Read,
234 max_bytes: u64,
235 expected: Sha256Digest,
236) -> Result<ArtifactIdentity, ArtifactError> {
237 hash_reader(reader, max_bytes)?.verify(expected)
238}
239
240pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
249 let mut bytes = Vec::new();
250 visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
251 reserve_bounded(&mut bytes, chunk.len(), max_bytes).map_err(ArtifactError::Allocation)?;
252 bytes.extend_from_slice(chunk);
253 Ok(())
254 })?;
255 Ok(bytes)
256}
257
258fn reserve_bounded<T>(
261 items: &mut Vec<T>,
262 additional: usize,
263 limit: usize,
264) -> Result<(), std::collections::TryReserveError> {
265 let required = items.len() + additional;
266 if required > items.capacity() {
267 let capacity = items.capacity().saturating_mul(2).max(required).min(limit);
268 items.try_reserve_exact(capacity - items.len())?;
269 }
270 Ok(())
271}
272
273fn visit_reader<E: From<ArtifactError>>(
274 reader: &mut impl Read,
275 limit: u64,
276 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
277) -> Result<u64, E> {
278 let mut bytes = 0_u64;
279 let mut buffer = [0_u8; 16 * 1024];
280 loop {
281 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
282 let allowance = remaining.saturating_add(1).min(buffer.len());
283 let count = match reader.read(&mut buffer[..allowance]) {
284 Ok(count) => count,
285 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
286 Err(source) => return Err(ArtifactError::Io(source).into()),
287 };
288 if count > allowance {
289 return Err(ArtifactError::Io(io::Error::new(
290 io::ErrorKind::InvalidData,
291 "reader returned more bytes than its buffer can hold",
292 ))
293 .into());
294 }
295 if count == 0 {
296 return Ok(bytes);
297 }
298 if count as u64 > limit - bytes {
299 return Err(ArtifactError::LimitExceeded { limit }.into());
300 }
301 visit(&buffer[..count])?;
302 bytes += count as u64;
303 }
304}