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

nordisk/gunnar

crates/gunnar-lfs/src/allowance.rs

//! **How much room the TENANT has left**, as distinct from how large an object
//! this SERVER accepts.
//!
//! [`crate::Limits`] is the operator's: one set of numbers for the whole box,
//! the same for every namespace, and `max_object_bytes` is a wall around one
//! object. This is the other axis — one namespace's remaining plan, which is a
//! different number for every tenant, changes when they pay, and bounds what
//! they may ACCUMULATE rather than what one request may carry.
//!
//! # Why this exists at all
//!
//! git bytes and LFS bytes land on the same filesystem, are reported by the same
//! `Usage.Read` (as `pack_bytes` and `lfs_bytes`), and — this is the part that
//! made it urgent — are billed at the same `disk_gib_month` on monetize's price
//! list. Only one of them had a door. `gunnar_wire::receive_pack` has counted
//! pack bytes against a plan ceiling and cut the transfer mid-stream since
//! `9f948a2`; the LFS wire counted nothing. A tenant one byte under a 10 GiB
//! plan could open an upload session and put 8 GiB in it — under
//! `max_object_bytes`, so not refused — and then open another.
//!
//! > *"i believe if each user crosses its effective allowed space the operation
//! > should not be allowed"* — the ruling of 2026-09-07
//!
//! # The number is decided elsewhere, deliberately
//!
//! This crate does not know what a plan is, what monetize is, or what a tenant
//! holds on disk. It is handed a byte count and a sentence, exactly as
//! `gunnar_wire::PackCeiling` is, and for the same reason: the arithmetic that
//! produces them needs the signed entitlement fact and a walk of the namespace's
//! repositories, both of which live in `gunnar-server`. Putting it here would
//! drag the entitlement cache into the crate that serves `git-lfs-transfer`, and
//! then a community build could not compile without it.
//!
//! # Exact, with no slack — and that is a fact about LFS, not a preference
//!
//! `gunnar_server::entitlement::EntitlementSlot::pack_ceiling` gives a push
//! 6.25 % of slack because a pack's wire bytes and its stored bytes are
//! genuinely different numbers that nobody knows before the transfer: a thin
//! pack is completed server-side with bases the client never sent, and the
//! engine then stores the result compressed. **An LFS object has no such gap.**
//! `crate::store` stores objects RAW — measured, level-6 DEFLATE over real LFS
//! content came out at ratio 1.000 — and [`crate::VerifyingSink`] checks the
//! bytes against the declared size as they pass, so a client that declares 4 KiB
//! and sends 4 GiB is cut at the first byte past the declaration. Declared bytes
//! ARE the bytes that land. The ceiling can therefore be exact, and an exact
//! ceiling is the only kind a tenant can plan against.

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

/// **A namespace's remaining room, and the sentence a tenant who runs out
/// reads.**
///
/// Two fields and no methods but one, because there is no policy here: the
/// policy is `gunnar-server`'s, which computed [`Allowance::bytes`] from the
/// signed fact's `caps["lfs_bytes"]` minus what the namespace holds, and wrote
/// [`Allowance::refusal`] naming the plan, the cap and the pay page. This type
/// is the number travelling to where the bytes are — the deliberate twin of
/// `gunnar_wire::PackCeiling`, which does the same job one wire over.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Allowance {
    /// Bytes this session may still commit. **Inclusive**: an upload that
    /// brings the running total to exactly this number lands, and the tenant is
    /// refused on the next one. `0` is a real zero — the tenant is exactly at
    /// their cap — and NOT the "0 means unlimited" that a `caps` map uses; a
    /// namespace with no cap simply has no `Allowance` at all.
    pub bytes: u64,
    /// What the client is told past it. A whole refusal, naming the plan, the
    /// cap, what is held and where to buy more, because a tenant cut off here
    /// must not be left reading `insufficient storage` — which says the
    /// operator's disk is full and invites a support ticket that no amount of
    /// paying would close.
    pub refusal: String,
}

impl Allowance {
    /// **May one more object of `size` bytes land, given `taken` already has?**
    ///
    /// `Err` is [`Error::NoRoomLeft`] carrying [`Allowance::refusal`] verbatim,
    /// which is 507 on both wires and is NOT fatal to the session: the tenant is
    /// told, the payload they were already sending is drained, and the next
    /// command is answered. See [`Error::OutOfSpace`] for why 507 rather than
    /// [`Error::TooLarge`]'s 413 — 413 is a policy of the operator's that is
    /// identical tomorrow, this is a fact about one plan that a purchase changes
    /// in a minute.
    ///
    /// `saturating_add` because `size` is a number a hostile client declared:
    /// `u64::MAX` must refuse, not wrap into room that is not there.
    /// `tests/quota.rs` already pins that `max_object_bytes` survives the same
    /// declaration, and this is the second gate on the same untrusted integer.
    pub fn admit(&self, taken: u64, size: u64) -> Result<()> {
        if taken.saturating_add(size) > self.bytes {
            return Err(Error::NoRoomLeft(self.refusal.clone()));
        }
        Ok(())
    }
}