Skip to main content

rustlavel_http/compression/
gzip.rs

1//! The two framings around a DEFLATE stream: gzip (RFC 1952) and zlib
2//! (RFC 1950).
3//!
4//! Both wrap the same raw stream from `deflate.rs`; they differ only in the
5//! header and which checksum ends them. HTTP names them `gzip` and —
6//! confusingly — `deflate`: despite the name, `Content-Encoding: deflate` is
7//! the zlib format in practice (RFC 9110 §8.4.1.2 says so, and every browser
8//! and server agrees), so the middleware uses `zlib_compress` and
9//! `zlib_decompress` for it, never the raw stream.
10
11use super::checksum::{adler32, crc32};
12use super::deflate::{self, DEFAULT_MAX_OUTPUT, InflateError};
13
14/// The two identification bytes every gzip member begins with (RFC 1952
15/// §2.3.1).
16const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
17/// The only compression method the format has ever defined.
18const CM_DEFLATE: u8 = 8;
19/// The fixed part of a member header: ID1, ID2, CM, FLG, MTIME(4), XFL, OS.
20const GZIP_HEADER_LEN: usize = 10;
21/// CRC-32 and ISIZE, four bytes each.
22const GZIP_TRAILER_LEN: usize = 8;
23
24/// The FLG bits (RFC 1952 §2.3.1). FTEXT is a hint only and changes nothing
25/// about how the member is read.
26const FTEXT: u8 = 1 << 0;
27const FHCRC: u8 = 1 << 1;
28const FEXTRA: u8 = 1 << 2;
29const FNAME: u8 = 1 << 3;
30const FCOMMENT: u8 = 1 << 4;
31/// Bits 5–7 are reserved and must be zero.
32const FLG_RESERVED: u8 = 0b1110_0000;
33
34/// The OS byte for "unknown" — the honest value for a stream that was never
35/// a file on any filesystem.
36const OS_UNKNOWN: u8 = 255;
37
38type Result<T> = std::result::Result<T, InflateError>;
39
40/// Wrap `input` as a single gzip member.
41///
42/// The header is the minimal one: no name, no comment, no extra field, MTIME
43/// zero (there is no file whose time it could be), XFL zero and OS unknown.
44pub fn compress(input: &[u8]) -> Vec<u8> {
45    let body = deflate::compress(input);
46    let mut out = Vec::with_capacity(GZIP_HEADER_LEN + body.len() + GZIP_TRAILER_LEN);
47    out.extend_from_slice(&GZIP_MAGIC);
48    out.push(CM_DEFLATE);
49    out.push(0); // FLG
50    out.extend_from_slice(&[0, 0, 0, 0]); // MTIME
51    out.push(0); // XFL
52    out.push(OS_UNKNOWN);
53    out.extend_from_slice(&body);
54    out.extend_from_slice(&crc32(input).to_le_bytes());
55    // ISIZE is the length modulo 2^32 (RFC 1952 §2.3.1), which is what the
56    // truncating cast gives.
57    out.extend_from_slice(&(input.len() as u32).to_le_bytes());
58    out
59}
60
61/// Unwrap and inflate gzip data, with output capped at
62/// `deflate::DEFAULT_MAX_OUTPUT`.
63pub fn decompress(input: &[u8]) -> Result<Vec<u8>> {
64    decompress_with_limit(input, DEFAULT_MAX_OUTPUT)
65}
66
67/// Unwrap and inflate gzip data, refusing to produce more than `max_out`
68/// bytes in total.
69///
70/// A gzip file may be several members back to back (RFC 1952 §2.2) — that is
71/// what `cat a.gz b.gz` produces, and `gzip -d` reads it as one file — so
72/// every member is decoded and the outputs concatenated. Each member's CRC-32
73/// and ISIZE are verified.
74pub fn decompress_with_limit(input: &[u8], max_out: usize) -> Result<Vec<u8>> {
75    let mut output = Vec::new();
76    let mut rest = input;
77    loop {
78        let (body, consumed) = decompress_member(rest, max_out - output.len())?;
79        output.extend_from_slice(&body);
80        rest = &rest[consumed..];
81        if rest.is_empty() {
82            return Ok(output);
83        }
84    }
85}
86
87/// Decode one member from the start of `input`, returning its contents and
88/// how many bytes it occupied.
89fn decompress_member(input: &[u8], max_out: usize) -> Result<(Vec<u8>, usize)> {
90    let header_len = gzip_header_len(input)?;
91    let (body, deflate_len) = deflate::inflate(&input[header_len..], max_out)?;
92    let trailer_start = header_len + deflate_len;
93    let trailer =
94        input.get(trailer_start..trailer_start + GZIP_TRAILER_LEN).ok_or(InflateError::Truncated)?;
95    if crc32(&body) != le_u32(&trailer[..4]) {
96        return Err(InflateError::ChecksumMismatch);
97    }
98    if body.len() as u32 != le_u32(&trailer[4..]) {
99        return Err(InflateError::LengthMismatch);
100    }
101    Ok((body, trailer_start + GZIP_TRAILER_LEN))
102}
103
104/// Validate a member header and return its total length, optional fields
105/// included (RFC 1952 §2.3). The fields appear in the order FEXTRA, FNAME,
106/// FCOMMENT, FHCRC when their flags are set; nothing in them affects the
107/// data, so they are skipped rather than kept.
108fn gzip_header_len(input: &[u8]) -> Result<usize> {
109    let fixed = input.get(..GZIP_HEADER_LEN).ok_or(InflateError::Truncated)?;
110    if fixed[..2] != GZIP_MAGIC || fixed[2] != CM_DEFLATE {
111        return Err(InflateError::InvalidHeader);
112    }
113    let flags = fixed[3];
114    if flags & FLG_RESERVED != 0 {
115        return Err(InflateError::InvalidHeader);
116    }
117    let mut pos = GZIP_HEADER_LEN;
118    if flags & FEXTRA != 0 {
119        let xlen = usize::from(le_u16(input.get(pos..pos + 2).ok_or(InflateError::Truncated)?));
120        pos += 2 + xlen;
121    }
122    if flags & FNAME != 0 {
123        pos = skip_zero_terminated(input, pos)?;
124    }
125    if flags & FCOMMENT != 0 {
126        pos = skip_zero_terminated(input, pos)?;
127    }
128    if flags & FHCRC != 0 {
129        // The header CRC is the low sixteen bits of the CRC-32 of everything
130        // before it (RFC 1952 §2.3.1).
131        let header = input.get(..pos).ok_or(InflateError::Truncated)?;
132        let stored = le_u16(input.get(pos..pos + 2).ok_or(InflateError::Truncated)?);
133        if (crc32(header) & 0xFFFF) as u16 != stored {
134            return Err(InflateError::ChecksumMismatch);
135        }
136        pos += 2;
137    }
138    let _ = FTEXT; // Advisory; nothing to do with it.
139    if pos > input.len() {
140        return Err(InflateError::Truncated);
141    }
142    Ok(pos)
143}
144
145/// The position just past the zero byte that ends a string starting at `pos`.
146fn skip_zero_terminated(input: &[u8], pos: usize) -> Result<usize> {
147    let rest = input.get(pos..).ok_or(InflateError::Truncated)?;
148    let end = rest.iter().position(|&b| b == 0).ok_or(InflateError::Truncated)?;
149    Ok(pos + end + 1)
150}
151
152fn le_u16(bytes: &[u8]) -> u16 {
153    u16::from_le_bytes([bytes[0], bytes[1]])
154}
155
156fn le_u32(bytes: &[u8]) -> u32 {
157    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
158}
159
160// --- zlib -----------------------------------------------------------------------
161
162/// CMF: CM=8 (DEFLATE) in the low nibble, CINFO=7 (a 32 KiB window) in the
163/// high one (RFC 1950 §2.2).
164const ZLIB_CMF: u8 = 0x78;
165/// FLG: FLEVEL=2 ("default"), no FDICT, and the FCHECK that makes
166/// `CMF * 256 + FLG` a multiple of 31. `78 9c` is the pair zlib itself writes
167/// at its default level, which makes our streams look familiar in a hex dump.
168const ZLIB_FLG: u8 = 0x9c;
169const FDICT: u8 = 1 << 5;
170
171/// Wrap `input` in the zlib format — what HTTP calls `deflate`.
172pub fn zlib_compress(input: &[u8]) -> Vec<u8> {
173    debug_assert_eq!((u16::from(ZLIB_CMF) * 256 + u16::from(ZLIB_FLG)) % 31, 0);
174    let body = deflate::compress(input);
175    let mut out = Vec::with_capacity(2 + body.len() + 4);
176    out.push(ZLIB_CMF);
177    out.push(ZLIB_FLG);
178    out.extend_from_slice(&body);
179    // Adler-32 is stored big-endian, unlike everything in gzip (RFC 1950 §2.2).
180    out.extend_from_slice(&adler32(input).to_be_bytes());
181    out
182}
183
184/// Unwrap and inflate zlib data, with output capped at
185/// `deflate::DEFAULT_MAX_OUTPUT`.
186pub fn zlib_decompress(input: &[u8]) -> Result<Vec<u8>> {
187    zlib_decompress_with_limit(input, DEFAULT_MAX_OUTPUT)
188}
189
190/// Unwrap and inflate zlib data, refusing to produce more than `max_out`
191/// bytes. The Adler-32 trailer is verified, and a stream that asks for a
192/// preset dictionary (FDICT) is rejected: nothing in HTTP defines one.
193pub fn zlib_decompress_with_limit(input: &[u8], max_out: usize) -> Result<Vec<u8>> {
194    let header = input.get(..2).ok_or(InflateError::Truncated)?;
195    let (cmf, flg) = (header[0], header[1]);
196    // CM must be DEFLATE and CINFO at most 7: larger windows are not defined.
197    if cmf & 0x0F != CM_DEFLATE || cmf >> 4 > 7 {
198        return Err(InflateError::InvalidHeader);
199    }
200    if (u16::from(cmf) * 256 + u16::from(flg)) % 31 != 0 || flg & FDICT != 0 {
201        return Err(InflateError::InvalidHeader);
202    }
203    let (body, deflate_len) = deflate::inflate(&input[2..], max_out)?;
204    let trailer_start = 2 + deflate_len;
205    let trailer = input.get(trailer_start..trailer_start + 4).ok_or(InflateError::Truncated)?;
206    if adler32(&body) != u32::from_be_bytes([trailer[0], trailer[1], trailer[2], trailer[3]]) {
207        return Err(InflateError::ChecksumMismatch);
208    }
209    if input.len() > trailer_start + 4 {
210        return Err(InflateError::TrailingData);
211    }
212    Ok(body)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    const FOX: &[u8] = b"The quick brown fox jumps over the lazy dog.";
220
221    /// gzip.compress(FOX, mtime=0) from Python 3 / zlib 1.2.12.
222    const PYTHON_GZIP_FOX: [u8; 63] = [
223        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0x0b, 0xc9, 0x48, 0x55, 0x28, 0x2c, 0xcd,
224        0x4c, 0xce, 0x56, 0x48, 0x2a, 0xca, 0x2f, 0xcf, 0x53, 0x48, 0xcb, 0xaf, 0x50, 0xc8, 0x2a, 0xcd, 0x2d,
225        0x28, 0x56, 0xc8, 0x2f, 0x4b, 0x2d, 0x52, 0x28, 0x01, 0x4a, 0xe7, 0x24, 0x56, 0x55, 0x2a, 0xa4, 0xe4,
226        0xa7, 0xeb, 0x01, 0x00, 0xe9, 0x25, 0x90, 0x51, 0x2c, 0x00, 0x00, 0x00,
227    ];
228
229    /// zlib.compress(b"hello hello hello hello", 9).
230    const PYTHON_ZLIB_HELLO: [u8; 16] =
231        [0x78, 0xda, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0x57, 0xc8, 0x40, 0x27, 0x01, 0x68, 0x03, 0x08, 0xb1];
232
233    /// A member with FEXTRA, FNAME, FCOMMENT and FHCRC all set, holding
234    /// b"header flags galore". Assembled by hand from Python's raw deflate
235    /// output and checked with `gzip -t` before being embedded here.
236    const GZIP_ALL_FLAGS: [u8; 70] = [
237        0x1f, 0x8b, 0x08, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x08, 0x00, 0x41, 0x42, 0x04, 0x00, 0x31,
238        0x32, 0x33, 0x34, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x61, 0x20, 0x63, 0x6f, 0x6d,
239        0x6d, 0x65, 0x6e, 0x74, 0x00, 0x63, 0x30, 0xcb, 0x48, 0x4d, 0x4c, 0x49, 0x2d, 0x52, 0x48, 0xcb, 0x49,
240        0x4c, 0x2f, 0x56, 0x48, 0x4f, 0xcc, 0xc9, 0x2f, 0x4a, 0x05, 0x00, 0x98, 0xb7, 0x0c, 0xbe, 0x13, 0x00,
241        0x00, 0x00,
242    ];
243
244    fn fox_text() -> Vec<u8> {
245        (0..40)
246            .map(|i| format!("line {i}: the quick brown fox jumps over the lazy dog {}\n", i * i))
247            .collect::<String>()
248            .into_bytes()
249    }
250
251    #[test]
252    fn gzip_round_trips() {
253        for input in [&b""[..], b"x", FOX, &fox_text(), &vec![7u8; 100_000]] {
254            let packed = compress(input);
255            assert_eq!(&packed[..2], &GZIP_MAGIC);
256            assert_eq!(packed[2], CM_DEFLATE);
257            assert_eq!(decompress(&packed).unwrap(), input);
258        }
259    }
260
261    #[test]
262    fn gzip_decodes_python_output() {
263        assert_eq!(decompress(&PYTHON_GZIP_FOX).unwrap(), FOX);
264    }
265
266    #[test]
267    fn gzip_skips_every_optional_header_field() {
268        assert_eq!(decompress(&GZIP_ALL_FLAGS).unwrap(), b"header flags galore");
269    }
270
271    #[test]
272    fn gzip_decodes_concatenated_members() {
273        let mut stream = PYTHON_GZIP_FOX.to_vec();
274        stream.extend_from_slice(&compress(b" And again."));
275        assert_eq!(decompress(&stream).unwrap(), b"The quick brown fox jumps over the lazy dog. And again.");
276    }
277
278    #[test]
279    fn gzip_rejects_wrong_crc() {
280        let mut stream = PYTHON_GZIP_FOX;
281        stream[56] ^= 0x01; // first byte of the CRC-32
282        assert_eq!(decompress(&stream), Err(InflateError::ChecksumMismatch));
283    }
284
285    #[test]
286    fn gzip_rejects_wrong_isize() {
287        let mut stream = PYTHON_GZIP_FOX;
288        stream[60] ^= 0x01; // first byte of ISIZE
289        assert_eq!(decompress(&stream), Err(InflateError::LengthMismatch));
290    }
291
292    #[test]
293    fn gzip_rejects_wrong_header_crc() {
294        let mut stream = GZIP_ALL_FLAGS;
295        stream[39] ^= 0x01; // low byte of the FHCRC field
296        assert_eq!(decompress(&stream), Err(InflateError::ChecksumMismatch));
297    }
298
299    #[test]
300    fn gzip_rejects_bad_headers() {
301        assert_eq!(decompress(&[0x1f, 0x8c, 0x08, 0, 0, 0, 0, 0, 0, 255]), Err(InflateError::InvalidHeader));
302        // Compression method 7 was never defined.
303        assert_eq!(decompress(&[0x1f, 0x8b, 0x07, 0, 0, 0, 0, 0, 0, 255]), Err(InflateError::InvalidHeader));
304        // Reserved flag bit set.
305        assert_eq!(
306            decompress(&[0x1f, 0x8b, 0x08, 0x80, 0, 0, 0, 0, 0, 255]),
307            Err(InflateError::InvalidHeader)
308        );
309    }
310
311    #[test]
312    fn gzip_rejects_truncation_anywhere() {
313        for cut in 0..PYTHON_GZIP_FOX.len() {
314            let result = decompress(&PYTHON_GZIP_FOX[..cut]);
315            assert!(matches!(result, Err(InflateError::Truncated)), "cut at {cut}: {result:?}");
316        }
317        // Optional fields that run off the end: an FNAME with no terminator,
318        // and an FEXTRA whose length outruns the data.
319        assert_eq!(
320            decompress(&[0x1f, 0x8b, 0x08, FNAME, 0, 0, 0, 0, 0, 255, b'a', b'b']),
321            Err(InflateError::Truncated)
322        );
323        assert_eq!(
324            decompress(&[0x1f, 0x8b, 0x08, FEXTRA, 0, 0, 0, 0, 0, 255, 0xff, 0xff, 1]),
325            Err(InflateError::Truncated)
326        );
327    }
328
329    #[test]
330    fn gzip_applies_the_limit_across_members() {
331        let stream = [compress(&[0u8; 3000]), compress(&[0u8; 3000])].concat();
332        assert_eq!(decompress_with_limit(&stream, 6000).unwrap().len(), 6000);
333        assert_eq!(decompress_with_limit(&stream, 5999), Err(InflateError::OutputTooLarge));
334    }
335
336    #[test]
337    fn gzip_never_panics_on_garbage() {
338        let mut stream = GZIP_ALL_FLAGS.to_vec();
339        stream.extend_from_slice(&PYTHON_GZIP_FOX);
340        for i in 0..stream.len() {
341            for bit in 0..8 {
342                let mut corrupt = stream.clone();
343                corrupt[i] ^= 1 << bit;
344                let _ = decompress_with_limit(&corrupt, 1 << 16);
345            }
346        }
347    }
348
349    #[test]
350    fn zlib_round_trips() {
351        for input in [&b""[..], b"x", FOX, &fox_text(), &vec![7u8; 100_000]] {
352            let packed = zlib_compress(input);
353            assert_eq!(&packed[..2], &[0x78, 0x9c]);
354            assert_eq!(zlib_decompress(&packed).unwrap(), input);
355        }
356    }
357
358    #[test]
359    fn zlib_decodes_python_output() {
360        assert_eq!(zlib_decompress(&PYTHON_ZLIB_HELLO).unwrap(), b"hello hello hello hello");
361    }
362
363    #[test]
364    fn zlib_decodes_python_dynamic_block_output() {
365        // The header (`78 da`) and Adler-32 trailer python wrote for
366        // zlib.compress(fox_text(), 9), around a body of ours: the raw
367        // dynamic-block stream python produced is already checked in
368        // deflate.rs, and this proves our checksum of the text agrees with
369        // zlib's without repeating 270 bytes of vector.
370        let raw_body = {
371            let packed = zlib_compress(&fox_text());
372            packed[2..packed.len() - 4].to_vec()
373        };
374        let mut stream = vec![0x78, 0xda];
375        stream.extend_from_slice(&raw_body);
376        stream.extend_from_slice(&[0x22, 0x97, 0x00, 0x2a]); // python's Adler-32 of the text
377        assert_eq!(zlib_decompress(&stream).unwrap(), fox_text());
378    }
379
380    #[test]
381    fn zlib_accepts_any_valid_fcheck_and_level() {
382        // Python at level 9 writes FLEVEL=3: `78 da`.
383        assert_eq!(PYTHON_ZLIB_HELLO[1], 0xda);
384        // Level 1 writes `78 01`; the body is the same raw stream.
385        let mut stream = PYTHON_ZLIB_HELLO;
386        stream[1] = 0x01;
387        assert_eq!(zlib_decompress(&stream).unwrap(), b"hello hello hello hello");
388    }
389
390    #[test]
391    fn zlib_rejects_wrong_adler() {
392        let mut stream = PYTHON_ZLIB_HELLO;
393        stream[15] ^= 0x01;
394        assert_eq!(zlib_decompress(&stream), Err(InflateError::ChecksumMismatch));
395    }
396
397    #[test]
398    fn zlib_rejects_bad_headers() {
399        // FCHECK wrong.
400        let mut stream = PYTHON_ZLIB_HELLO;
401        stream[1] = 0x9d;
402        assert_eq!(zlib_decompress(&stream), Err(InflateError::InvalidHeader));
403        // A preset dictionary we cannot have.
404        let mut stream = PYTHON_ZLIB_HELLO;
405        stream[1] = 0xbb; // FDICT set, FCHECK valid: 0x78bb % 31 == 0
406        assert_eq!((0x78u16 * 256 + 0xbb) % 31, 0);
407        assert_eq!(zlib_decompress(&stream), Err(InflateError::InvalidHeader));
408        // Not DEFLATE.
409        let mut stream = PYTHON_ZLIB_HELLO;
410        stream[0] = 0x77;
411        assert_eq!(zlib_decompress(&stream), Err(InflateError::InvalidHeader));
412    }
413
414    #[test]
415    fn zlib_rejects_truncation_and_trailing_data() {
416        for cut in 0..PYTHON_ZLIB_HELLO.len() {
417            let result = zlib_decompress(&PYTHON_ZLIB_HELLO[..cut]);
418            assert!(matches!(result, Err(InflateError::Truncated)), "cut at {cut}: {result:?}");
419        }
420        let mut stream = PYTHON_ZLIB_HELLO.to_vec();
421        stream.push(0);
422        assert_eq!(zlib_decompress(&stream), Err(InflateError::TrailingData));
423    }
424
425    #[test]
426    fn zlib_applies_the_limit() {
427        let packed = zlib_compress(&[0u8; 1 << 20]);
428        assert_eq!(zlib_decompress_with_limit(&packed, 1000), Err(InflateError::OutputTooLarge));
429    }
430}