gunnar-wire 1.7.0

git smart-protocol state machines: upload-pack (v2 with a v0 fallback) and receive-pack (v0), transport-agnostic
Documentation
//! **A push that crosses its byte ceiling is cut DURING the transfer.**
//!
//! # The hole this covers
//!
//! `git-receive-pack`'s pack header carries an **object count, not a byte
//! count**, so the size of a push is unknown at the moment it is authorised.
//! Everything gunnar had was on one side or the other of the transfer:
//! `gunnar_ssh::PlanGate::pack_quota` refuses a tenant who is *already* at
//! `caps["pack_bytes"]`, before a byte moves, and
//! `gunnar_policy::gates::limits` refuses a pack that has *already been
//! received* — its own header says so plainly: *"this gate rejects before the
//! references are applied … not before the bytes arrive"*. Between them a
//! tenant one byte under a 10 GiB cap could send 400 GiB, and the appliance
//! would read, spool and index every byte of it before anything looked.
//!
//! [`gunnar_wire::PackCeiling`] is the thing that was missing, and this file is
//! its proof. **The distinction that matters is that the refusal happens in
//! flight**: a test that only asserted "the push was rejected" would pass
//! against a server that read all 400 GiB and then said no, which is a
//! different bug and not this one. So every assertion here is about applied
//! output — **how many bytes the client was actually asked for** — and not
//! about the verdict alone.
//!
//! # Every "is cut" has its "is not cut" twin
//!
//! A guard that refused every push would satisfy the first test in this file
//! and nothing else. So:
//!
//! * the same push under a ceiling ABOVE its size lands, and the whole pack is
//!   read (`a_push_under_its_ceiling_lands_and_the_whole_pack_is_read`);
//! * a push of exactly the ceiling lands, because the ceiling is the last byte
//!   that is allowed and not the first that is not
//!   (`a_push_of_exactly_the_ceiling_lands`);
//! * with no ceiling configured — the community default, and every gunnar
//!   before 2026-09-05 — the same oversized push lands untouched
//!   (`with_no_ceiling_the_same_push_lands`).
//!
//! # What was measured before the fix
//!
//! With the two guards in `Counting::read`/`Counting::fill_buf` removed and
//! everything else in place, `a_push_over_its_ceiling_is_cut_mid_transfer`
//! fails on its first assertion, MEASURED 2026-09-05:
//!
//! ```text
//! assertion `left == right` failed: the server took 1049154 of 1049154
//! offered bytes; it must stop one byte past the 65536-byte ceiling (after
//! 140 bytes of command list), not read the push to its end
//!   left: 1049154
//!  right: 65677
//! ```
//!
//! Every other test in this file stayed green through that removal, which is
//! the point: they are the "is not cut" half and they cannot tell a working
//! ceiling from a missing one. Only the byte count can.

mod common;

use gix_hash::Kind;
use gunnar_store::git_storage_trait::GitOps;
use gunnar_wire::receive_pack::{AllowAll, Config};
use gunnar_wire::{PackCeiling, ReceivePack, RefStatus};

use common::{incompressible_pack, push_bytes, validating_ingest, Fixture, Tracked};

/// The ceiling every test here is held to, unless it says otherwise.
const CEILING: u64 = 64 * 1024;

/// What the tenant is told. Composed by the caller in production
/// (`gunnar_server::entitlement`, which names the plan, the cap and the billing
/// URL); a literal here, because the point of the field is that `gunnar-wire`
/// passes it through without knowing what a plan is.
const REFUSAL: &str = "plan 10gb would pass its pack quota with this push (cap 10737418240 \
                       bytes, 10737352704 already held, this push sent more than 65536 bytes): \
                       the transfer was stopped and nothing was stored; delete or buy more at \
                       https://gunnar.rs/billing/team";

struct Pushed {
    outcome: gunnar_wire::PushOutcome,
    /// Bytes the server took off the wire, command list included.
    taken: u64,
    /// Bytes the client offered in total.
    offered: u64,
    /// The bytes the client was sent back.
    report: String,
}

/// Push `pack` at `refs/heads/fresh` under `ceiling`, and report what the
/// server actually consumed.
fn push(fixture: &Fixture, pack: &[u8], ceiling: Option<PackCeiling>) -> Pushed {
    let zero = Kind::Sha1.null().to_string();
    // `refs/heads/fresh` at an object the fixture ALREADY holds: the pack's
    // contents are not the subject here and a connectivity failure would be a
    // second reason for the push to be refused, which would make a refusal
    // prove nothing.
    let input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/fresh")],
        "report-status object-format=sha1",
        pack,
    );
    let offered = input.len() as u64;
    let mut reader = Tracked::new(input);
    let rp = ReceivePack::new(&fixture.git, fixture.kind).with_config(Config {
        pack_ceiling: ceiling,
        ..Config::default()
    });
    let mut out = Vec::new();
    let outcome = rp
        .serve(
            &mut reader,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .expect("serve")
        .expect("a push happened");
    Pushed {
        outcome,
        taken: reader.taken,
        offered,
        report: String::from_utf8_lossy(&out).into_owned(),
    }
}

fn ceiling(bytes: u64) -> Option<PackCeiling> {
    Some(PackCeiling {
        bytes,
        refusal: REFUSAL.to_owned(),
        // **No ledger, and that is this file's subject.** One push against its
        // own number is what these tests are about, and it is what a build with
        // no shared ledger still does. The byte counts asserted below are
        // therefore the unmetered arithmetic — which is exactly why they are
        // worth asserting after the ledger landed: `Counting::admit` returns
        // what it was asked for when there is nothing to ask, so if any of
        // these numbers had moved, the ledger would have changed a path it must
        // not touch. `tests/concurrent_pack_ceiling.rs` is where the shared
        // budget is proved.
        ledger: None,
    })
}

// ── the row: a push that crosses the ceiling is stopped in flight ──────────

#[test]
fn a_push_over_its_ceiling_is_cut_mid_transfer() {
    let f = Fixture::build(Kind::Sha1);
    let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
    assert!(
        pack.len() as u64 > CEILING * 8,
        "the fixture pack must dwarf the ceiling or this test proves nothing: {} bytes",
        pack.len()
    );

    let pushed = push(&f, &pack, ceiling(CEILING));

    // ── 1. the transfer stopped DURING the pack, not after it ─────────────
    //
    // The number, not an inequality: the server takes the command list, then
    // the ceiling, then the one byte that trips it, and then nothing. An
    // inequality here would pass for a server that read a hundred megabytes
    // more than it had to, and "how much of a hostile upload did we take"
    // is the entire question this row was opened about.
    let commands = pushed.offered - pack.len() as u64;
    assert_eq!(
        pushed.taken,
        commands + CEILING + 1,
        "the server took {} of {} offered bytes; it must stop one byte past the {CEILING}-byte \
         ceiling (after {commands} bytes of command list), not read the push to its end",
        pushed.taken,
        pushed.offered
    );
    assert!(
        pushed.taken < pushed.offered / 4,
        "the client was still asked for {} of {} bytes — that is a refusal AFTER the transfer, \
         which is a different bug",
        pushed.taken,
        pushed.offered
    );

    // ── 2. the tenant is told a sentence they can act on ──────────────────
    assert_eq!(
        pushed.outcome.unpack.as_ref().unwrap_err(),
        REFUSAL,
        "the client got the indexer's opinion of a truncated stream instead of the ceiling's \
         own line"
    );
    assert!(
        pushed.report.contains(REFUSAL),
        "the refusal never reached the wire; the client read: {:?}",
        pushed.report
    );
    assert!(
        pushed.report.contains("https://gunnar.rs/billing/team"),
        "a refusal that does not say where to pay: {:?}",
        pushed.report
    );

    // ── 3. nothing was stored and nothing moved ───────────────────────────
    assert!(
        pushed.outcome.pack.is_none(),
        "a cut push reported an indexed pack: {:?}",
        pushed.outcome.pack
    );
    assert!(
        pushed.outcome.new_objects.is_err(),
        "a cut push must not claim to know which objects it introduced"
    );
    assert!(
        matches!(pushed.outcome.statuses[0], RefStatus::Rejected(_)),
        "the reference was accepted on a push that never arrived: {:?}",
        pushed.outcome.statuses
    );
    assert!(
        !pushed.outcome.all_ok(),
        "a cut push reported success: {:?}",
        pushed.outcome
    );
    let refs = f.git.refs().expect("list refs");
    assert!(
        !refs.iter().any(|row| row.name == "refs/heads/fresh"),
        "the cut push moved a reference: {refs:?}"
    );
    // The repository is exactly what it was. The fixture's three references
    // are named rather than counted, because a count of three would also hold
    // for three references that had all been rewritten.
    let mut names: Vec<&str> = refs.iter().map(|row| row.name.as_str()).collect();
    names.sort_unstable();
    assert_eq!(
        names,
        ["refs/heads/main", "refs/heads/side", "refs/tags/v1"],
        "the repository is not what it was before the cut push"
    );

    // ── 4. the count the operator's log and the bill read is the real one ──
    assert_eq!(
        pushed.outcome.pack_bytes,
        Some(CEILING + 1),
        "`pack_bytes` must be what came off the wire, which is what the ceiling stopped at"
    );
}

// ── the twins: what is NOT cut ─────────────────────────────────────────────

#[test]
fn a_push_under_its_ceiling_lands_and_the_whole_pack_is_read() {
    let f = Fixture::build(Kind::Sha1);
    let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
    let generous = pack.len() as u64 * 2;

    let pushed = push(&f, &pack, ceiling(generous));

    assert_eq!(
        pushed.outcome.unpack,
        Ok(()),
        "a push well under its ceiling was refused"
    );
    assert_eq!(pushed.outcome.statuses[0], RefStatus::Ok);
    assert_eq!(
        pushed.taken, pushed.offered,
        "the whole push must be read when nothing stops it"
    );
    assert_eq!(pushed.outcome.pack_bytes, Some(pack.len() as u64));
    assert!(f
        .git
        .refs()
        .expect("list refs")
        .iter()
        .any(|row| row.name == "refs/heads/fresh"));
}

#[test]
fn a_push_of_exactly_the_ceiling_lands() {
    // The boundary is not decoration: a hard cap with zero slack refuses the
    // push that would land exactly at the limit, and that is the refusal a
    // tenant cannot understand. `Counting::over` is `>` and this is what says
    // so — flip it to `>=` and this test goes red while every other test in
    // the file stays green.
    let f = Fixture::build(Kind::Sha1);
    let pack = incompressible_pack(Kind::Sha1, 4, 64 * 1024);
    let exact = pack.len() as u64;

    let pushed = push(&f, &pack, ceiling(exact));

    assert_eq!(
        pushed.outcome.unpack,
        Ok(()),
        "a push of exactly {exact} bytes under a {exact}-byte ceiling was refused"
    );
    assert_eq!(pushed.taken, pushed.offered);
}

#[test]
fn with_no_ceiling_the_same_push_lands() {
    // The community build, and every gunnar before this ceiling existed. A
    // `None` here must mean what it always meant — read the pack to its end —
    // and not "cut at zero".
    let f = Fixture::build(Kind::Sha1);
    let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);

    let pushed = push(&f, &pack, None);

    assert_eq!(pushed.outcome.unpack, Ok(()));
    assert_eq!(pushed.outcome.statuses[0], RefStatus::Ok);
    assert_eq!(pushed.taken, pushed.offered);
}

#[test]
fn a_deletion_carries_no_pack_and_is_never_cut() {
    // A ceiling of one byte, and a `git push --delete`, which sends no pack at
    // all. The rule — a deletions-only push is a MEASURED zero, not an
    // absent measurement — must survive the ceiling: a guard that refused this
    // would make a full tenant unable to free space, which is the one thing
    // they must always be able to do.
    let f = Fixture::build(Kind::Sha1);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[(&f.three.to_string(), &zero, "refs/heads/side")],
        "report-status object-format=sha1",
        &[],
    );
    let mut reader = Tracked::new(input);
    let rp = ReceivePack::new(&f.git, f.kind).with_config(Config {
        pack_ceiling: ceiling(1),
        ..Config::default()
    });
    let mut out = Vec::new();
    let outcome = rp
        .serve(
            &mut reader,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .expect("serve")
        .expect("a push happened");

    assert_eq!(
        outcome.unpack,
        Ok(()),
        "a deletion was cut by a byte ceiling it never sent a byte against"
    );
    assert_eq!(outcome.statuses[0], RefStatus::Ok);
    assert_eq!(outcome.pack_bytes, Some(0));
    assert!(!f
        .git
        .refs()
        .expect("list refs")
        .iter()
        .any(|row| row.name == "refs/heads/side"));
}