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