use flate2::write::GzEncoder;
use flate2::{Compression, Crc};
use gzip_zero::{DecodeStep, Decoder, DecoderBuffers, HISTORY_SIZE};
use std::io::Write;
fn gz(data: &[u8]) -> Vec<u8> {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
}
fn decode_chunked(stream: &[u8], chunk: usize) -> (Vec<u8>, Result<(), String>) {
let mut history = vec![0u8; HISTORY_SIZE];
let mut decoder = Decoder::new(DecoderBuffers {
history: &mut history,
})
.unwrap();
let mut out = Vec::new();
let mut pos = 0usize;
let mut guard = 0usize;
loop {
guard += 1;
assert!(guard < 50_000_000, "no-progress / infinite loop detected");
let end = (pos + chunk).min(stream.len());
let step = decoder.decode(&stream[pos..end]).expect("decode error");
if let DecodeStep::Output { bytes, .. } = &step {
assert!(!bytes.is_empty(), "Output step with empty slice");
out.extend_from_slice(bytes);
}
pos += step.consumed();
let done = matches!(step, DecodeStep::NeedInput { .. }) && pos == stream.len();
if done {
break;
}
}
let fin = decoder.finish().map_err(|e| e.to_string());
(out, fin)
}
#[test]
fn decodes_output_sizes_around_ring_boundary() {
for size in [
0usize,
1,
HISTORY_SIZE - 1,
HISTORY_SIZE,
HISTORY_SIZE + 1,
2 * HISTORY_SIZE,
2 * HISTORY_SIZE + 1,
3 * HISTORY_SIZE,
] {
let payload: Vec<u8> = (0..size).map(|i| (i * 7 + i / 251) as u8).collect();
let stream = gz(&payload);
for chunk in [1usize, 3, 7, 4096, stream.len().max(1)] {
let (out, fin) = decode_chunked(&stream, chunk);
assert_eq!(out, payload, "size={size} chunk={chunk}");
assert!(
fin.is_ok(),
"size={size} chunk={chunk}: finish() -> {fin:?}"
);
}
}
}
#[test]
fn back_reference_before_member_start_cannot_leak() {
let secret = vec![b'B'; HISTORY_SIZE + 8192];
let mut stream = gz(&secret);
let mut bits: Vec<u8> = Vec::new();
let mut cur = 0u8;
let mut nbits = 0u8;
let push = |bit: u32, bits: &mut Vec<u8>, cur: &mut u8, nbits: &mut u8| {
*cur |= ((bit & 1) as u8) << *nbits;
*nbits += 1;
if *nbits == 8 {
bits.push(*cur);
*cur = 0;
*nbits = 0;
}
};
let lsb = |v: u32, n: u32, bits: &mut Vec<u8>, cur: &mut u8, nbits: &mut u8| {
for i in 0..n {
push(v >> i, bits, cur, nbits);
}
};
let msb = |v: u32, n: u32, bits: &mut Vec<u8>, cur: &mut u8, nbits: &mut u8| {
for i in (0..n).rev() {
push(v >> i, bits, cur, nbits);
}
};
lsb(1, 1, &mut bits, &mut cur, &mut nbits); lsb(1, 2, &mut bits, &mut cur, &mut nbits); msb(0x30 + 65, 8, &mut bits, &mut cur, &mut nbits); msb(1, 7, &mut bits, &mut cur, &mut nbits); msb(13, 5, &mut bits, &mut cur, &mut nbits); lsb(3, 5, &mut bits, &mut cur, &mut nbits); msb(0, 7, &mut bits, &mut cur, &mut nbits); if nbits > 0 {
bits.push(cur);
}
let expected: Vec<u8> = vec![b'A', 0, 0, 0];
let mut crc = Crc::new();
crc.update(&expected);
stream.extend_from_slice(&[0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0, 0x00, 0xff]);
stream.extend_from_slice(&bits);
stream.extend_from_slice(&crc.sum().to_le_bytes());
stream.extend_from_slice(&(expected.len() as u32).to_le_bytes());
let (out, fin) = decode_chunked(&stream, 1);
assert!(fin.is_ok(), "finish() -> {fin:?}");
let member2 = &out[secret.len()..];
assert_eq!(
member2,
&expected[..],
"cross-member back-reference leaked previous member data"
);
}
#[test]
fn empty_input_slices_are_idempotent() {
let payload = b"hello world".to_vec();
let stream = gz(&payload);
let mut history = vec![0u8; HISTORY_SIZE];
let mut decoder = Decoder::new(DecoderBuffers {
history: &mut history,
})
.unwrap();
for _ in 0..100 {
let step = decoder.decode(&[]).unwrap();
assert_eq!(step.consumed(), 0);
}
let mut out = Vec::new();
let mut pos = 0;
loop {
let step = decoder.decode(&stream[pos..]).unwrap();
if let DecodeStep::Output { bytes, .. } = &step {
out.extend_from_slice(bytes);
}
pos += step.consumed();
if matches!(step, DecodeStep::NeedInput { .. }) && pos == stream.len() {
break;
}
}
assert_eq!(out, payload);
assert!(decoder.finish().is_ok());
}