use crate::container::Container;
use crate::plan::{Config, compress_objects};
use crate::unit::DeflateError;
pub fn gzip_compress(src: &[u8], cfg: &Config) -> Result<Vec<u8>, DeflateError> {
let one: [&[u8]; 1] = [src];
let c = Config {
container: Container::Gzip,
split_above: 1,
coalesce_below: 0,
..*cfg
};
Ok(compress_objects(&one, &c)?
.pop()
.expect("one input, one output"))
}
pub fn gzip_compress_members(
src: &[u8],
cfg: &Config,
member: usize,
) -> Result<Vec<u8>, DeflateError> {
crate::limits::check_pieces("gzip members", src.len(), member)?;
let member = member.max(1);
let chunks: Vec<&[u8]> = if src.is_empty() {
vec![&src[..]]
} else {
src.chunks(member).collect()
};
let resolved = if cfg.skip.skip(src) {
crate::precompressed::SkipPolicy::already_compressed()
} else {
crate::precompressed::SkipPolicy::compress_everything()
};
let c = Config {
container: Container::Gzip,
coalesce_below: 0,
split_above: usize::MAX,
skip: resolved,
..*cfg
};
let members = compress_objects(&chunks, &c)?;
let total: usize = members.iter().map(|m| m.len()).sum();
let mut out = Vec::with_capacity(total);
for m in &members {
out.extend_from_slice(m);
}
Ok(out)
}
pub fn gzip_members_of(data: &[u8]) -> Option<Vec<&[u8]>> {
let mut out = Vec::new();
let mut i = 0usize;
while i < data.len() {
let rest = &data[i..];
if rest.len() < 18 || rest[0] != 0x1f || rest[1] != 0x8b || rest[2] != 0x08 {
return None;
}
if rest[3] != 0 {
return None; }
let body = &rest[10..];
let mut d = flate2::Decompress::new(false);
let mut sink = Vec::new();
let mut scratch = vec![0u8; 64 * 1024];
loop {
let before_in = d.total_in();
let before_out = d.total_out();
let status = d
.decompress(
&body[(before_in as usize).min(body.len())..],
&mut scratch,
flate2::FlushDecompress::None,
)
.ok()?;
let produced = (d.total_out() - before_out) as usize;
sink.extend_from_slice(&scratch[..produced]);
match status {
flate2::Status::StreamEnd => break,
_ => {
if d.total_in() == before_in && produced == 0 {
return None; }
}
}
}
let consumed = 10 + d.total_in() as usize + 8; if i + consumed > data.len() {
return None;
}
out.push(&data[i..i + consumed]);
i += consumed;
}
if out.is_empty() { None } else { Some(out) }
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn corpus(n: usize) -> Vec<u8> {
let words: [&[u8]; 4] = [
b"2026-08-03T10:15:00Z INFO request id=af39c0 status=200\n",
b"pub fn run() -> usize { let x = 1; x + 2 }\n",
b"the quick brown fox jumps over the lazy dog\n",
b"use std::collections::HashMap;\n",
];
let mut v = Vec::with_capacity(n + 64);
let mut s: u64 = 0x9E3779B97F4A7C15;
while v.len() < n {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
v.extend_from_slice(words[(s >> 33) as usize % words.len()]);
}
v.truncate(n);
v
}
fn ungzip(z: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
flate2::read::MultiGzDecoder::new(z)
.read_to_end(&mut out)
.unwrap();
out
}
#[test]
fn the_single_member_shape_round_trips_at_every_size() {
for &len in &[0usize, 1, 4095, 4096, 100_000, 3_000_000] {
let src = corpus(len);
let single = gzip_compress(&src, &Config::default()).unwrap();
assert_eq!(ungzip(&single), src, "single-member len={len}");
assert_eq!(
&single[..3],
&[0x1f, 0x8b, 0x08],
"len={len}: not a gzip header"
);
}
}
#[test]
fn degenerate_member_sizes_round_trip_on_small_inputs() {
let cfg = Config::default();
for &len in &[0usize, 1, 2, 3, 4095, 4096, 8192] {
let src = corpus(len);
for &m in &[1usize, 2, 3, 4096] {
let multi = gzip_compress_members(&src, &cfg, m).unwrap();
assert_eq!(ungzip(&multi), src, "multi-member len={len} member={m}");
}
}
}
#[test]
fn large_inputs_round_trip_at_realistic_member_sizes() {
let cfg = Config::default();
for &len in &[100_000usize, 3_000_000] {
let src = corpus(len);
for &m in &[64 << 10, 1 << 20] {
let multi = gzip_compress_members(&src, &cfg, m).unwrap();
assert_eq!(ungzip(&multi), src, "multi-member len={len} member={m}");
}
}
}
#[test]
fn a_degenerate_member_size_is_refused_not_attempted() {
let cfg = Config::default();
let just_over = crate::limits::MAX_PIECES + 1;
let e = gzip_compress_members(&vec![b'x'; just_over], &cfg, 1)
.expect_err("one member per byte over the ceiling must be refused");
assert!(
matches!(e, DeflateError::TooManyPieces { pieces, .. } if pieces == just_over),
"wrong error: {e}"
);
let m = e.to_string();
assert!(
m.contains("gzip members"),
"message does not name the axis: {m}"
);
assert!(
m.contains(&just_over.to_string()),
"message does not name the request: {m}"
);
assert!(
m.contains(&crate::limits::MAX_PIECES.to_string()),
"message does not name the ceiling: {m}"
);
assert!(
gzip_compress_members(&vec![b'x'; 3_000_000], &cfg, 1).is_err(),
"3 million one-byte gzip members were accepted — this is the 2026-08-03 OOM"
);
assert!(
gzip_compress_members(&vec![b'x'; 3_000_000], &cfg, 0).is_err(),
"member=0 escaped"
);
assert!(
gzip_compress_members(&vec![b'x'; 4096], &cfg, 1).is_ok(),
"4096 members at m=1"
);
assert!(gzip_compress_members(&vec![b'x'; 3_000_000], &cfg, 64 << 10).is_ok());
}
#[test]
fn every_member_decodes_alone() {
let src = corpus(2_000_000);
let z = gzip_compress_members(&src, &Config::default(), 256 * 1024).unwrap();
let members = gzip_members_of(&z).expect("output must parse as a member sequence");
assert!(
members.len() >= 7,
"expected ~8 members, got {}",
members.len()
);
let mut rebuilt = Vec::new();
for (i, m) in members.iter().enumerate() {
let mut got = Vec::new();
flate2::read::GzDecoder::new(*m)
.read_to_end(&mut got)
.unwrap_or_else(|e| panic!("member {i} does not decode standalone: {e}"));
rebuilt.extend_from_slice(&got);
}
assert_eq!(
rebuilt, src,
"members decoded alone must concatenate to the input"
);
}
#[test]
fn a_split_single_member_is_still_exactly_one_member() {
let src = corpus(2_000_000);
let z = gzip_compress(
&src,
&Config {
segment: 64 << 10,
..Config::default()
},
)
.unwrap();
let members = gzip_members_of(&z).expect("must parse");
assert_eq!(
members.len(),
1,
"a full-flush-split body must remain ONE gzip member; got {} — the split \
path is emitting member headers and the two shapes are not distinct",
members.len()
);
assert_eq!(ungzip(&z), src);
}
#[test]
fn a_wrong_crc_trailer_is_rejected() {
let src = corpus(300_000);
let mut z = gzip_compress(&src, &Config::default()).unwrap();
assert_eq!(ungzip(&z), src, "must read clean before corruption");
let n = z.len();
z[n - 5] ^= 0xff; let mut out = Vec::new();
let r = flate2::read::MultiGzDecoder::new(&z[..]).read_to_end(&mut out);
assert!(
r.is_err(),
"the decoder did NOT verify the gzip CRC-32 — the round-trip tests in \
this module say nothing about crc32_combine"
);
}
#[test]
fn deterministic_and_no_timestamp() {
let src = corpus(4_000_000);
let base = gzip_compress(&src, &Config::default().workers(1)).unwrap();
for w in [2usize, 8, 32] {
assert_eq!(
gzip_compress(&src, &Config::default().workers(w)).unwrap(),
base,
"workers={w}: gzip output differs across worker counts"
);
}
assert_eq!(
&base[4..8],
&[0, 0, 0, 0],
"MTIME must be zero for reproducibility"
);
let m1 = gzip_compress_members(&src, &Config::default().workers(1), 1 << 20).unwrap();
let m2 = gzip_compress_members(&src, &Config::default().workers(16), 1 << 20).unwrap();
assert_eq!(m1, m2, "multi-member output differs across worker counts");
}
#[test]
fn member_shape_costs_ratio_and_that_proves_independence() {
let src = corpus(8 << 20);
let cfg = Config::default();
let single = gzip_compress(
&src,
&Config {
segment: 8 << 20,
..cfg
},
)
.unwrap();
let multi = gzip_compress_members(&src, &cfg, 256 * 1024).unwrap();
assert!(
multi.len() > single.len(),
"independent members ({} B) must be LARGER than one stream ({} B); if not, \
they are sharing an LZ77 window and are not independent",
multi.len(),
single.len()
);
let overhead = multi.len() as f64 / single.len() as f64;
assert!(
overhead < 1.5,
"member overhead {:.1}% is implausibly large",
(overhead - 1.0) * 100.0
);
}
}