use std::fmt;
use miniz_oxide::inflate::TINFLStatus;
use miniz_oxide::inflate::core::{DecompressorOxide, decompress as inflate, inflate_flags};
use zeroize::Zeroizing;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GzipError {
NotGzip,
TooLarge,
Damaged,
WrongChecksum,
}
impl fmt::Display for GzipError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NotGzip => "this is not a gzip file",
Self::TooLarge => "this is larger than it may be once decompressed",
Self::Damaged => "the compressed data is damaged",
Self::WrongChecksum => "the compressed data does not match its checksum",
})
}
}
impl std::error::Error for GzipError {}
const FHCRC: u8 = 2;
const FEXTRA: u8 = 4;
const FNAME: u8 = 8;
const FCOMMENT: u8 = 16;
pub fn compress(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255];
out.extend(miniz_oxide::deflate::compress_to_vec(data, 9));
out.extend(crc32(data).to_le_bytes());
out.extend((data.len() as u32).to_le_bytes());
out
}
pub fn is_gzip(bytes: &[u8]) -> bool {
bytes.starts_with(&[0x1f, 0x8b])
}
pub fn decompress(bytes: &[u8], limit: usize) -> Result<Zeroizing<Vec<u8>>, GzipError> {
if bytes.len() < 18 || !is_gzip(bytes) || bytes[2] != 8 || bytes[3] & 0xe0 != 0 {
return Err(GzipError::NotGzip);
}
let flags = bytes[3];
let mut at = 10;
let take = |at: &mut usize, n: usize| -> Result<(), GzipError> {
*at = at
.checked_add(n)
.filter(|&end| end <= bytes.len() - 8)
.ok_or(GzipError::NotGzip)?;
Ok(())
};
if flags & FEXTRA != 0 {
take(&mut at, 2)?;
let len = usize::from(u16::from_le_bytes([bytes[at - 2], bytes[at - 1]]));
take(&mut at, len)?;
}
for flag in [FNAME, FCOMMENT] {
if flags & flag != 0 {
let end = bytes[at..bytes.len() - 8]
.iter()
.position(|&b| b == 0)
.ok_or(GzipError::NotGzip)?;
take(&mut at, end + 1)?;
}
}
if flags & FHCRC != 0 {
take(&mut at, 2)?;
}
let (body, trailer) = bytes[at..].split_at(bytes.len() - at - 8);
let crc = u32::from_le_bytes([trailer[0], trailer[1], trailer[2], trailer[3]]);
let size = u32::from_le_bytes([trailer[4], trailer[5], trailer[6], trailer[7]]) as usize;
if size > limit {
return Err(GzipError::TooLarge);
}
let mut out = Zeroizing::new(vec![0u8; size + 1]);
let mut state = Box::<DecompressorOxide>::default();
let (status, read, written) = inflate(
&mut state,
body,
&mut out,
0,
inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF,
);
if status != TINFLStatus::Done || read != body.len() {
return Err(GzipError::Damaged);
}
if written != size || crc32(&out[..written]) != crc {
return Err(GzipError::WrongChecksum);
}
out.truncate(written);
Ok(out)
}
pub(crate) fn crc32(data: &[u8]) -> u32 {
let mut crc = !0u32;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = if crc & 1 == 1 {
(crc >> 1) ^ 0xEDB8_8320
} else {
crc >> 1
};
}
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc32_is_gzips() {
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
assert_eq!(crc32(b""), 0);
}
#[test]
fn what_is_compressed_decompresses_to_itself() {
let data = b"<TQSL_IDENT:4>test\n<Rec_Type:5>tCERT\n<eor>\n".repeat(50);
let gz = compress(&data);
assert_eq!(gz[..4], [0x1f, 0x8b, 8, 0]);
assert_eq!(
decompress(&gz, data.len()).as_deref().map(|v| v.as_slice()),
Ok(&data[..])
);
assert_eq!(compress(&data), gz, "the same data, the same bytes");
}
#[test]
fn a_header_with_a_name_comment_extra_and_crc_is_read() {
let data = b"tqslconfig".repeat(20);
let plain = compress(&data);
let mut gz = plain[..10].to_vec();
gz[3] = FHCRC | FEXTRA | FNAME | FCOMMENT;
gz.extend([3, 0, b'a', b'b', b'c']);
gz.extend(b"tqslconfig.tbk\0a comment\0");
gz.extend([0, 0]);
gz.extend(&plain[10..]);
assert_eq!(
decompress(&gz, 1 << 20).as_deref().map(|v| v.as_slice()),
Ok(&data[..])
);
}
#[test]
fn damaged_lying_or_oversized_input_is_refused() {
let data = b"abcdefgh".repeat(100);
let gz = compress(&data);
assert_eq!(
decompress(b"not gzip at all, really", 1 << 20),
Err(GzipError::NotGzip)
);
assert_eq!(decompress(&gz, data.len() - 1), Err(GzipError::TooLarge));
let mut short = gz.clone();
let n = short.len();
short[n - 4..].copy_from_slice(&10u32.to_le_bytes());
assert!(decompress(&short, 1 << 20).is_err());
let mut crc = gz.clone();
crc[n - 8] ^= 1;
assert_eq!(decompress(&crc, 1 << 20), Err(GzipError::WrongChecksum));
let mut body = gz.clone();
body[12] ^= 0xff;
assert!(decompress(&body, 1 << 20).is_err());
let mut reserved = gz.clone();
reserved[3] = 0x20;
assert_eq!(decompress(&reserved, 1 << 20), Err(GzipError::NotGzip));
let mut unended = gz[..10].to_vec();
unended[3] = FNAME;
unended.extend([b'x'; 20]);
assert_eq!(decompress(&unended, 1 << 20), Err(GzipError::NotGzip));
let mut huge = gz;
huge[n - 4..].copy_from_slice(&u32::MAX.to_le_bytes());
assert_eq!(decompress(&huge, 1 << 20), Err(GzipError::TooLarge));
}
}