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;
26mod writer;
27
28pub use chunks::{ChunkDigestError, chunk_digests};
29pub use copy::{CopyError, copy_reader};
30#[cfg(feature = "gzip")]
31pub use gzip::{GzipError, decode_gzip, encode_gzip, gzip_matches, hash_gzip};
32pub use hashing::HashingWriter;
33pub use matching::MatchingWriter;
34pub use writer::{BoundedWriter, WriterError};
35
36use buffer::reserve_bounded;
37
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub struct Sha256Digest([u8; 32]);
41
42impl Sha256Digest {
43 #[must_use]
45 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
46 Self(bytes)
47 }
48
49 #[must_use]
51 pub const fn as_bytes(&self) -> &[u8; 32] {
52 &self.0
53 }
54
55 #[must_use]
60 pub const fn hex_bytes(self) -> [u8; 64] {
61 const HEX: &[u8; 16] = b"0123456789abcdef";
62 let mut encoded = [0; 64];
63 let mut index = 0;
64 while index < self.0.len() {
65 let byte = self.0[index];
66 encoded[index * 2] = HEX[(byte >> 4) as usize];
67 encoded[index * 2 + 1] = HEX[(byte & 0x0f) as usize];
68 index += 1;
69 }
70 encoded
71 }
72
73 #[must_use]
75 pub fn compute(bytes: &[u8]) -> Self {
76 Self(Sha256::digest(bytes).into())
77 }
78}
79
80impl fmt::Display for Sha256Digest {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str(std::str::from_utf8(&self.hex_bytes()).expect("hex encoding is ASCII"))
83 }
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum DigestParseError {
89 Length {
91 actual: usize,
93 },
94 Digit {
96 offset: usize,
98 },
99}
100
101impl fmt::Display for DigestParseError {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 Self::Length { actual } => {
105 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
106 }
107 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
108 }
109 }
110}
111impl std::error::Error for DigestParseError {}
112
113impl FromStr for Sha256Digest {
114 type Err = DigestParseError;
115
116 fn from_str(text: &str) -> Result<Self, Self::Err> {
117 if text.len() != 64 {
118 return Err(DigestParseError::Length { actual: text.len() });
119 }
120 let mut bytes = [0; 32];
121 for (offset, digit) in text.bytes().enumerate() {
122 let nibble = match digit {
123 b'0'..=b'9' => digit - b'0',
124 b'a'..=b'f' => digit - b'a' + 10,
125 _ => return Err(DigestParseError::Digit { offset }),
126 };
127 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
128 }
129 Ok(Self(bytes))
130 }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct ArtifactIdentity {
136 pub bytes: u64,
138 pub sha256: Sha256Digest,
140}
141
142impl ArtifactIdentity {
143 pub fn verify(self, expected: Sha256Digest) -> Result<Self, ArtifactError> {
151 if self.sha256 != expected {
152 return Err(ArtifactError::DigestMismatch {
153 expected,
154 actual: self,
155 });
156 }
157 Ok(self)
158 }
159}
160
161#[derive(Debug)]
163pub enum ArtifactError {
164 Io(io::Error),
166 NotRegularFile,
168 LimitExceeded {
170 limit: u64,
172 },
173 LengthMismatch {
175 expected: u64,
177 actual: u64,
179 },
180 DigestMismatch {
182 expected: Sha256Digest,
184 actual: ArtifactIdentity,
186 },
187 Allocation(std::collections::TryReserveError),
189}
190
191impl fmt::Display for ArtifactError {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::Io(source) => write!(f, "artifact read failed: {source}"),
195 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
196 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
197 Self::LengthMismatch { expected, actual } => {
198 write!(f, "artifact has {actual} bytes, expected {expected}")
199 }
200 Self::DigestMismatch { expected, actual } => write!(
201 f,
202 "artifact SHA-256 is {}, expected {expected}",
203 actual.sha256
204 ),
205 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
206 }
207 }
208}
209impl std::error::Error for ArtifactError {
210 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
211 match self {
212 Self::Io(source) => Some(source),
213 Self::Allocation(source) => Some(source),
214 _ => None,
215 }
216 }
217}
218impl From<io::Error> for ArtifactError {
219 fn from(source: io::Error) -> Self {
220 Self::Io(source)
221 }
222}
223
224impl From<ArtifactError> for io::Error {
226 fn from(error: ArtifactError) -> Self {
227 match error {
228 ArtifactError::Io(source) => source,
229 other => Self::other(other),
230 }
231 }
232}
233
234pub fn hash_reader(
245 mut reader: impl Read,
246 max_bytes: u64,
247) -> Result<ArtifactIdentity, ArtifactError> {
248 let mut hasher = Sha256::new();
249 let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
250 hasher.update(chunk);
251 Ok(())
252 })?;
253 Ok(ArtifactIdentity {
254 bytes,
255 sha256: Sha256Digest(hasher.finalize().into()),
256 })
257}
258
259pub fn hash_exact_reader(
267 reader: impl Read,
268 expected_bytes: u64,
269) -> Result<ArtifactIdentity, ArtifactError> {
270 let identity = hash_reader(reader, expected_bytes)?;
271 if identity.bytes != expected_bytes {
272 return Err(ArtifactError::LengthMismatch {
273 expected: expected_bytes,
274 actual: identity.bytes,
275 });
276 }
277 Ok(identity)
278}
279
280pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
289 let mut bytes = Vec::new();
290 visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
291 reserve_bounded(&mut bytes, chunk.len(), max_bytes).map_err(ArtifactError::Allocation)?;
292 bytes.extend_from_slice(chunk);
293 Ok(())
294 })?;
295 Ok(bytes)
296}
297
298pub fn read_exact_reader(
306 reader: impl Read,
307 expected_bytes: usize,
308) -> Result<Vec<u8>, ArtifactError> {
309 let bytes = read_reader(reader, expected_bytes)?;
310 if bytes.len() != expected_bytes {
311 return Err(ArtifactError::LengthMismatch {
312 expected: expected_bytes as u64,
313 actual: bytes.len() as u64,
314 });
315 }
316 Ok(bytes)
317}
318
319fn visit_reader<E: From<ArtifactError>>(
320 reader: &mut impl Read,
321 limit: u64,
322 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
323) -> Result<u64, E> {
324 let mut bytes = 0_u64;
325 let mut buffer = [0_u8; 16 * 1024];
326 loop {
327 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
328 let allowance = remaining.saturating_add(1).min(buffer.len());
329 let count = match reader.read(&mut buffer[..allowance]) {
330 Ok(count) => count,
331 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
332 Err(source) => return Err(ArtifactError::Io(source).into()),
333 };
334 if count > allowance {
335 return Err(ArtifactError::Io(io::Error::new(
336 io::ErrorKind::InvalidData,
337 "reader returned more bytes than its buffer can hold",
338 ))
339 .into());
340 }
341 if count == 0 {
342 return Ok(bytes);
343 }
344 if count as u64 > limit - bytes {
345 return Err(ArtifactError::LimitExceeded { limit }.into());
346 }
347 visit(&buffer[..count])?;
348 bytes += count as u64;
349 }
350}