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/refusal_delivery.rs

//! **A refusal that never reaches the client is not a refusal.**
//!
//! `tests/quota.rs` proves the decision — the store never sees a byte of an
//! over-quota object. What it cannot prove, at the size it sends, is that the
//! client is ever *told*: it writes exactly one `CHUNK_BYTES` payload, and
//! `Limits::max_drain_bytes` is two chunks, so its refusal is the only one in
//! the tree that fits inside the drain budget. Its own comment says "two
//! chunks, which is what `max_drain_bytes` budgets for" beside a script that
//! writes one.
//!
//! Everything real is on the other side of that line. A quota exists to refuse
//! objects far larger than 128 KiB, and a client refusing to stop at 128 KiB is
//! the ordinary case, not a hostile one: git-lfs writes `put-object`, its
//! arguments, the delim and then the **whole object** before it reads a byte of
//! the response. So the case this file sends — a payload past the drain budget
//! — is the only one that ever happens in production, and it was the one that
//! answered nothing at all.
//!
//! Every case asserts the **status code on the wire**, not a returned `Result`:
//! the returned error was correct all along, and it went nowhere.

use std::io::BufRead;

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

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()
}

/// Every `status <n>` line the server put on the wire, in order.
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 a conversation that is **expected to end the session**, and hand back
/// what was written before it did.
///
/// `quota.rs`'s helper `.expect()`s the session, which is right for the
/// conversations it drives and wrong for these: the session ending is part of
/// the contract here, and what is under test is what went out first.
fn serve_until_close(script: Vec<u8>, operation: Operation) -> (Vec<u8>, DiscardingLfsStore) {
    let store = DiscardingLfsStore::new();
    let mut input = std::io::BufReader::new(std::io::Cursor::new(script));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    let _ = Session::new(&store, operation).serve(reader, &mut output);
    (output, store)
}

/// A `put-object` whose payload runs past the drain budget.
fn oversized_put(size: u64, chunks: usize) -> 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, format!("put-object {}", oid(2)).as_bytes()).unwrap();
    gunnar_pktline::write_text(&mut s, format!("size={size}").as_bytes()).unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    for _ in 0..chunks {
        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();
    s
}

/// **The defect.** The client is refused for size and keeps writing, as every
/// real client does. The 413 must still go out.
#[test]
fn an_over_quota_put_is_told_413_even_when_the_client_will_not_stop() {
    let drain_budget = Limits::default().max_drain_bytes;
    let chunks = (drain_budget as usize / CHUNK_BYTES) + 2;
    assert!(
        (chunks * CHUNK_BYTES) as u64 > drain_budget,
        "this case must exceed the drain budget or it is the case quota.rs \
         already sends"
    );

    let (wire, store) = serve_until_close(oversized_put(EIGHT_GIB + 1, chunks), Operation::Upload);

    assert_eq!(
        statuses(&wire),
        vec![200, 413],
        "the refusal never reached the client; it got a closed channel.\nwire: {}",
        String::from_utf8_lossy(&wire).escape_debug()
    );
    assert!(
        String::from_utf8_lossy(&wire).contains("accepts at most"),
        "the 413 carries no reason:\n{}",
        String::from_utf8_lossy(&wire).escape_debug()
    );
    // The decision itself is unchanged: still nothing reaches the store.
    assert_eq!(store.accepted(), 0);
    assert_eq!(store.committed(), 0);
}

/// The same for a refusal that is **not** about size, so the fix is not a
/// special case bolted onto the quota. A `download` grant cannot `put-object`,
/// and that 405 has the identical delivery problem.
#[test]
fn a_wrong_operation_put_is_told_405_even_when_the_client_will_not_stop() {
    let drain_budget = Limits::default().max_drain_bytes;
    let chunks = (drain_budget as usize / CHUNK_BYTES) + 2;

    // A size well under the quota: the only reason to refuse is the grant.
    let (wire, store) = serve_until_close(oversized_put(16, chunks), Operation::Download);

    assert_eq!(
        statuses(&wire),
        vec![200, 405],
        "an authorisation refusal was delivered as a dropped channel.\nwire: {}",
        String::from_utf8_lossy(&wire).escape_debug()
    );
    assert_eq!(store.accepted(), 0);
}

/// **The identity-value half, in two directions.**
///
/// A client that respects the drain budget must still get its 413 *and* keep
/// the session — `quit` is answered, which is the third `200`. Without this,
/// every assertion above would hold against a server that closed on every
/// refusal, which is not the behaviour being asked for.
#[test]
fn a_refusal_the_server_can_resynchronise_after_does_not_end_the_session() {
    let store = DiscardingLfsStore::new();
    // One chunk, inside the two-chunk budget.
    let script = oversized_put(EIGHT_GIB + 1, 1);
    let mut input = 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::Upload)
        .serve(reader, &mut output)
        .expect("a drain that stayed inside its budget must not end the session");

    assert_eq!(
        statuses(&output),
        vec![200, 413, 200],
        "wire: {}",
        String::from_utf8_lossy(&output).escape_debug()
    );
}

/// And the other identity: an upload that is not refused at all still goes
/// through, so none of the above is passing against a server that refuses
/// everything.
#[test]
fn an_accepted_upload_is_unaffected() {
    const PAYLOAD: &[u8] = b"delivered\n";
    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 store = DiscardingLfsStore::new();
    let mut input = std::io::BufReader::new(std::io::Cursor::new(s));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    Session::new(&store, Operation::Upload)
        .serve(reader, &mut output)
        .expect("an accepted upload must not fail the session");

    assert_eq!(statuses(&output), vec![200, 200, 200]);
    assert_eq!(store.committed(), 1);
}