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

nordisk/znippy-zoomies

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