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/tests/quota.rs

//! The object quota, **at its real default value**.
//!
//! `limits.rs` had one test, and it asserted the defaults were non-zero. That
//! cannot fail for any plausible value and it never touches the arithmetic. The
//! quota is 8 GiB, which is past `u32`, past `i32`, and the place where a
//! `as u32` or an `as i32` anywhere on the declaration path stops being
//! invisible — so it is the value the boundary has to be tested at, not a
//! convenient small one.
//!
//! Nothing here moves 8 GiB. It does not need to: **every refusal in this file
//! happens before a byte of payload is read**, which is the property being
//! asserted. A quota that only refused after taking the bytes would be a quota
//! that protects nothing, and it would pass a test that merely checked the
//! final answer.

use std::io::BufRead;

use gunnar_lfs::{DiscardingLfsStore, Limits, Oid, Operation, Session, CHUNK_BYTES};

/// The shipped default, spelled out. Compared against `Limits::default()` below
/// rather than read from it, so a change to the constant fails here loudly
/// instead of silently retargeting every assertion in this file.
const EIGHT_GIB: u64 = 8 * 1024 * 1024 * 1024;

fn oid(seed: u8) -> Oid {
    let c = char::from_digit(seed as u32 % 16, 16).unwrap();
    Oid::parse(&String::from(c).repeat(64)).unwrap()
}

fn statuses(wire: &[u8]) -> Vec<u16> {
    String::from_utf8_lossy(wire)
        .split("status ")
        .skip(1)
        .filter_map(|rest| rest.split('\n').next()?.trim().parse().ok())
        .collect()
}

/// Drive one conversation and hand back the wire plus the store.
fn serve(script: Vec<u8>, operation: Operation) -> (Vec<u8>, DiscardingLfsStore) {
    let store = DiscardingLfsStore::new();
    let mut input: std::io::BufReader<std::io::Cursor<Vec<u8>>> =
        std::io::BufReader::new(std::io::Cursor::new(script));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    Session::new(&store, operation)
        .serve(reader, &mut output)
        .expect("a well-framed conversation must not fail the session");
    (output, store)
}

fn batch_script(size: u64) -> Vec<u8> {
    let mut s = Vec::new();
    gunnar_pktline::write_text(&mut s, b"version 1").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"batch").unwrap();
    gunnar_pktline::write_text(&mut s, b"hash-algo=sha256").unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, format!("{} {size}", oid(1)).as_bytes()).unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"quit").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    s
}

#[test]
fn the_default_quota_is_eight_gibibytes_and_the_boundary_is_exact() {
    assert_eq!(
        Limits::default().max_object_bytes,
        EIGHT_GIB,
        "the shipped quota changed; every assertion in this file targets 8 GiB"
    );

    // Exactly at the quota: accepted. `>` and not `>=` — an object of exactly
    // the permitted size is permitted, and an off-by-one here silently costs a
    // customer their largest legal upload.
    let (wire, _) = serve(batch_script(EIGHT_GIB), Operation::Upload);
    assert_eq!(
        statuses(&wire),
        vec![200, 200, 200],
        "an object of exactly the quota must be accepted.\nwire: {}",
        String::from_utf8_lossy(&wire)
    );

    // One byte over: 413, and the whole batch is refused.
    let (wire, _) = serve(batch_script(EIGHT_GIB + 1), Operation::Upload);
    assert_eq!(
        statuses(&wire),
        vec![200, 413, 200],
        "one byte over the quota must be 413.\nwire: {}",
        String::from_utf8_lossy(&wire)
    );
    assert!(
        String::from_utf8_lossy(&wire).contains("8589934592"),
        "the refusal must state the limit in bytes so an operator can act on it"
    );
}

/// The same boundary on a `download` session must **not** fire. The quota is a
/// limit on what this server will accept, not on what it will serve — an object
/// already stored is served whatever its size, and applying the check in both
/// directions would make a lowered quota retroactively hide existing objects.
#[test]
fn the_quota_does_not_apply_to_a_download_batch() {
    let (wire, _) = serve(batch_script(EIGHT_GIB * 4), Operation::Download);
    assert_eq!(
        statuses(&wire),
        vec![200, 200, 200],
        "a download batch must not be refused for size.\nwire: {}",
        String::from_utf8_lossy(&wire)
    );
}

/// **`put-object` is the load-bearing check**, because a client may skip `batch`
/// entirely. Refused before the payload: the store is never asked to begin a
/// write, and not one byte reaches it.
#[test]
fn put_object_over_quota_is_refused_before_the_store_is_touched() {
    let mut s = Vec::new();
    gunnar_pktline::write_text(&mut s, b"version 1").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, format!("put-object {}", oid(2)).as_bytes()).unwrap();
    gunnar_pktline::write_text(&mut s, format!("size={}", EIGHT_GIB + 1).as_bytes()).unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    // The client is already writing when the refusal is decided. Two chunks,
    // which is what `max_drain_bytes` budgets for.
    gunnar_pktline::write_data(&mut s, &vec![0u8; CHUNK_BYTES]).unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"quit").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();

    let (wire, store) = serve(s, Operation::Upload);
    assert_eq!(
        statuses(&wire),
        vec![200, 413, 200],
        "wire: {}",
        String::from_utf8_lossy(&wire)
    );
    assert_eq!(
        store.accepted(),
        0,
        "the refusal must come before the store sees a byte; it saw {}",
        store.accepted()
    );
    assert_eq!(store.committed(), 0);
    // And the session resynchronised: `quit` after the drained payload was still
    // read as `quit`, which is what the third 200 above is.
}

/// The identity-value half for the whole file. Every assertion above is about a
/// refusal, and all of them would pass against a server that refused every
/// upload. One byte under the quota, actually transferred, actually committed.
#[test]
fn an_object_under_the_quota_still_goes_through() {
    const PAYLOAD: &[u8] = b"under the quota\n";
    // sha256 of PAYLOAD, from coreutils, not from gunnar.
    let mut hasher = gix_hash::hasher(gix_hash::Kind::Sha256);
    hasher.update(PAYLOAD);
    let real = match hasher.try_finalize().unwrap() {
        gix_hash::ObjectId::Sha256(d) => Oid::from_digest(d),
        other => unreachable!("{other:?}"),
    };

    let mut s = Vec::new();
    gunnar_pktline::write_text(&mut s, b"version 1").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, format!("put-object {real}").as_bytes()).unwrap();
    gunnar_pktline::write_text(&mut s, format!("size={}", PAYLOAD.len()).as_bytes()).unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    gunnar_pktline::write_data(&mut s, PAYLOAD).unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"quit").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();

    let (wire, store) = serve(s, Operation::Upload);
    assert_eq!(
        statuses(&wire),
        vec![200, 200, 200],
        "wire: {}",
        String::from_utf8_lossy(&wire)
    );
    assert_eq!(store.accepted(), PAYLOAD.len() as u64);
    assert_eq!(store.committed(), 1, "the object must have been committed");
}

/// A declared size at the top of `u64` must be refused as too large, not wrap.
///
/// `VerifyingSink::accept` uses `saturating_add` precisely so a hostile
/// declaration cannot make the running total wrap past the claim; this asserts
/// the declaration itself never gets that far.
#[test]
fn an_absurd_declared_size_is_refused_and_does_not_wrap() {
    for size in [u64::MAX, u64::MAX - 1, i64::MAX as u64, EIGHT_GIB * 1024] {
        let (wire, store) = serve(batch_script(size), Operation::Upload);
        assert_eq!(
            statuses(&wire),
            vec![200, 413, 200],
            "size {size} must be refused 413.\nwire: {}",
            String::from_utf8_lossy(&wire)
        );
        assert_eq!(store.accepted(), 0);
    }
}

/// A configured quota is honoured, not just the default — otherwise every test
/// above proves only that one hardcoded number works.
#[test]
fn a_configured_quota_replaces_the_default_in_both_checks() {
    let store = DiscardingLfsStore::new();
    let limits = Limits::new().max_object_bytes(1024);

    let mut input = std::io::BufReader::new(std::io::Cursor::new(batch_script(1025)));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    Session::new(&store, Operation::Upload)
        .with_limits(limits)
        .serve(reader, &mut output)
        .unwrap();
    assert_eq!(statuses(&output), vec![200, 413, 200]);
    assert!(String::from_utf8_lossy(&output).contains("1024"));

    // And exactly at the configured limit, accepted — the boundary moved with
    // the setting rather than staying at 8 GiB.
    let mut input = std::io::BufReader::new(std::io::Cursor::new(batch_script(1024)));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    Session::new(&store, Operation::Upload)
        .with_limits(limits)
        .serve(reader, &mut output)
        .unwrap();
    assert_eq!(statuses(&output), vec![200, 200, 200]);
}