znippy-zip-core/src/limits.rs
//! **The pre-size ceiling** — how much buffer a Central Directory is allowed to
//! ask for before it has produced a single byte to justify it.
//!
//! [`crate::central_dir::EntryLocation`] already bounds every size it publishes
//! against the archive it came from: the compressed bytes must physically fit in
//! the file, and `uncompressed_size` may not exceed what those bytes could
//! produce at DEFLATE's documented 1032:1 worst case. That stops the 16 EiB claim
//! on a 200-byte file. It does not stop the *ratio*: a 4.2 MB archive may legally
//! declare a 4.0 GiB entry and still pass, because 4.2 MB of DEFLATE genuinely
//! could expand that far.
//!
//! Both extractor binaries then did
//!
//! ```ignore
//! let need = out_size + linflate::OVERWRITE_HEADROOM;
//! if out.len() < need { out.resize(need, 0); } // zero-filled → real pages
//! ```
//!
//! on every worker thread at once. Measured on oden, 2026-08-03, on a 4_201_405
//! byte archive holding one entry whose directory claims 4_294_967_294 bytes,
//! under `ulimit -v 2097152`:
//!
//! ```text
//! ljar: 1 entries, 32 parallel writers
//! memory allocation of 4294967584 bytes failed
//! ```
//!
//! One entry. Thirty-two workers and a directory full of them multiply it.
//!
//! # Why clamping is right here, and refusing is not
//!
//! `out_size` reaches those binaries as a **hint**, not as a contract: both
//! already fall back to a growing decoder when `inflate_segment` returns
//! `OutputOverflow`. So shrinking the hint cannot lose output or change a byte —
//! it only decides whether the buffer arrives in one piece or grows into place.
//! That is exactly the situation [`linflate::clamp_expected_size`] documents as
//! the one case where a clamp beats an error. (An archive that really does hold a
//! larger entry still extracts, correctly; it pays one extra geometric growth.)
//!
//! The clamp is against an absolute number rather than against `compressed.len()`
//! for one reason: a bound derived from the input scales *with the attacker's
//! input*. `compressed.len() * k` lets a bigger archive buy a bigger allocation,
//! which is the amplification this module exists to remove. A constant does not.
/// The largest buffer a Central Directory's claim alone may pre-size:
/// **64 MiB**.
///
/// Chosen so that peak memory from a hostile archive is
/// `MAX_PRESIZE * n_workers` and *nothing else* — independent of the archive's
/// size, its entry count, and every number inside it. On a 32-core box that is
/// 2 GiB, against the 1032:1-of-archive-size it replaces.
///
/// 64 MiB is above every entry in a real JAR — the largest class or resource in a
/// published artifact is orders of magnitude below it — so in practice nothing
/// changes for the archives these binaries are pointed at. An entry genuinely
/// larger than this still extracts byte-for-byte via the growing decoder both
/// binaries already fall back to; it pays one extra pass.
pub const MAX_PRESIZE: usize = 64 << 20;
/// The buffer length to pre-size for an entry whose directory claims
/// `claimed_uncompressed` bytes.
///
/// Lossless: see the module docs. The caller must still handle
/// `InflateError::OutputOverflow` by growing — which is what makes it lossless,
/// so a caller that stops doing so has broken this contract, not tightened it.
#[inline]
pub fn presize_for(claimed_uncompressed: usize) -> usize {
claimed_uncompressed.min(MAX_PRESIZE)
}
#[cfg(test)]
mod tests {
use super::*;
/// The clamp, from both sides of its edge — and it must be the identity
/// everywhere below the edge, or every honest extraction pays for it.
#[test]
fn the_presize_is_the_identity_below_the_ceiling_and_the_ceiling_above_it() {
assert_eq!(presize_for(0), 0);
assert_eq!(presize_for(1), 1);
assert_eq!(
presize_for(4 << 20),
4 << 20,
"a 4 MiB entry is pre-sized exactly"
);
assert_eq!(
presize_for(MAX_PRESIZE),
MAX_PRESIZE,
"the exact ceiling survives"
);
assert_eq!(presize_for(MAX_PRESIZE + 1), MAX_PRESIZE);
assert_eq!(presize_for(usize::MAX), MAX_PRESIZE);
}
/// The specific claim that aborted `ljar`, and the property that makes the
/// clamp worth having: the result must not depend on the claim at all once
/// the claim is implausible.
#[test]
fn a_four_gigabyte_claim_pre_sizes_no_differently_from_a_petabyte_one() {
let four_gib = 4_294_967_294usize;
assert_eq!(presize_for(four_gib), MAX_PRESIZE);
assert_eq!(presize_for(four_gib), presize_for(1 << 50));
// A 63x reduction on this exact claim (`4_294_967_294 / 67_108_864`).
// Stated as the ratio rather than as `claim / 64`, which lands one byte
// *below* `MAX_PRESIZE` — that form failed on arithmetic, not behaviour.
assert!(
four_gib / presize_for(four_gib) >= 60,
"the clamp must be a real reduction, not a rounding: {four_gib} -> {}",
presize_for(four_gib)
);
}
/// Not vacuous: the ceiling has to be above what a real archive holds, or it
/// would push every extraction onto the growing path.
#[test]
fn the_ceiling_still_pre_sizes_every_realistic_entry_exactly() {
for entry in [4 << 10, 512 << 10, 8 << 20, 63 << 20] {
assert_eq!(
presize_for(entry),
entry,
"{entry} is an ordinary archive entry"
);
}
}
}