Skip to main content

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}