#[inline]
pub fn is_gzip(data: &[u8]) -> bool {
data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b
}
pub fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>, DecompressError> {
decompress_gzip_with_limit(data, usize::MAX)
}
pub fn decompress_gzip_with_limit(
data: &[u8],
max_output_bytes: usize,
) -> Result<Vec<u8>, DecompressError> {
if data.len() < 18 {
return Err(DecompressError::TooShort);
}
if !is_gzip(data) {
return Err(DecompressError::NotGzip);
}
let flags = data[3];
let mut pos = 10;
if flags & 0x04 != 0 {
if pos + 2 > data.len() {
return Err(DecompressError::InvalidHeader);
}
let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + xlen;
}
if flags & 0x08 != 0 {
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
pos += 1; }
if flags & 0x10 != 0 {
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
pos += 1; }
if flags & 0x02 != 0 {
pos += 2;
}
if pos > data.len() - 8 {
return Err(DecompressError::InvalidHeader);
}
let deflate_data = &data[pos..data.len() - 8];
let advertised_size = u32::from_le_bytes([
data[data.len() - 4],
data[data.len() - 3],
data[data.len() - 2],
data[data.len() - 1],
]) as usize;
if advertised_size > max_output_bytes {
return Err(DecompressError::OutputTooLarge {
advertised: advertised_size,
limit: max_output_bytes,
});
}
miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(deflate_data, max_output_bytes)
.or_else(|_| {
miniz_oxide::inflate::decompress_to_vec_with_limit(deflate_data, max_output_bytes)
})
.map_err(|_| DecompressError::DecompressFailed)
}
pub fn decompress_if_gzip(data: &[u8]) -> std::borrow::Cow<'_, [u8]> {
if is_gzip(data) {
match decompress_gzip(data) {
Ok(decompressed) => std::borrow::Cow::Owned(decompressed),
Err(_) => std::borrow::Cow::Borrowed(data),
}
} else {
std::borrow::Cow::Borrowed(data)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecompressError {
TooShort,
NotGzip,
InvalidHeader,
DecompressFailed,
OutputTooLarge { advertised: usize, limit: usize },
}
impl std::fmt::Display for DecompressError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooShort => write!(f, "Data too short for gzip"),
Self::NotGzip => write!(f, "Not gzip compressed"),
Self::InvalidHeader => write!(f, "Invalid gzip header"),
Self::DecompressFailed => write!(f, "Decompression failed"),
Self::OutputTooLarge { advertised, limit } => {
write!(
f,
"Decompressed payload too large (advertised {} bytes > limit {} bytes)",
advertised, limit
)
},
}
}
}
impl std::error::Error for DecompressError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_gzip() {
assert!(is_gzip(&[0x1f, 0x8b, 0x08]));
assert!(!is_gzip(&[0x00, 0x00]));
assert!(!is_gzip(&[0x1f]));
assert!(!is_gzip(&[]));
}
#[test]
fn test_decompress_if_gzip_plain() {
let plain = b"Hello, World!";
let result = decompress_if_gzip(plain);
assert_eq!(&*result, plain);
}
#[test]
fn test_decompress_if_gzip_plain_returns_borrowed() {
let plain = b"not compressed";
let result = decompress_if_gzip(plain);
assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
}
#[test]
fn test_decompress_if_gzip_empty() {
let result = decompress_if_gzip(&[]);
assert!(result.is_empty());
assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
}
#[test]
fn test_decompress_gzip_with_limit_rejects_large_advertised_size() {
let mut payload = vec![0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0, 0, 0x03];
payload.extend_from_slice(&[0, 0, 0, 0]); payload.extend_from_slice(&(1024u32).to_le_bytes());
let err =
decompress_gzip_with_limit(&payload, 16).expect_err("must reject oversize payload");
assert!(matches!(
err,
DecompressError::OutputTooLarge {
advertised: 1024,
limit: 16,
}
));
}
}