ic_host_tools/artifact/gzip/mod.rs
1//! Bounded single-member gzip decoding shared by artifact and archive callers.
2
3#[cfg(test)]
4mod tests;
5
6use super::{ArtifactError, read_reader};
7use std::fmt;
8
9/// A gzip input, decoding or complete-consumption failure.
10#[derive(Debug)]
11pub enum GzipError {
12 /// Complete compressed input exceeds its allowance before decoding.
13 InputLimit {
14 /// Observed compressed byte count.
15 actual: usize,
16 /// Maximum compressed bytes permitted by the caller.
17 limit: usize,
18 },
19 /// Gzip decoding, payload integrity, decoded size or allocation failed.
20 Decode(ArtifactError),
21 /// Bytes follow the first member, including another gzip member.
22 TrailingData,
23}
24
25impl fmt::Display for GzipError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::InputLimit { actual, limit } => {
29 write!(f, "compressed input has {actual} bytes, exceeding {limit}")
30 }
31 Self::Decode(_) => f.write_str("bounded gzip decoding failed"),
32 Self::TrailingData => f.write_str("bytes follow the single gzip member"),
33 }
34 }
35}
36
37impl std::error::Error for GzipError {
38 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39 match self {
40 Self::Decode(source) => Some(source),
41 _ => None,
42 }
43 }
44}
45
46/// Decode exactly one complete gzip member into bounded, fallible storage.
47///
48/// Checks the complete compressed input allowance before parsing. Independently
49/// bounds decoded bytes, checks payload CRC and length, and rejects concatenated
50/// members or any trailing bytes. A valid empty member is accepted with a zero
51/// decoded allowance. Input remains unchanged on every return path.
52///
53/// Compressed and decoded bytes may be resident together. Callers own limits,
54/// digest admission and payload interpretation: decoding establishes neither
55/// Wasm validity nor an admitted executable identity. Verify downloaded archive
56/// digests before decoding, as [`crate::archive::extract_tar_gz`] does.
57///
58/// # Errors
59/// Returns typed input overflow, gzip/read/integrity, decoded overflow,
60/// allocation or trailing-data failures. No partial decoded bytes are returned.
61pub fn decode_gzip(
62 bytes: &[u8],
63 max_compressed_bytes: usize,
64 max_decoded_bytes: usize,
65) -> Result<Vec<u8>, GzipError> {
66 if bytes.len() > max_compressed_bytes {
67 return Err(GzipError::InputLimit {
68 actual: bytes.len(),
69 limit: max_compressed_bytes,
70 });
71 }
72 let mut decoder = flate2::bufread::GzDecoder::new(bytes);
73 let payload = read_reader(&mut decoder, max_decoded_bytes).map_err(GzipError::Decode)?;
74 if !decoder.into_inner().is_empty() {
75 return Err(GzipError::TrailingData);
76 }
77 Ok(payload)
78}