Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

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"
            );
        }
    }
}