linflate/src/limits.rs
//! **The plausibility ceiling on a claimed uncompressed size** — the one place
//! this crate refuses to allocate a buffer that the input could never fill.
//!
//! Every convenience entry point here takes an `expected_size` that its caller
//! read out of a *header*: the ZIP central directory's uncompressed-size field
//! (ljar, lzip-parallel), the gzip ISIZE trailer (lgz). Those are four or eight
//! bytes of **attacker-chosen data at a known offset** — not a caller mistake,
//! a file property. Handed straight to an allocator they turn an 18 MB archive
//! into a request for petabytes.
//!
//! Same shape as the `ldeflate` piece-count OOM on 2026-08-03 (~497 GB
//! allocated, kernel OOM killer, three unrelated tmux sessions gone) and as the
//! `znippy-zip-core` lying-EOCD fix: a header number reaching an allocator with
//! nothing in between.
//!
//! # Why this bound is against reality, not a constant
//!
//! A DEFLATE stream cannot expand without limit. Its maximum expansion ratio is
//! **1032:1** — the figure zlib itself documents, reached only by a stream of
//! back-to-back maximum-length (258 byte) matches encoded in the shortest
//! possible dynamic-Huffman codes. So `compressed.len()` is a *hard physical
//! ceiling* on the output: no arbitrary constant is needed, and no legitimate
//! archive is ever refused, because a legitimate header states a size the stream
//! can actually produce.
//!
//! That is the same discipline `znippy_zip_core::central_dir` uses — bound the
//! claim against the source's real length — rather than picking a number.
//!
//! # Error, not clamp — here
//!
//! [`check_expected_size`] returns `Err`. A caller who asked for a buffer the
//! stream cannot fill is holding a lying header and wants to know, and the
//! message names the claim, the input it is impossible for, the ceiling and the
//! largest value that would be accepted.
//!
//! [`clamp_expected_size`] exists beside it for the *one* situation where
//! clamping is provably lossless: a caller that (a) already holds the compressed
//! bytes and (b) falls back to a growing decoder on `OutputOverflow`. Shrinking
//! a hint to the physical maximum cannot lose output, because output above the
//! physical maximum cannot exist. The two extractor binaries take that path; a
//! library returning a `Vec` does not.
use crate::{InflateError, OVERWRITE_HEADROOM};
/// The largest number of output bytes one input byte of DEFLATE can produce:
/// **1032**.
///
/// This is zlib's documented worst case, not an estimate: a maximum-length
/// match (258 bytes) costs at least two bits of length code plus the distance
/// code, so `258 * 8 / 2 = 1032` bytes out per byte in is the ceiling the format
/// itself imposes. Real data is nowhere near it — a megabyte of zeros deflates
/// at roughly 158:1 — so the bound has ~6.5x of headroom over the most
/// compressible input that exists.
pub const MAX_DEFLATE_RATIO: usize = 1032;
/// Below this, the ratio bound is not applied.
///
/// The crate's own "no size hint" heuristics guess `(compressed.len() * 4).max(4096)`
/// and `(compressed.len() * 4).max(64 * 1024)`. On a handful of input bytes those
/// floors legitimately exceed `len * 1032`, and a guard that rejected the crate's
/// own default guess would be a bug, not a bound. 64 KiB per call is noise next to
/// the petabyte-class requests this module exists to stop.
pub const MIN_PLAUSIBLE_OUTPUT: usize = 64 * 1024;
/// The largest output `compressed_len` bytes of DEFLATE could possibly produce.
///
/// Saturating, so a pathological length cannot wrap the multiply into a small
/// number — which would turn a ceiling into a floor.
#[inline]
pub fn max_inflated_size(compressed_len: usize) -> usize {
compressed_len
.saturating_mul(MAX_DEFLATE_RATIO)
.max(MIN_PLAUSIBLE_OUTPUT)
}
/// Validate a header-supplied `expected` output size against the compressed
/// bytes it claims to describe, and return the buffer length to allocate
/// (`expected + OVERWRITE_HEADROOM`).
///
/// Refuses rather than clamps: see the module docs. Also the one place the
/// `+ OVERWRITE_HEADROOM` addition is checked — `expected` near `usize::MAX`
/// used to wrap it to a *tiny* buffer, which is worse than a big one.
#[inline]
pub fn check_expected_size(compressed_len: usize, expected: usize) -> Result<usize, InflateError> {
let max = max_inflated_size(compressed_len);
if expected > max {
return Err(InflateError::ImplausibleSize {
expected,
compressed: compressed_len,
max,
});
}
expected
.checked_add(OVERWRITE_HEADROOM)
.ok_or(InflateError::ImplausibleSize {
expected,
compressed: compressed_len,
max,
})
}
/// Shrink a header-supplied size *hint* to the physical maximum.
///
/// **Only sound for a caller that holds `compressed` and has a growing fallback**
/// — see the module docs. Lossless by construction: the returned buffer is large
/// enough for every byte the stream can emit, because a stream cannot emit more
/// than [`max_inflated_size`] bytes.
#[inline]
pub fn clamp_expected_size(compressed_len: usize, expected: usize) -> usize {
expected.min(max_inflated_size(compressed_len))
}
#[cfg(test)]
mod tests {
use super::*;
/// The ceiling arithmetic itself: saturating, floored, and monotone.
#[test]
fn max_inflated_size_is_the_ratio_bound_it_claims_to_be() {
assert_eq!(max_inflated_size(0), MIN_PLAUSIBLE_OUTPUT);
assert_eq!(
max_inflated_size(1),
MIN_PLAUSIBLE_OUTPUT,
"floored, not 1032"
);
assert_eq!(max_inflated_size(1_000_000), 1_032_000_000);
assert_eq!(
max_inflated_size(usize::MAX),
usize::MAX,
"a huge length must saturate UP, never wrap to a small ceiling"
);
}
/// The boundary, from both sides — a ceiling that only ever says no is as
/// useless as one that only ever says yes.
#[test]
fn the_ceiling_accepts_at_max_and_refuses_at_max_plus_one() {
let comp = 1_000_000usize;
let max = max_inflated_size(comp);
assert_eq!(
check_expected_size(comp, max).unwrap(),
max + OVERWRITE_HEADROOM
);
let e = check_expected_size(comp, max + 1).unwrap_err();
assert!(
matches!(e, InflateError::ImplausibleSize { expected, max: m, .. }
if expected == max + 1 && m == max),
"wrong error for a one-over request: {e}"
);
}
/// `expected + OVERWRITE_HEADROOM` used to be an unchecked add. A size near
/// `usize::MAX` wrapped it to a *tiny* buffer — a silently undersized
/// allocation, which is worse than a huge one. Both the ratio bound and the
/// checked add must stop it.
#[test]
fn a_size_near_usize_max_cannot_wrap_the_headroom_addition() {
for expected in [
usize::MAX,
usize::MAX - 1,
usize::MAX - OVERWRITE_HEADROOM + 1,
] {
let e = check_expected_size(4096, expected).unwrap_err();
assert!(
matches!(e, InflateError::ImplausibleSize { .. }),
"expected={expected}: {e}"
);
}
}
/// The message has to be actionable without a backtrace: the claim, the
/// input it is impossible for, and the largest value that would be taken.
#[test]
fn the_error_names_the_claim_the_input_and_the_ceiling() {
let e = check_expected_size(18_000_000, 4_000_000_000_000_000).unwrap_err();
let m = e.to_string();
for needle in ["4000000000000000", "18000000", "18576000000", "1032"] {
assert!(
m.contains(needle),
"message {m:?} does not mention {needle:?}"
);
}
// And the ceiling it names must actually be accepted.
assert!(check_expected_size(18_000_000, 18_576_000_000).is_ok());
}
/// Not vacuous: every ratio DEFLATE can actually reach must still pass. A
/// megabyte of zeros deflates at ~158:1; the bound has to clear that by a
/// wide margin or it would refuse real archives.
#[test]
fn the_ceiling_still_admits_every_ratio_deflate_can_reach() {
assert!(
check_expected_size(1 << 20, 1032 << 20).is_ok(),
"the exact 1032:1 worst case"
);
assert!(
check_expected_size(6_600, 1 << 20).is_ok(),
"1 MiB of zeros at ~158:1"
);
assert!(
check_expected_size(1 << 20, 1 << 20).is_ok(),
"incompressible, 1:1"
);
assert!(check_expected_size(0, 0).is_ok(), "an empty stream");
}
/// The clamp is the *lossless* variant: it never returns less than the
/// stream can produce, and never more than the ratio allows.
#[test]
fn the_clamp_shrinks_only_impossible_hints() {
assert_eq!(
clamp_expected_size(1000, 500),
500,
"an honest hint is untouched"
);
assert_eq!(clamp_expected_size(1 << 20, 4_000_000_000_000), 1032 << 20);
assert_eq!(
clamp_expected_size(1 << 20, 1032 << 20),
1032 << 20,
"the exact max survives"
);
}
}