use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use crate::cli::InflateMode;
const MIN_CAPACITY: u64 = 1 << 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strategy {
Stream,
Buffer { capacity: usize, ceiling: usize },
}
fn is_gzip(path: &Path) -> bool {
use std::io::Read;
let Ok(mut f) = std::fs::File::open(path) else { return false };
let mut m = [0u8; 2];
f.read_exact(&mut m).is_ok() && m == [0x1f, 0x8b]
}
pub fn isize_hint(path: &Path) -> Option<u64> {
let mut f = std::fs::File::open(path).ok()?;
let len = f.metadata().ok()?.len();
if len < 18 {
return None; }
f.seek(SeekFrom::End(-4)).ok()?;
let mut b = [0u8; 4];
f.read_exact(&mut b).ok()?;
Some(u32::from_le_bytes(b) as u64)
}
fn effective_ceiling(budget: u64) -> u64 {
budget.max(MIN_CAPACITY)
}
pub fn choose(mode: InflateMode, path: &Path, budget: u64) -> Strategy {
match mode {
InflateMode::Stream => Strategy::Stream,
InflateMode::Buffer if !is_gzip(path) => Strategy::Stream,
InflateMode::Buffer => buffer_within_budget(path, budget, isize_hint(path)),
InflateMode::Auto => Strategy::Stream,
}
}
fn buffer_within_budget(path: &Path, budget: u64, hint: Option<u64>) -> Strategy {
let ceiling = effective_ceiling(budget);
let compressed = std::fs::metadata(path).map(|m| m.len()).unwrap_or(u64::MAX);
if compressed > ceiling {
return Strategy::Stream;
}
let capacity = hint.unwrap_or(1 << 20).min(budget).max(MIN_CAPACITY);
Strategy::Buffer { capacity: capacity as usize, ceiling: ceiling as usize }
}
pub fn inflate_buffered(path: &Path, capacity: usize, ceiling: usize) -> io::Result<Vec<u8>> {
let raw = std::fs::read(path)?;
inflate_bytes(&raw, path, capacity, ceiling)
}
pub fn inflate_bytes(
raw: &[u8],
path: &Path,
capacity: usize,
ceiling: usize,
) -> io::Result<Vec<u8>> {
let mut cap = capacity.max(MIN_CAPACITY as usize);
let ceiling = ceiling.max(cap);
let trailer = raw.last_chunk::<4>().map_or(0, |b| u32::from_le_bytes(*b) as usize);
if trailer > ceiling {
return Err(io::Error::other(format!(
"the {ceiling}-byte inflate budget would be exceeded: \
the archive inflates to at least {trailer} bytes"
)));
}
loop {
let mut d = libdeflater::Decompressor::new();
let mut out = vec![0u8; cap];
match d.gzip_decompress(raw, &mut out) {
Ok(n) => {
out.truncate(n);
return Ok(out);
}
Err(libdeflater::DecompressionError::InsufficientSpace) if cap < ceiling => {
cap = cap.saturating_mul(4).min(ceiling);
}
Err(libdeflater::DecompressionError::InsufficientSpace) => {
return Err(io::Error::other(format!(
"the {ceiling}-byte inflate budget would be exceeded"
)));
}
Err(e) => {
return Err(io::Error::other(format!(
"whole-buffer inflate failed for {}: {e:?}",
path.display()
)));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn gz(body: &[u8]) -> Vec<u8> {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(body).unwrap();
e.finish().unwrap()
}
fn incompressible(n: usize) -> Vec<u8> {
let mut s: u64 = 0x2545F4914F6CDD1D;
(0..n)
.map(|_| {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1);
(s >> 33) as u8
})
.collect()
}
fn tmp(bytes: &[u8]) -> (tempfile::TempDir, std::path::PathBuf) {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("x.gz");
std::fs::write(&p, bytes).unwrap();
(d, p)
}
#[test]
fn reads_the_isize_trailer() {
let body = vec![b'a'; 5000];
let (_d, p) = tmp(&gz(&body));
assert_eq!(isize_hint(&p), Some(5000));
}
#[test]
fn isize_of_a_too_short_file_is_none() {
let (_d, p) = tmp(b"tiny");
assert_eq!(isize_hint(&p), None);
}
#[test]
fn auto_streams_even_when_the_hint_fits_the_budget() {
let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
assert!(matches!(
choose(crate::cli::InflateMode::Auto, &p, 64 * 1024 * 1024),
Strategy::Stream
));
}
#[test]
fn explicit_modes_override_the_heuristic() {
let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
assert!(matches!(choose(crate::cli::InflateMode::Stream, &p, u64::MAX), Strategy::Stream));
assert!(matches!(choose(crate::cli::InflateMode::Buffer, &p, 0), Strategy::Buffer { .. }));
}
#[test]
fn a_trailer_over_the_ceiling_is_refused_without_decompressing() {
let mut bytes = gz(&incompressible(200_000));
bytes[12] ^= 0xff;
bytes[13] ^= 0xff;
let err = inflate_bytes(&bytes, Path::new("x.gz"), 1 << 16, 100_000).unwrap_err();
assert!(err.to_string().contains("budget"), "decompressed despite the trailer: {err}");
}
#[test]
fn buffered_inflate_grows_when_the_hint_was_too_small() {
let (_d, p) = tmp(&gz(&vec![b'z'; 200_000]));
let out = inflate_buffered(&p, 16, 1 << 20).unwrap();
assert_eq!(out.len(), 200_000);
}
#[test]
fn buffer_mode_capacity_and_ceiling_never_exceed_the_budget() {
let (_d, p) = tmp(&gz(&vec![b'a'; 500_000]));
let budget = 10_000u64;
match choose(InflateMode::Buffer, &p, budget) {
Strategy::Buffer { capacity, ceiling } => {
let bound = budget.max(1 << 16);
assert!(capacity as u64 <= bound, "capacity {capacity} exceeds budget-derived bound {bound}");
assert!(ceiling as u64 <= bound, "ceiling {ceiling} exceeds budget-derived bound {bound}");
}
Strategy::Stream => {} }
}
#[test]
fn a_compressed_file_larger_than_the_budget_selects_stream() {
let (_d, p) = tmp(&gz(&incompressible(200_000)));
assert!(matches!(choose(InflateMode::Buffer, &p, 1024), Strategy::Stream));
}
#[test]
fn inflate_budget_zero_still_means_never_buffer_in_auto() {
let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
assert!(matches!(choose(InflateMode::Auto, &p, 0), Strategy::Stream));
}
}