1use super::checksum::{adler32, crc32};
12use super::deflate::{self, DEFAULT_MAX_OUTPUT, InflateError};
13
14const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
17const CM_DEFLATE: u8 = 8;
19const GZIP_HEADER_LEN: usize = 10;
21const GZIP_TRAILER_LEN: usize = 8;
23
24const FTEXT: u8 = 1 << 0;
27const FHCRC: u8 = 1 << 1;
28const FEXTRA: u8 = 1 << 2;
29const FNAME: u8 = 1 << 3;
30const FCOMMENT: u8 = 1 << 4;
31const FLG_RESERVED: u8 = 0b1110_0000;
33
34const OS_UNKNOWN: u8 = 255;
37
38type Result<T> = std::result::Result<T, InflateError>;
39
40pub 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); out.extend_from_slice(&[0, 0, 0, 0]); out.push(0); out.push(OS_UNKNOWN);
53 out.extend_from_slice(&body);
54 out.extend_from_slice(&crc32(input).to_le_bytes());
55 out.extend_from_slice(&(input.len() as u32).to_le_bytes());
58 out
59}
60
61pub fn decompress(input: &[u8]) -> Result<Vec<u8>> {
64 decompress_with_limit(input, DEFAULT_MAX_OUTPUT)
65}
66
67pub 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
87fn 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
104fn 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 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; if pos > input.len() {
140 return Err(InflateError::Truncated);
141 }
142 Ok(pos)
143}
144
145fn 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
160const ZLIB_CMF: u8 = 0x78;
165const ZLIB_FLG: u8 = 0x9c;
169const FDICT: u8 = 1 << 5;
170
171pub 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 out.extend_from_slice(&adler32(input).to_be_bytes());
181 out
182}
183
184pub fn zlib_decompress(input: &[u8]) -> Result<Vec<u8>> {
187 zlib_decompress_with_limit(input, DEFAULT_MAX_OUTPUT)
188}
189
190pub 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 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 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 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 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; 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; 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; 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 assert_eq!(decompress(&[0x1f, 0x8b, 0x07, 0, 0, 0, 0, 0, 0, 255]), Err(InflateError::InvalidHeader));
304 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 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 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]); assert_eq!(zlib_decompress(&stream).unwrap(), fox_text());
378 }
379
380 #[test]
381 fn zlib_accepts_any_valid_fcheck_and_level() {
382 assert_eq!(PYTHON_ZLIB_HELLO[1], 0xda);
384 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 let mut stream = PYTHON_ZLIB_HELLO;
401 stream[1] = 0x9d;
402 assert_eq!(zlib_decompress(&stream), Err(InflateError::InvalidHeader));
403 let mut stream = PYTHON_ZLIB_HELLO;
405 stream[1] = 0xbb; assert_eq!((0x78u16 * 256 + 0xbb) % 31, 0);
407 assert_eq!(zlib_decompress(&stream), Err(InflateError::InvalidHeader));
408 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}