ic_host_artifacts/artifact/
mod.rs1use sha2::{Digest, Sha256};
9use std::{
10 fmt,
11 io::{self, Read},
12 str::FromStr,
13};
14
15mod copy;
16#[cfg(feature = "gzip")]
17mod gzip;
18#[cfg(test)]
19mod tests;
20mod writer;
21
22pub use copy::{CopyError, copy_reader};
23#[cfg(feature = "gzip")]
24pub use gzip::{GzipError, decode_gzip};
25pub use writer::{BoundedWriter, WriterError};
26
27#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29pub struct Sha256Digest([u8; 32]);
30
31impl Sha256Digest {
32 #[must_use]
34 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
35 Self(bytes)
36 }
37
38 #[must_use]
40 pub const fn as_bytes(&self) -> &[u8; 32] {
41 &self.0
42 }
43
44 #[must_use]
46 pub fn compute(bytes: &[u8]) -> Self {
47 Self(Sha256::digest(bytes).into())
48 }
49}
50
51impl fmt::Display for Sha256Digest {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 for byte in self.0 {
54 write!(f, "{byte:02x}")?;
55 }
56 Ok(())
57 }
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum DigestParseError {
63 Length {
65 actual: usize,
67 },
68 Digit {
70 offset: usize,
72 },
73}
74
75impl fmt::Display for DigestParseError {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 Self::Length { actual } => {
79 write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
80 }
81 Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
82 }
83 }
84}
85impl std::error::Error for DigestParseError {}
86
87impl FromStr for Sha256Digest {
88 type Err = DigestParseError;
89
90 fn from_str(text: &str) -> Result<Self, Self::Err> {
91 if text.len() != 64 {
92 return Err(DigestParseError::Length { actual: text.len() });
93 }
94 let mut bytes = [0; 32];
95 for (offset, digit) in text.bytes().enumerate() {
96 let nibble = match digit {
97 b'0'..=b'9' => digit - b'0',
98 b'a'..=b'f' => digit - b'a' + 10,
99 _ => return Err(DigestParseError::Digit { offset }),
100 };
101 bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
102 }
103 Ok(Self(bytes))
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct ArtifactIdentity {
110 pub bytes: u64,
112 pub sha256: Sha256Digest,
114}
115
116#[derive(Debug)]
118pub enum ArtifactError {
119 Io(io::Error),
121 NotRegularFile,
123 LimitExceeded {
125 limit: u64,
127 },
128 DigestMismatch {
130 expected: Sha256Digest,
132 actual: ArtifactIdentity,
134 },
135 Allocation(std::collections::TryReserveError),
137}
138
139impl fmt::Display for ArtifactError {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 match self {
142 Self::Io(source) => write!(f, "artifact read failed: {source}"),
143 Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
144 Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
145 Self::DigestMismatch { expected, actual } => write!(
146 f,
147 "artifact SHA-256 is {}, expected {expected}",
148 actual.sha256
149 ),
150 Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
151 }
152 }
153}
154impl std::error::Error for ArtifactError {
155 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156 match self {
157 Self::Io(source) => Some(source),
158 Self::Allocation(source) => Some(source),
159 _ => None,
160 }
161 }
162}
163impl From<io::Error> for ArtifactError {
164 fn from(source: io::Error) -> Self {
165 Self::Io(source)
166 }
167}
168
169pub fn hash_reader(
179 mut reader: impl Read,
180 max_bytes: u64,
181) -> Result<ArtifactIdentity, ArtifactError> {
182 let mut hasher = Sha256::new();
183 let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
184 hasher.update(chunk);
185 Ok(())
186 })?;
187 Ok(ArtifactIdentity {
188 bytes,
189 sha256: Sha256Digest(hasher.finalize().into()),
190 })
191}
192
193pub fn verify_reader(
198 reader: impl Read,
199 max_bytes: u64,
200 expected: Sha256Digest,
201) -> Result<ArtifactIdentity, ArtifactError> {
202 let actual = hash_reader(reader, max_bytes)?;
203 if actual.sha256 != expected {
204 return Err(ArtifactError::DigestMismatch { expected, actual });
205 }
206 Ok(actual)
207}
208
209pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
218 let mut bytes = Vec::new();
219 visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
220 bytes
221 .try_reserve_exact(chunk.len())
222 .map_err(ArtifactError::Allocation)?;
223 bytes.extend_from_slice(chunk);
224 Ok(())
225 })?;
226 Ok(bytes)
227}
228
229fn visit_reader<E: From<ArtifactError>>(
230 reader: &mut impl Read,
231 limit: u64,
232 mut visit: impl FnMut(&[u8]) -> Result<(), E>,
233) -> Result<u64, E> {
234 let mut bytes = 0_u64;
235 let mut buffer = [0_u8; 16 * 1024];
236 loop {
237 let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
238 let allowance = remaining.saturating_add(1).min(buffer.len());
239 let count = match reader.read(&mut buffer[..allowance]) {
240 Ok(count) => count,
241 Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
242 Err(source) => return Err(ArtifactError::Io(source).into()),
243 };
244 if count > allowance {
245 return Err(ArtifactError::Io(io::Error::new(
246 io::ErrorKind::InvalidData,
247 "reader returned more bytes than its buffer can hold",
248 ))
249 .into());
250 }
251 if count == 0 {
252 return Ok(bytes);
253 }
254 if count as u64 > limit - bytes {
255 return Err(ArtifactError::LimitExceeded { limit }.into());
256 }
257 visit(&buffer[..count])?;
258 bytes += count as u64;
259 }
260}