linflate/src/lib.rs
//! linflate — Fast pure-Rust DEFLATE decompressor.
//!
//! Full-buffer, zero-copy, SIMD-optimized. Follows libdeflate's architecture
//! with zlib-ng's branchless refill and SIMD match copy, thread-local table
//! pool and segment-aware `inflate_segment` API.
//!
//! # Usage
//! ```no_run
//! # fn try_main(compressed: &[u8], expected_size: usize) -> Result<(), linflate::InflateError> {
//! let mut output = vec![0u8; expected_size + linflate::OVERWRITE_HEADROOM];
//! let written = linflate::inflate_into(compressed, &mut output)?;
//! output.truncate(written);
//! # Ok(()) }
//! ```
pub mod bitreader;
pub mod copy;
pub mod deflate_scan;
pub mod fastloop;
pub mod fixed;
pub mod limits;
pub mod tables;
pub use limits::{MAX_DEFLATE_RATIO, clamp_expected_size, max_inflated_size};
use bitreader::BitReader;
use tables::DecompressTables;
/// **Introspection / emit marker** — record one functional-status row for the
/// nornir test matrix. Wraps `nornir_testmatrix::functional_status` behind the
/// `testmatrix` feature (a compiled-out `#[inline]` no-op otherwise, with no
/// nornir dep in the default build). Mirrors the korp-collectors reference
/// wiring so `nornir test --features testmatrix` SEES each inflate surface.
#[inline]
pub fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(component, check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (component, check, ok, detail);
}
}
/// Extra bytes of output buffer headroom required for SIMD overwrite.
/// Caller must allocate `uncompressed_size + OVERWRITE_HEADROOM`.
pub const OVERWRITE_HEADROOM: usize = copy::MAX_OVERWRITE + 258;
/// Errors from the DEFLATE decompressor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InflateError {
InvalidBlockType,
InvalidStoredLength,
InvalidHuffmanTable,
InvalidDistance,
InvalidCodeLengths,
OutputOverflow,
UnexpectedEof,
DataError,
/// A caller asked to pre-allocate for more output than `compressed` bytes of
/// DEFLATE could physically produce — i.e. a lying header. See
/// [`limits`] for the ratio this is bounded against and why it is an error
/// rather than a clamp.
ImplausibleSize {
/// The output size claimed, in bytes.
expected: usize,
/// The compressed input it claims to describe, in bytes.
compressed: usize,
/// The largest output that input could produce, in bytes.
max: usize,
},
}
impl std::fmt::Display for InflateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidBlockType => write!(f, "invalid DEFLATE block type"),
Self::InvalidStoredLength => write!(f, "invalid stored block length"),
Self::InvalidHuffmanTable => write!(f, "invalid Huffman table"),
Self::InvalidDistance => write!(f, "invalid back-reference distance"),
Self::InvalidCodeLengths => write!(f, "invalid code lengths"),
Self::OutputOverflow => write!(f, "output buffer overflow"),
Self::UnexpectedEof => write!(f, "unexpected end of input"),
Self::DataError => write!(f, "DEFLATE data error"),
Self::ImplausibleSize {
expected,
compressed,
max,
} => write!(
f,
"claimed output size {expected} is impossible from {compressed} compressed bytes: \
DEFLATE cannot expand by more than {MAX_DEFLATE_RATIO}:1, so at most {max} bytes; \
pass at most {max}"
),
}
}
}
impl std::error::Error for InflateError {}
/// Decompress raw DEFLATE data into a pre-allocated output buffer.
///
/// Returns the number of bytes written.
///
/// The output buffer must have at least `OVERWRITE_HEADROOM` extra bytes
/// beyond the expected decompressed size for SIMD overwrite safety.
pub fn inflate_into(compressed: &[u8], output: &mut [u8]) -> Result<usize, InflateError> {
inflate_impl(compressed, output, false)
}
/// Decompress a DEFLATE segment that may not end on BFINAL=1.
///
/// Used by the chunk-level parallel decoder for segments split at
/// Z_FULL_FLUSH boundaries. After BFINAL=0 blocks, if input is exhausted,
/// returns success with the bytes decompressed so far.
pub fn inflate_segment(compressed: &[u8], output: &mut [u8]) -> Result<usize, InflateError> {
inflate_impl(compressed, output, true)
}
/// Decompress a DEFLATE segment with a prefix window for LZ77 back-reference
/// resolution across segment boundaries.
///
/// `output[..prefix_len]` must already contain the window bytes (typically the
/// last 32KB of the previous segment's output). Decoding starts at `prefix_len`
/// and back-references can reach into the prefix.
///
/// Returns the number of NEW bytes written (not counting the prefix).
pub fn inflate_segment_with_prefix(
compressed: &[u8],
output: &mut [u8],
prefix_len: usize,
) -> Result<usize, InflateError> {
inflate_impl_at(compressed, output, prefix_len, true, 0)
}
/// Same as `inflate_segment_with_prefix` but stops after `limit` output bytes.
/// Used for pass-2 fixup where only the first 32KB needs correction.
///
/// Returns the number of NEW bytes written (not counting the prefix).
pub fn inflate_segment_with_prefix_limited(
compressed: &[u8],
output: &mut [u8],
prefix_len: usize,
limit: usize,
) -> Result<usize, InflateError> {
inflate_impl_at(compressed, output, prefix_len, true, limit)
}
/// Where an [`inflate_bits`] decode stopped.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BitsDecoded {
/// New bytes written after the prefix.
pub written: usize,
/// Bit offset (from the start of `compressed`) just past the last block
/// decoded — where the next block starts.
pub end_bit: usize,
/// The last block decoded had BFINAL=1.
pub final_block: bool,
}
/// Decode raw DEFLATE that starts at BIT `start_bit` of `compressed`.
///
/// Writes at `output[prefix_len..]`; `output[..prefix_len]` is the
/// back-reference window (the previous output's tail) and may be empty.
/// Decodes block by block and stops after the first block that ends at or
/// after `stop_bit` (pass `usize::MAX` to run to the final block), or after a
/// final block. The returned `end_bit` is exact, so a second call starting
/// there resumes the stream.
///
/// `start_bit` must be the first bit of a block header. The output buffer
/// needs [`OVERWRITE_HEADROOM`] spare bytes past the data, as everywhere.
pub fn inflate_bits(
compressed: &[u8],
start_bit: usize,
stop_bit: usize,
output: &mut [u8],
prefix_len: usize,
) -> Result<BitsDecoded, InflateError> {
let byte0 = start_bit / 8;
if byte0 >= compressed.len() {
return Err(InflateError::UnexpectedEof);
}
if prefix_len > output.len() {
return Err(InflateError::OutputOverflow);
}
let slice = &compressed[byte0..];
let base = slice.as_ptr();
tables::with_tables(|tables| {
let mut bits = BitReader::new(slice);
unsafe { bits.refill() };
let skip = (start_bit % 8) as u32;
if bits.bits_remaining() < skip {
return Err(InflateError::UnexpectedEof);
}
bits.consume(skip);
let mut out_pos = prefix_len;
loop {
unsafe { bits.refill() };
if bits.bits_remaining() < 3 {
return Err(InflateError::UnexpectedEof);
}
let bfinal = bits.take(1);
let btype = bits.take(2);
match btype {
0 => out_pos = decode_stored(&mut bits, output, out_pos)?,
1 => {
fixed::load_fixed_tables(tables);
out_pos +=
unsafe { fastloop::inflate_fast(&mut bits, tables, output, out_pos) }?;
}
2 => {
decode_dynamic_header(&mut bits, tables)?;
out_pos +=
unsafe { fastloop::inflate_fast(&mut bits, tables, output, out_pos) }?;
}
_ => return Err(InflateError::InvalidBlockType),
}
// SAFETY: `base` is the start of `slice`, the reader's input.
let end_bit = byte0 * 8 + unsafe { bits.consumed_bits(base) };
if bfinal != 0 || end_bit >= stop_bit {
return Ok(BitsDecoded {
written: out_pos - prefix_len,
end_bit,
final_block: bfinal != 0,
});
}
}
})
}
fn inflate_impl(
compressed: &[u8],
output: &mut [u8],
allow_partial: bool,
) -> Result<usize, InflateError> {
inflate_impl_at(compressed, output, 0, allow_partial, 0)
}
/// Core implementation with configurable start position and output limit.
/// `start_pos`: where to begin writing (prefix window lives before this).
/// `limit`: if > 0, stop after this many new bytes written.
fn inflate_impl_at(
compressed: &[u8],
output: &mut [u8],
start_pos: usize,
allow_partial: bool,
limit: usize,
) -> Result<usize, InflateError> {
tables::with_tables(|tables| {
let mut bits = BitReader::new(compressed);
let mut out_pos = start_pos;
let stop_at = if limit > 0 { start_pos + limit } else { 0 };
loop {
// Check output limit.
if stop_at > 0 && out_pos >= stop_at {
return Ok(out_pos - start_pos);
}
// Ensure we have bits for block header.
unsafe { bits.refill() };
if bits.bits_remaining() < 3 {
if allow_partial && out_pos > start_pos {
return Ok(out_pos - start_pos);
}
return Err(InflateError::UnexpectedEof);
}
let bfinal = bits.take(1);
let btype = bits.take(2);
match btype {
0 => {
// Stored block.
out_pos = decode_stored(&mut bits, output, out_pos)?;
}
1 => {
// Fixed Huffman.
fixed::load_fixed_tables(tables);
let written =
unsafe { fastloop::inflate_fast(&mut bits, tables, output, out_pos) }?;
out_pos += written;
}
2 => {
// Dynamic Huffman.
decode_dynamic_header(&mut bits, tables)?;
let written =
unsafe { fastloop::inflate_fast(&mut bits, tables, output, out_pos) }?;
out_pos += written;
}
_ => return Err(InflateError::InvalidBlockType),
}
if bfinal != 0 {
break;
}
}
Ok(out_pos - start_pos)
})
}
/// Convenience: decompress into a newly allocated Vec.
///
/// `expected_size` is almost always a **header field** — the ZIP central
/// directory's uncompressed size, a gzip ISIZE trailer — so it is chosen by
/// whoever wrote the file, and it lands directly on an allocator here. It is
/// therefore bounded against the only thing that cannot lie: how much output
/// `compressed.len()` bytes of DEFLATE could physically produce
/// ([`limits::max_inflated_size`], 1032:1). A claim above that is refused with
/// [`InflateError::ImplausibleSize`] *before* the allocation, not after it.
///
/// This is a bound, not a policy: no legitimate archive is refused, because a
/// legitimate header states a size its own stream can produce. Callers that
/// merely want the hint trimmed (they hold the bytes and have a growing
/// fallback) want [`limits::clamp_expected_size`] instead.
pub fn inflate_to_vec(compressed: &[u8], expected_size: usize) -> Result<Vec<u8>, InflateError> {
// Checked here, not below: `expected_size + OVERWRITE_HEADROOM` was also an
// unchecked add, so a size near usize::MAX wrapped to a *tiny* buffer.
let total = limits::check_expected_size(compressed.len(), expected_size)?;
let mut output = Vec::with_capacity(total);
// SAFETY: inflate_into writes to the buffer and we truncate to `written` bytes.
// The OVERWRITE_HEADROOM may contain uninitialized overwrite bytes from SIMD,
// but truncate() ensures they're never exposed. The hot path must not pay for
// zero-initializing a buffer that inflate_into is about to overwrite, so we
// deliberately accept the uninit_vec pattern here.
#[allow(clippy::uninit_vec)]
unsafe {
output.set_len(total);
}
let written = inflate_into(compressed, &mut output)?;
output.truncate(written);
// Introspection marker: a full-buffer inflate completed.
#[cfg(feature = "testmatrix")]
functional_status(
"linflate",
"inflate_to_vec",
true,
&format!("in={} out={} bytes", compressed.len(), written),
);
Ok(output)
}
// ── Stored block decode ──────────────────────────────────────────────────────
fn decode_stored(
bits: &mut BitReader,
output: &mut [u8],
mut out_pos: usize,
) -> Result<usize, InflateError> {
// Align to byte boundary (discard partial-byte bits).
bits.align_to_byte();
// Need at least 32 bits for LEN + NLEN.
if bits.bits_remaining() < 32 {
unsafe { bits.refill() };
}
if bits.bits_remaining() < 32 {
return Err(InflateError::UnexpectedEof);
}
let len = bits.take_u16() as usize;
let nlen = bits.take_u16() as usize;
if len != (!nlen & 0xFFFF) {
return Err(InflateError::InvalidStoredLength);
}
if out_pos + len > output.len() {
return Err(InflateError::OutputOverflow);
}
// Copy `len` bytes. The refill machinery may already hold some of the
// payload in the bit buffer, so first drain those whole bytes; the buffer is
// byte-aligned here, so it empties cleanly (bits_remaining hits 0, never 1..7).
let mut remaining = len;
while remaining > 0 && bits.bits_remaining() >= 8 {
output[out_pos] = bits.take(8) as u8;
out_pos += 1;
remaining -= 1;
}
// The rest is a byte-aligned run sitting in the input remainder: memcpy it
// straight across and advance the reader past it. (A stored block is
// byte-aligned by definition, so no bit-shifting is needed — reading it back
// through the 8-bits-at-a-time refill path would be both slower and, near
// EOF, unable to satisfy an 8-bit refill on the final partial word.)
if remaining > 0 {
let ptr = bits.input_ptr();
let avail = unsafe { bits.input_end().offset_from(ptr) } as usize;
if avail < remaining {
return Err(InflateError::UnexpectedEof);
}
// SAFETY: `avail >= remaining` guarantees `ptr..ptr+remaining` is in
// bounds of the input, and `out_pos + remaining <= output.len()` was
// checked above. The bit buffer is drained (bits_remaining == 0), so
// `advance_bytes` correctly resynchronises the reader.
unsafe {
core::ptr::copy_nonoverlapping(ptr, output.as_mut_ptr().add(out_pos), remaining);
bits.advance_bytes(remaining);
}
out_pos += remaining;
}
Ok(out_pos)
}
// ── Dynamic Huffman header decode ────────────────────────────────────────────
/// Code-length alphabet order (RFC 1951 §3.2.7).
static CODELEN_ORDER: [usize; 19] = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
fn decode_dynamic_header(
bits: &mut BitReader,
tables: &mut DecompressTables,
) -> Result<(), InflateError> {
unsafe { bits.refill() };
if bits.bits_remaining() < 14 {
return Err(InflateError::UnexpectedEof);
}
let hlit = bits.take(5) as usize + 257; // 257..286
let hdist = bits.take(5) as usize + 1; // 1..32
let hclen = bits.take(4) as usize + 4; // 4..19
if hlit > 286 || hdist > 32 {
return Err(InflateError::InvalidCodeLengths);
}
// Step 1: Read code-length code lengths (3 bits each).
let mut codelen_lens = [0u8; 19];
for i in 0..hclen {
if bits.bits_remaining() < 3 {
unsafe { bits.refill() };
}
codelen_lens[CODELEN_ORDER[i]] = bits.take(3) as u8;
}
// Build the code-length decode table.
tables::build_decode_table(
&codelen_lens,
&mut tables.precode,
tables::PRECODE_TABLEBITS,
tables::TableKind::Precode,
)?;
// Step 2: Read litlen + dist code lengths using the code-length table.
let total = hlit + hdist;
let mut lens_buf = [0u8; 286 + 32]; // max litlen(286) + dist(32) = 318, on stack
let lens = &mut lens_buf[..total];
let mut i = 0;
while i < total {
if bits.bits_remaining() < 15 {
unsafe { bits.refill() };
}
let idx = (bits.raw_buf() as u32) & ((1u32 << tables::PRECODE_TABLEBITS) - 1);
let entry = tables.precode[idx as usize];
let code_len = entry & 0xF;
bits.consume(code_len);
let sym = (entry >> 16) & 0xFF;
match sym as usize {
0..=15 => {
lens[i] = sym as u8;
i += 1;
}
16 => {
// Repeat previous length 3-6 times.
if bits.bits_remaining() < 2 {
unsafe { bits.refill() };
}
let repeat = bits.take(2) as usize + 3;
if i == 0 || i + repeat > total {
return Err(InflateError::InvalidCodeLengths);
}
let prev = lens[i - 1];
for _ in 0..repeat {
lens[i] = prev;
i += 1;
}
}
17 => {
// Repeat 0 for 3-10 times.
if bits.bits_remaining() < 3 {
unsafe { bits.refill() };
}
let repeat = bits.take(3) as usize + 3;
if i + repeat > total {
return Err(InflateError::InvalidCodeLengths);
}
for _ in 0..repeat {
lens[i] = 0;
i += 1;
}
}
18 => {
// Repeat 0 for 11-138 times.
if bits.bits_remaining() < 7 {
unsafe { bits.refill() };
}
let repeat = bits.take(7) as usize + 11;
if i + repeat > total {
return Err(InflateError::InvalidCodeLengths);
}
for _ in 0..repeat {
lens[i] = 0;
i += 1;
}
}
_ => return Err(InflateError::InvalidCodeLengths),
}
}
// Step 3: Build litlen and dist tables.
let litlen_lens = &lens[..hlit];
let dist_lens = &lens[hlit..];
tables::build_decode_table(
litlen_lens,
&mut tables.litlen,
tables::LITLEN_TABLEBITS,
tables::TableKind::Litlen,
)?;
tables::build_decode_table(
dist_lens,
&mut tables.dist,
tables::DIST_TABLEBITS,
tables::TableKind::Dist,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// Minimal LSB-first DEFLATE bit writer for hand-building streams that end
/// in a stored (uncompressed) block. Only what the stored-block regression
/// tests need: raw bits, fixed-Huffman codes (MSB-first), and byte-align.
struct DeflateBitWriter {
bytes: Vec<u8>,
cur: u8,
nbits: u8,
}
impl DeflateBitWriter {
fn new() -> Self {
Self {
bytes: Vec::new(),
cur: 0,
nbits: 0,
}
}
fn bit(&mut self, b: u32) {
self.cur |= ((b & 1) as u8) << self.nbits;
self.nbits += 1;
if self.nbits == 8 {
self.bytes.push(self.cur);
self.cur = 0;
self.nbits = 0;
}
}
fn bits(&mut self, v: u32, n: u32) {
for i in 0..n {
self.bit(v >> i);
}
}
/// Emit a Huffman code (packed MSB-first per RFC 1951 §3.1.1).
fn huff(&mut self, code: u32, len: u32) {
for i in (0..len).rev() {
self.bit(code >> i);
}
}
fn align(&mut self) {
if self.nbits > 0 {
self.bytes.push(self.cur);
self.cur = 0;
self.nbits = 0;
}
}
}
/// Regression: a valid DEFLATE stream whose final block is a *stored*
/// (uncompressed) block whose payload runs to end-of-input must decode.
///
/// `decode_stored` used to memcpy the byte-aligned remainder correctly and
/// then redundantly re-copy it 8 bits at a time through the refill path,
/// bailing with `UnexpectedEof` when the last partial machine word couldn't
/// satisfy an 8-bit refill. The fix drops the re-copy and advances the reader
/// past the memcpy'd bytes. We sweep the *bit alignment* of the stored block
/// (0..16 preceding fixed-Huffman literals shift where the byte-aligned copy
/// starts) crossed with the payload length (0..600 bytes, spanning the
/// in-buffer / direct-copy / near-EOF-partial-word cases).
#[test]
fn inflate_stored_final_block_to_eof() {
for nlit in 0usize..16 {
for np in 0usize..600 {
let mut w = DeflateBitWriter::new();
// Fixed-Huffman block (BFINAL=0, BTYPE=01) carrying `nlit`
// literals + EOB, to bit-shift the following stored block.
w.bit(0);
w.bits(1, 2);
let lits: Vec<u8> = (0..nlit).map(|i| ((i * 17 + 3) % 144) as u8).collect();
for &l in &lits {
w.huff(0x30 + l as u32, 8);
} // literals 0..143 => 8-bit codes
w.huff(0, 7); // end-of-block symbol (256)
// Final stored block (BFINAL=1, BTYPE=00), byte-aligned.
w.bit(1);
w.bits(0, 2);
w.align();
let len = np as u16;
w.bytes.extend_from_slice(&len.to_le_bytes());
w.bytes.extend_from_slice(&(!len).to_le_bytes());
let payload: Vec<u8> = (0..np).map(|i| (i * 31 + 7) as u8).collect();
w.bytes.extend_from_slice(&payload); // payload ends exactly at EOF
let total = nlit + np;
let mut out = vec![0u8; total + OVERWRITE_HEADROOM];
let wn = inflate_into(&w.bytes, &mut out)
.unwrap_or_else(|e| panic!("nlit={} np={} failed: {:?}", nlit, np, e));
assert_eq!(wn, total, "nlit={} np={}", nlit, np);
assert_eq!(&out[..nlit], &lits[..], "literals nlit={} np={}", nlit, np);
assert_eq!(
&out[nlit..total],
&payload[..],
"payload nlit={} np={}",
nlit,
np
);
}
}
}
#[test]
fn inflate_empty_stored() {
// A stored block with 0 bytes: BFINAL=1, BTYPE=00, LEN=0, NLEN=0xFFFF
let data = [
0b00000001u8, // BFINAL=1 (bit 0), BTYPE=00 (bits 1-2), padding zeros
0x00,
0x00, // LEN = 0
0xFF,
0xFF, // NLEN = ~0
];
let mut out = vec![0u8; OVERWRITE_HEADROOM];
let written = inflate_into(&data, &mut out).expect("stored block");
assert_eq!(written, 0);
}
#[test]
fn inflate_stored_hello() {
// Stored block: BFINAL=1, BTYPE=00, LEN=5, NLEN=~5, "Hello"
let mut data = vec![0b00000001u8]; // BFINAL=1, BTYPE=00
let len: u16 = 5;
data.extend_from_slice(&len.to_le_bytes());
data.extend_from_slice(&(!len).to_le_bytes());
data.extend_from_slice(b"Hello");
let mut out = vec![0u8; 5 + OVERWRITE_HEADROOM];
let written = inflate_into(&data, &mut out).expect("stored hello");
assert_eq!(written, 5);
assert_eq!(&out[..5], b"Hello");
}
#[test]
fn inflate_fixed_roundtrip() {
let original = b"The quick brown fox jumps over the lazy dog. \
The quick brown fox jumps over the lazy dog.";
// Use compression level 1 to get fixed Huffman blocks.
let compressed = miniz_oxide::deflate::compress_to_vec(original, 1);
let mut out = vec![0u8; original.len() + OVERWRITE_HEADROOM];
let written = inflate_into(&compressed, &mut out).expect("inflate fixed");
assert_eq!(written, original.len());
assert_eq!(&out[..written], original.as_slice());
}
#[test]
fn inflate_dynamic_roundtrip() {
let original = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ".repeat(100);
// Level 6 typically produces dynamic Huffman blocks.
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let mut out = vec![0u8; original.len() + OVERWRITE_HEADROOM];
let written = inflate_into(&compressed, &mut out).expect("inflate dynamic");
assert_eq!(written, original.len());
assert_eq!(&out[..written], original.as_slice());
}
/// **A lying header size must be refused before it is allocated.**
///
/// `inflate_to_vec`'s `expected_size` is a header field in every real
/// caller (ZIP central-directory uncompressed size in ljar and
/// lzip-parallel, gzip ISIZE in lgz), so it is attacker-chosen. It reached
/// `Vec::with_capacity` + `set_len` with nothing in between.
///
/// Seen red with the `check_expected_size` call in `inflate_to_vec` replaced
/// by the old `expected_size + OVERWRITE_HEADROOM`: every case below came
/// back **`Ok`** — the crate reserved 8 GiB, 512 GiB and 4 PB of address
/// space for a 60-byte stream, decoded 3 000 bytes into it and returned
/// happily. That is the distinction this test exists to draw: the failure
/// mode was never an `Err`, it was a *successful* absurd allocation, so an
/// `is_err()`-shaped test would have proved nothing. (The 4 PB case is also
/// why the process must run under `ulimit -v`; unbounded it is the 12:32
/// OOM.)
#[test]
fn a_lying_expected_size_is_refused_before_it_is_allocated() {
let original = b"payload".repeat(500); // 3500 bytes
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let max = crate::limits::max_inflated_size(compressed.len());
for claim in [
max + 1,
8usize << 30, // 8 GiB — fits under a 24 GiB -v cap, so the
512usize << 30, // old code really did allocate it
4_000_000_000_000_000, // ~4 PB, the lgz ISIZE-sum shape
usize::MAX, // and the headroom-add wrap
] {
let e = inflate_to_vec(&compressed, claim)
.expect_err("a size this stream cannot produce must be refused");
match e {
InflateError::ImplausibleSize {
expected,
compressed: c,
max: m,
} => {
assert_eq!(expected, claim);
assert_eq!(c, compressed.len());
assert_eq!(m, max);
}
other => panic!("claim={claim}: allocated first and failed later ({other})"),
}
}
// Not vacuous, twice over: the honest size still works, and so does the
// most compressible input that exists (zeros, ~158:1 — far above any
// ratio a bound this crate ships must ever refuse).
assert_eq!(
inflate_to_vec(&compressed, original.len()).unwrap(),
original
);
let zeros = vec![0u8; 4 << 20];
let zc = miniz_oxide::deflate::compress_to_vec(&zeros, 6);
assert!(
zc.len() * crate::limits::MAX_DEFLATE_RATIO >= zeros.len(),
"the ratio bound would refuse 4 MiB of zeros ({} compressed) — it is too tight",
zc.len()
);
assert_eq!(inflate_to_vec(&zc, zeros.len()).unwrap().len(), zeros.len());
}
#[test]
fn inflate_to_vec_convenience() {
let original = b"Hello, World!".repeat(50);
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let result = inflate_to_vec(&compressed, original.len()).expect("inflate_to_vec");
assert_eq!(result, original);
}
#[test]
fn inflate_large_data() {
// 64 KB of pseudo-random data to exercise match copies.
let mut original = vec![0u8; 65536];
for (i, b) in original.iter_mut().enumerate() {
*b = ((i * 7 + 13) % 256) as u8;
}
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let result = inflate_to_vec(&compressed, original.len()).expect("large inflate");
assert_eq!(result.len(), original.len());
assert_eq!(result, original);
}
#[test]
fn inflate_all_zeros() {
// All-zeros: tests RLE (dist=1) match copy path.
let original = vec![0u8; 32768];
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let result = inflate_to_vec(&compressed, original.len()).expect("all zeros");
assert_eq!(result, original);
}
/// Regression: the fast-loop literal-burst path requires the bit buffer to
/// be refilled to the full ≥56-bit budget at the top of each iteration.
/// The 1- and 2-literal exit paths used to refill only conditionally (when
/// < 32 bits), which could leave as few as ~10 bits for the next iteration's
/// speculative `entry2` decode. With LITLEN_TABLEBITS = 11, an 11-bit literal
/// code would then be indexed with a zero in its top bit → wrong symbol and a
/// silently corrupt (and length-shifted) output. This deterministic,
/// multibyte-heavy corpus reproduces the exact bit alignment that triggered
/// the bug on real (pigz / zlib level-6) gzip streams.
#[test]
fn inflate_burst_refill_regression() {
// Deterministic multibyte text: diverse Unicode (Latin/Cyrillic/CJK/
// Arabic/Hiragana) produces a deep literal Huffman tree (11-bit codes)
// interleaved with literal runs (the burst path).
let mut original = Vec::new();
let mut s: u64 = 18 | 1;
let mut rng = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s
};
let tags = [
"name", "name:en", "name:de", "name:ru", "name:zh", "name:ar", "name:ja", "alt_name",
];
while original.len() < 200_000 {
original.extend_from_slice(b" <tag k=\"");
original.extend_from_slice(tags[(rng() as usize) % tags.len()].as_bytes());
original.extend_from_slice(b"\" v=\"");
let words = (rng() % 4 + 1) as usize;
for _ in 0..words {
let len = (rng() % 8 + 2) as usize;
for _ in 0..len {
let cp = match rng() % 5 {
0 => 0x41 + (rng() % 26),
1 => 0x400 + (rng() % 0x60),
2 => 0x4E00 + (rng() % 0x500),
3 => 0x600 + (rng() % 0x50),
_ => 0x3040 + (rng() % 0x90),
};
if let Some(c) = char::from_u32(cp as u32) {
let mut b = [0u8; 4];
original.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
}
}
original.push(b' ');
}
original.extend_from_slice(b"\"/>\n");
}
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 1);
let result = inflate_to_vec(&compressed, original.len()).expect("burst regression");
assert_eq!(result.len(), original.len(), "length must match exactly");
assert_eq!(result, original, "byte-exact decode (no burst corruption)");
}
#[test]
fn inflate_short_repeats() {
// Pattern with dist 2..7 back-references.
let mut original = Vec::with_capacity(4096);
for _ in 0..512 {
original.extend_from_slice(b"ABCABCABC");
}
let compressed = miniz_oxide::deflate::compress_to_vec(&original, 6);
let result = inflate_to_vec(&compressed, original.len()).expect("short repeats");
assert_eq!(result, original);
}
#[test]
fn inflate_vs_miniz_many_sizes() {
// Test our inflate against miniz_oxide across many data sizes and patterns.
let patterns: Vec<Vec<u8>> = vec![
// Java class file-like: starts with cafebabe, then mixed data
{
let mut v = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34];
for i in 0..2000 {
v.push((i * 37 + 13) as u8);
}
v
},
// Highly repetitive
b"package org.json;\nimport java.util.*;\n".repeat(200),
// Mixed: some unique, some repeated
{
let mut v = Vec::with_capacity(16384);
for i in 0..4096 {
if i % 10 < 3 {
v.extend_from_slice(&[0u8; 4]);
} else {
v.push((i * 7 + 3) as u8);
}
}
v
},
];
for (idx, original) in patterns.iter().enumerate() {
for level in [1, 6, 9] {
let compressed = miniz_oxide::deflate::compress_to_vec(original, level);
let miniz_out = miniz_oxide::inflate::decompress_to_vec(&compressed).unwrap();
let our_out = inflate_to_vec(&compressed, original.len())
.unwrap_or_else(|e| panic!("pattern {idx} level {level}: {e:?}"));
assert_eq!(
our_out,
miniz_out,
"pattern {idx} level {level}: output mismatch (lens {} vs {})",
our_out.len(),
miniz_out.len()
);
}
}
}
// `inflate_real_jar_entry` lived here and read its fixture from
// `/tmp/xml_class_compressed.bin`, answering the always-missing file with a
// bare `return` — so it printed `ok` on every run while asserting nothing,
// and a world-writable path decided what "correct output" meant. The real
// proof is now `tests/real_world_deflate.rs`: a GNU-gzip-produced stream
// checked against gzip's own CRC-32, plus a genuine JAR built from this
// crate's committed sources, with non-vacuity floors and no skippable
// precondition.
}