//! gzip (RFC 1952) compression and decompression — pure Rust. //! //! Implements the gzip framing on top of [`crate::deflate`]: a fixed 10-byte //! header (skipping the optional extra/name/comment fields on decode), the //! DEFLATE stream, and an 8-byte trailer carrying the CRC-32 of the //! uncompressed data and the (modulo 2^32) uncompressed length. //! //! Used by `CompressionStream('gzip')` / `DecompressionStream('gzip')` in the //! Streams API. Both one-shot and incremental variants are provided; the //! incremental forms hold no input beyond the in-flight DEFLATE state. use crate::deflate::{self, StreamingDeflater, StreamingInflater}; use std::fmt; // --------------------------------------------------------------------------- // Error type // --------------------------------------------------------------------------- #[derive(Debug, Clone, PartialEq, Eq)] pub enum GzipError { /// Input did not begin with the gzip magic bytes (0x1F, 0x8B). BadMagic, /// Compression method other than 8 (DEFLATE) was requested. UnsupportedMethod(u8), /// Reserved flag bits in FLG byte were set. ReservedFlags(u8), /// CRC-32 of the decompressed data did not match the trailer. CrcMismatch { expected: u32, actual: u32 }, /// Uncompressed length in the trailer did not match the actual length /// (mod 2^32). LengthMismatch { expected: u32, actual: u32 }, /// Input ended before the gzip frame completed. UnexpectedEof, /// Underlying DEFLATE failed. Deflate(deflate::DeflateError), } impl fmt::Display for GzipError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadMagic => write!(f, "invalid gzip magic"), Self::UnsupportedMethod(m) => write!(f, "unsupported compression method: {m}"), Self::ReservedFlags(b) => write!(f, "reserved flag bits set: {b:#04x}"), Self::CrcMismatch { expected, actual } => write!( f, "CRC-32 mismatch: expected {expected:#010x}, got {actual:#010x}" ), Self::LengthMismatch { expected, actual } => { write!(f, "ISIZE mismatch: expected {expected}, got {actual}") } Self::UnexpectedEof => write!(f, "unexpected end of gzip stream"), Self::Deflate(e) => write!(f, "deflate error: {e}"), } } } impl From for GzipError { fn from(e: deflate::DeflateError) -> Self { Self::Deflate(e) } } pub type Result = std::result::Result; // --------------------------------------------------------------------------- // CRC-32 (IEEE 802.3 polynomial 0xEDB88320) // --------------------------------------------------------------------------- /// Pre-computed table for byte-wise CRC-32 (polynomial 0xEDB88320 reflected). const CRC32_TABLE: [u32; 256] = { let mut table = [0u32; 256]; let mut i = 0u32; while i < 256 { let mut c = i; let mut k = 0; while k < 8 { c = if c & 1 != 0 { 0xEDB88320 ^ (c >> 1) } else { c >> 1 }; k += 1; } table[i as usize] = c; i += 1; } table }; /// Compute CRC-32 (IEEE) of `data`. pub fn crc32(data: &[u8]) -> u32 { let mut crc = Crc32::new(); crc.update(data); crc.finalize() } /// Incremental CRC-32 accumulator. #[derive(Debug, Clone, Copy)] pub struct Crc32 { state: u32, } impl Default for Crc32 { fn default() -> Self { Self::new() } } impl Crc32 { pub fn new() -> Self { Self { state: 0xFFFF_FFFF } } pub fn update(&mut self, data: &[u8]) { let mut s = self.state; for &b in data { s = CRC32_TABLE[((s ^ b as u32) & 0xFF) as usize] ^ (s >> 8); } self.state = s; } pub fn finalize(self) -> u32 { self.state ^ 0xFFFF_FFFF } } // --------------------------------------------------------------------------- // Streaming gzip inflater // --------------------------------------------------------------------------- enum GzInfStage { /// Reading the 10-byte fixed header. FixedHeader, /// Skipping the FEXTRA block (XLEN + data). Extra { remaining: u16, len_bytes: u8, }, /// Skipping FNAME (zero-terminated). Name, /// Skipping FCOMMENT (zero-terminated). Comment, /// Skipping FHCRC (2 bytes). HeaderCrc { remaining: u8, }, /// Inflating the DEFLATE payload. Body, /// Reading the 8-byte trailer. Trailer, Done, } /// Incremental gzip decompressor. pub struct StreamingGzipInflater { stage: GzInfStage, fixed: Vec, /// Flags byte from header, after we read it. flags: u8, inflater: StreamingInflater, crc: Crc32, isize_bytes: u32, trailer: Vec, saved_input: Vec, } impl Default for StreamingGzipInflater { fn default() -> Self { Self::new() } } impl StreamingGzipInflater { pub fn new() -> Self { Self { stage: GzInfStage::FixedHeader, fixed: Vec::with_capacity(10), flags: 0, inflater: StreamingInflater::new(), crc: Crc32::new(), isize_bytes: 0, trailer: Vec::with_capacity(8), saved_input: Vec::new(), } } pub fn push(&mut self, data: &[u8]) -> Result> { let mut buf: Vec = std::mem::take(&mut self.saved_input); buf.extend_from_slice(data); let mut out = Vec::new(); let mut cursor: usize = 0; loop { match &mut self.stage { GzInfStage::FixedHeader => { while self.fixed.len() < 10 && cursor < buf.len() { self.fixed.push(buf[cursor]); cursor += 1; } if self.fixed.len() < 10 { // Save remaining input for next push. break; } if self.fixed[0] != 0x1F || self.fixed[1] != 0x8B { return Err(GzipError::BadMagic); } let cm = self.fixed[2]; if cm != 8 { return Err(GzipError::UnsupportedMethod(cm)); } let flg = self.fixed[3]; if flg & 0xE0 != 0 { return Err(GzipError::ReservedFlags(flg)); } self.flags = flg; // Skip MTIME/XFL/OS (already consumed). self.stage = if flg & 0x04 != 0 { GzInfStage::Extra { remaining: 0, len_bytes: 0, } } else if flg & 0x08 != 0 { GzInfStage::Name } else if flg & 0x10 != 0 { GzInfStage::Comment } else if flg & 0x02 != 0 { GzInfStage::HeaderCrc { remaining: 2 } } else { GzInfStage::Body }; } GzInfStage::Extra { remaining, len_bytes, } => { while *len_bytes < 2 && cursor < buf.len() { let b = buf[cursor]; cursor += 1; if *len_bytes == 0 { *remaining = b as u16; } else { *remaining |= (b as u16) << 8; } *len_bytes += 1; } if *len_bytes < 2 { break; } let take = (*remaining as usize).min(buf.len() - cursor); cursor += take; *remaining -= take as u16; if *remaining == 0 { self.stage = next_optional_stage(self.flags & !0x04); } else { break; } } GzInfStage::Name => { while cursor < buf.len() { let b = buf[cursor]; cursor += 1; if b == 0 { self.stage = next_optional_stage(self.flags & !0x08); break; } } if !matches!(self.stage, GzInfStage::Name) { continue; } break; } GzInfStage::Comment => { while cursor < buf.len() { let b = buf[cursor]; cursor += 1; if b == 0 { self.stage = next_optional_stage(self.flags & !0x10); break; } } if !matches!(self.stage, GzInfStage::Comment) { continue; } break; } GzInfStage::HeaderCrc { remaining } => { while *remaining > 0 && cursor < buf.len() { cursor += 1; *remaining -= 1; } if *remaining == 0 { self.stage = GzInfStage::Body; } else { break; } } GzInfStage::Body => { // Feed bytes one at a time so we can detect end-of-stream // and capture any trailing trailer bytes. let mut idx = cursor; while idx < buf.len() { let chunk = self.inflater.push(&[buf[idx]])?; if !chunk.is_empty() { self.crc.update(&chunk); self.isize_bytes = self.isize_bytes.wrapping_add(chunk.len() as u32); out.extend(chunk); } idx += 1; if self.inflater.is_done() { cursor = idx; self.stage = GzInfStage::Trailer; break; } } if matches!(self.stage, GzInfStage::Body) { cursor = idx; break; } } GzInfStage::Trailer => { while self.trailer.len() < 8 && cursor < buf.len() { self.trailer.push(buf[cursor]); cursor += 1; } if self.trailer.len() == 8 { let expected_crc = (self.trailer[0] as u32) | ((self.trailer[1] as u32) << 8) | ((self.trailer[2] as u32) << 16) | ((self.trailer[3] as u32) << 24); let actual_crc = self.crc.finalize(); if actual_crc != expected_crc { return Err(GzipError::CrcMismatch { expected: expected_crc, actual: actual_crc, }); } let expected_len = (self.trailer[4] as u32) | ((self.trailer[5] as u32) << 8) | ((self.trailer[6] as u32) << 16) | ((self.trailer[7] as u32) << 24); if expected_len != self.isize_bytes { return Err(GzipError::LengthMismatch { expected: expected_len, actual: self.isize_bytes, }); } self.stage = GzInfStage::Done; } else { break; } } GzInfStage::Done => break, } } if cursor < buf.len() { // Preserve unconsumed input for next push. self.saved_input = buf[cursor..].to_vec(); } Ok(out) } pub fn finish(&mut self) -> Result> { if !matches!(self.stage, GzInfStage::Done) { return Err(GzipError::UnexpectedEof); } Ok(Vec::new()) } } /// One-shot gzip decompression: decode a complete gzip member (RFC 1952) and /// return the uncompressed bytes. pub fn gunzip(input: &[u8]) -> Result> { let mut inflater = StreamingGzipInflater::new(); let out = inflater.push(input)?; inflater.finish()?; Ok(out) } fn next_optional_stage(remaining_flags: u8) -> GzInfStage { if remaining_flags & 0x08 != 0 { GzInfStage::Name } else if remaining_flags & 0x10 != 0 { GzInfStage::Comment } else if remaining_flags & 0x02 != 0 { GzInfStage::HeaderCrc { remaining: 2 } } else { GzInfStage::Body } } // --------------------------------------------------------------------------- // Streaming gzip deflater // --------------------------------------------------------------------------- /// Incremental gzip compressor. pub struct StreamingGzipDeflater { deflater: StreamingDeflater, crc: Crc32, isize_bytes: u32, header_emitted: bool, finished: bool, } impl Default for StreamingGzipDeflater { fn default() -> Self { Self::new() } } impl StreamingGzipDeflater { pub fn new() -> Self { Self { deflater: StreamingDeflater::new(), crc: Crc32::new(), isize_bytes: 0, header_emitted: false, finished: false, } } pub fn push(&mut self, data: &[u8]) -> Vec { if self.finished { return Vec::new(); } self.crc.update(data); self.isize_bytes = self.isize_bytes.wrapping_add(data.len() as u32); let mut out = Vec::new(); if !self.header_emitted { out.extend(gzip_header()); self.header_emitted = true; } out.extend(self.deflater.push(data)); out } pub fn finish(&mut self) -> Vec { if self.finished { return Vec::new(); } self.finished = true; let mut out = Vec::new(); if !self.header_emitted { out.extend(gzip_header()); self.header_emitted = true; } out.extend(self.deflater.finish()); let crc = self.crc.finalize(); out.push(crc as u8); out.push((crc >> 8) as u8); out.push((crc >> 16) as u8); out.push((crc >> 24) as u8); let isz = self.isize_bytes; out.push(isz as u8); out.push((isz >> 8) as u8); out.push((isz >> 16) as u8); out.push((isz >> 24) as u8); out } } /// Minimal gzip header: magic, deflate method, no flags, zero MTIME, no extra /// flags, OS = 0xFF (unknown). fn gzip_header() -> [u8; 10] { [ 0x1F, 0x8B, // magic 0x08, // CM = DEFLATE 0x00, // FLG = none 0x00, 0x00, 0x00, 0x00, // MTIME = 0 0x00, // XFL 0xFF, // OS = unknown ] } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn crc32_empty() { assert_eq!(crc32(b""), 0); } #[test] fn crc32_known_values() { // Well-known vectors. assert_eq!(crc32(b"a"), 0xE8B7BE43); assert_eq!(crc32(b"abc"), 0x352441C2); assert_eq!( crc32(b"The quick brown fox jumps over the lazy dog"), 0x414FA339 ); } #[test] fn crc32_incremental_matches_oneshot() { let data = b"hello world, gzip!"; let mut c = Crc32::new(); c.update(&data[..5]); c.update(&data[5..]); assert_eq!(c.finalize(), crc32(data)); } #[test] fn gzip_roundtrip_empty() { let mut d = StreamingGzipDeflater::new(); let mut comp = d.push(b""); comp.extend(d.finish()); let mut i = StreamingGzipInflater::new(); let out = i.push(&comp).unwrap(); i.finish().unwrap(); assert!(out.is_empty()); } #[test] fn gzip_roundtrip_basic() { let payload = b"Hello, gzip streaming world!"; let mut d = StreamingGzipDeflater::new(); let mut comp = d.push(payload); comp.extend(d.finish()); let mut i = StreamingGzipInflater::new(); let mut out = Vec::new(); for &b in &comp { out.extend(i.push(&[b]).unwrap()); } i.finish().unwrap(); assert_eq!(out, payload); } #[test] fn gzip_roundtrip_chunked() { let payload: Vec = (0..3000u32) .map(|i| (i.wrapping_mul(2654435761) & 0xff) as u8) .collect(); let mut d = StreamingGzipDeflater::new(); let mut comp = Vec::new(); for chunk in payload.chunks(19) { comp.extend(d.push(chunk)); } comp.extend(d.finish()); let mut i = StreamingGzipInflater::new(); let mut out = Vec::new(); for chunk in comp.chunks(5) { out.extend(i.push(chunk).unwrap()); } i.finish().unwrap(); assert_eq!(out, payload); } #[test] fn gunzip_oneshot_roundtrip() { let payload = b"one-shot gunzip helper payload"; let mut d = StreamingGzipDeflater::new(); let mut comp = d.push(payload); comp.extend(d.finish()); assert_eq!(gunzip(&comp).unwrap(), payload); } #[test] fn gunzip_oneshot_rejects_truncated() { let payload = b"truncated stream"; let mut d = StreamingGzipDeflater::new(); let mut comp = d.push(payload); comp.extend(d.finish()); comp.truncate(comp.len() - 4); assert!(gunzip(&comp).is_err()); } #[test] fn gzip_inflate_rejects_bad_magic() { let mut i = StreamingGzipInflater::new(); let bytes = [0x00u8; 10]; assert!(matches!(i.push(&bytes), Err(GzipError::BadMagic))); } #[test] fn gzip_inflate_rejects_bad_crc() { let payload = b"hello"; let mut d = StreamingGzipDeflater::new(); let mut comp = d.push(payload); comp.extend(d.finish()); // Corrupt one byte of CRC (positions n-8..n-4). let n = comp.len(); comp[n - 8] ^= 0xAA; let mut i = StreamingGzipInflater::new(); let mut err = None; for &b in &comp { match i.push(&[b]) { Ok(_) => {} Err(e) => { err = Some(e); break; } } } assert!(matches!(err, Some(GzipError::CrcMismatch { .. }))); } }