#[cfg(test)]
mod tests;
use super::{
ArtifactError, ArtifactIdentity, BoundedWriter, MatchingWriter, hash_reader, read_reader,
visit_reader,
};
use std::{
fmt,
io::{self, Write},
};
pub fn encode_gzip(
bytes: &[u8],
writer: impl Write,
compression: flate2::Compression,
max_compressed_bytes: u64,
) -> io::Result<()> {
let mut encoder = flate2::GzBuilder::new().mtime(0).write(
BoundedWriter::new(writer, max_compressed_bytes),
compression,
);
encoder.write_all(bytes)?;
encoder.finish()?;
Ok(())
}
#[derive(Debug)]
pub enum GzipError {
InputLimit {
actual: usize,
limit: usize,
},
Decode(ArtifactError),
TrailingData,
}
impl fmt::Display for GzipError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputLimit { actual, limit } => {
write!(f, "input has {actual} bytes, exceeding {limit}")
}
Self::Decode(_) => f.write_str("bounded payload processing failed"),
Self::TrailingData => f.write_str("bytes follow the single gzip member"),
}
}
}
impl std::error::Error for GzipError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Decode(source) => Some(source),
_ => None,
}
}
}
pub fn decode_gzip(
bytes: &[u8],
max_compressed_bytes: usize,
max_decoded_bytes: usize,
) -> Result<Vec<u8>, GzipError> {
with_decoder(bytes, max_compressed_bytes, |decoder| {
read_reader(decoder, max_decoded_bytes)
})
}
pub fn hash_gzip(
bytes: &[u8],
max_compressed_bytes: usize,
max_decoded_bytes: u64,
) -> Result<ArtifactIdentity, GzipError> {
with_decoder(bytes, max_compressed_bytes, |decoder| {
hash_reader(decoder, max_decoded_bytes)
})
}
pub fn hash_gzip_or_raw(
bytes: &[u8],
max_input_bytes: usize,
max_payload_bytes: u64,
) -> Result<ArtifactIdentity, GzipError> {
if bytes.starts_with(&[0x1f, 0x8b]) {
hash_gzip(bytes, max_input_bytes, max_payload_bytes)
} else {
check_input_limit(bytes, max_input_bytes)?;
hash_reader(bytes, max_payload_bytes).map_err(GzipError::Decode)
}
}
pub fn gzip_matches(
bytes: &[u8],
expected: &[u8],
max_compressed_bytes: usize,
) -> Result<bool, GzipError> {
with_decoder(bytes, max_compressed_bytes, |decoder| {
let mut matching = MatchingWriter::new(expected);
visit_reader::<ArtifactError>(decoder, expected.len() as u64, |chunk| {
matching.write_all(chunk).map_err(ArtifactError::Io)
})?;
Ok(matching.is_complete_match())
})
}
const fn check_input_limit(bytes: &[u8], max_compressed_bytes: usize) -> Result<(), GzipError> {
if bytes.len() > max_compressed_bytes {
return Err(GzipError::InputLimit {
actual: bytes.len(),
limit: max_compressed_bytes,
});
}
Ok(())
}
fn with_decoder<T>(
bytes: &[u8],
max_compressed_bytes: usize,
consume: impl FnOnce(&mut flate2::bufread::GzDecoder<&[u8]>) -> Result<T, ArtifactError>,
) -> Result<T, GzipError> {
check_input_limit(bytes, max_compressed_bytes)?;
let mut decoder = flate2::bufread::GzDecoder::new(bytes);
let payload = consume(&mut decoder).map_err(GzipError::Decode)?;
if !decoder.into_inner().is_empty() {
return Err(GzipError::TrailingData);
}
Ok(payload)
}