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

nordisk/edda

crates/gunnar-delta/src/limits.rs

//! Hard caps: what stands between a sliding window and an OOM kill.
//!
//! # Why this module exists at all
//!
//! Delta search is the classic shape where a parameter that reads as a tuning
//! knob is actually a memory multiplier. `window` alone is harmless; `window` ×
//! `object size` × `threads` is a product, and every one of its factors is
//! caller-supplied. A test in this tree took a 499 GB machine out on 2026-08-03
//! by crossing a degenerate parameter with a large input, and this is exactly
//! that shape.
//!
//! So the rule here is: **refuse, do not attempt.** Every bound is checked in
//! [`Limits::validate`] *before* the first allocation, and an out-of-bounds
//! request comes back as [`crate::Error::Refused`] naming the field, the value
//! and the cap. There is no "best effort" path and no silent clamping; a clamp
//! would let a caller ask for 100× the memory and never learn it did not get it.

use crate::error::{Error, Result};

/// Bounds on everything that can multiply into memory or time.
///
/// The defaults are sized so that the worst case fits comfortably inside a few
/// GiB with every core busy, which is the configuration a repack runs in. They
/// are *caps*, not settings: normal git parameters (window 10, depth 50) sit
/// two to three orders of magnitude below every one of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
    /// Most objects one [`crate::deltify`] call may consider.
    ///
    /// The per-object bookkeeping is small but not free; at the default this is
    /// under a gigabyte of state before a single object byte is touched.
    pub max_objects: usize,

    /// Largest permitted `window`.
    ///
    /// Time is `O(objects × window)` delta attempts and memory is
    /// `O(threads × window × index_size)`. git's default is 10 and its
    /// aggressive setting is 250; anything past this cap is a typo, not a
    /// request.
    pub max_window: usize,

    /// Largest permitted `depth`.
    ///
    /// Depth costs no memory here, chains being resolved in one serial pass,
    /// but a decoder pays for it on every read, so a runaway value is a
    /// present a caller gives to whoever clones the result. git's default is 50.
    pub max_depth: usize,

    /// Objects larger than this are never used as a delta **base**.
    ///
    /// Indexing a base allocates roughly `len × 0.75`, and the index cache holds
    /// `window + 1` of them per thread. This is the single most important number
    /// in the struct: it is what bounds `threads × window × index_size`.
    /// Exceeding it is not an error; such an object is skipped as a candidate.
    /// Refusing a whole repack because one blob is big would be the wrong
    /// behaviour.
    pub max_base_size: usize,

    /// Objects larger than this are never **deltified**; they go out whole.
    ///
    /// Also not an error, for the same reason. The scan itself is `O(n)` in the
    /// target with a bounded candidate chain, so this bound is about latency and
    /// about not spending a minute on a blob that will not delta anyway.
    pub max_target_size: usize,

    /// Refuse to build a single base index larger than this.
    ///
    /// Redundant with `max_base_size` under the defaults and deliberately kept:
    /// it is the check that still fires if someone raises `max_base_size`
    /// without doing the multiplication.
    pub max_index_bytes: usize,

    /// Byte budget for one thread's index cache.
    ///
    /// When adding an index would exceed it the oldest cached index is dropped,
    /// which costs a rebuild rather than an allocation failure. Multiply by the
    /// thread count for the crate's peak resident set.
    pub index_memory_per_thread: usize,

    /// Refuse a request whose objects sum to more than this.
    ///
    /// The crate never copies object data, the window holding borrowed slices,
    /// so this does not bound *this* crate's allocations. It bounds the caller's,
    /// and it is the one number that catches "someone handed us a planet".
    pub max_total_input: u64,
}

const MIB: usize = 1024 * 1024;

impl Default for Limits {
    fn default() -> Self {
        Limits {
            max_objects: 16_000_000,
            max_window: 1024,
            max_depth: 1000,
            max_base_size: 64 * MIB,
            max_target_size: 64 * MIB,
            max_index_bytes: 96 * MIB,
            index_memory_per_thread: 256 * MIB,
            max_total_input: 64 * 1024 * MIB as u64,
        }
    }
}

impl Limits {
    /// Check one value against one cap, naming the field in the error.
    pub(crate) fn check(what: &'static str, requested: u64, limit: u64) -> Result<()> {
        if requested > limit {
            return Err(Error::Refused {
                what,
                requested,
                limit,
            });
        }
        Ok(())
    }

    /// Reject a request before anything is allocated for it.
    ///
    /// `window` and `depth` come from the caller's options; `objects` and
    /// `total_bytes` from the input.
    pub fn validate(
        &self,
        window: usize,
        depth: usize,
        objects: usize,
        total_bytes: u64,
    ) -> Result<()> {
        Self::check("window", window as u64, self.max_window as u64)?;
        Self::check("depth", depth as u64, self.max_depth as u64)?;
        Self::check("objects", objects as u64, self.max_objects as u64)?;
        Self::check("total input bytes", total_bytes, self.max_total_input)?;
        Ok(())
    }
}