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

//! A command that ended with the wrong packet is a **400**, not an EOF.
//!
//! `Error::Framing` means *the pkt-line boundary is lost and there is no way to
//! find the next command*, and it is fatal. Four sites raised it where that was
//! not true: `read_args` had already consumed the section up to and including
//! its terminator and handed the caller which one arrived, so the reader knew
//! exactly where it stood. The client got a closed channel where the protocol
//! defines a status code — the same shape as the quota refusal that `refuse!`'s
//! drain error replaced.
//!
//! Every test here asserts **two** things, because either alone is blind:
//!
//! * the 400 is on the wire, and
//! * the session survives to answer the `quit` that follows it.
//!
//! A session that answered 400 and then died would pass the first. A session
//! that silently ignored the malformed terminator would pass the second.

use std::io::BufRead;

use gunnar_lfs::{DiscardingLfsStore, MemLfsStore, Oid, Operation, Session};

fn oid() -> Oid {
    Oid::parse(&"1".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()
}

/// `version 1` terminated properly, so the malformed command under test is not
/// also the un-negotiated one.
fn negotiated(script: &mut Vec<u8>) {
    gunnar_pktline::write_text(script, b"version 1").unwrap();
    gunnar_pktline::write_flush(script).unwrap();
}

fn quit(script: &mut Vec<u8>) {
    gunnar_pktline::write_text(script, b"quit").unwrap();
    gunnar_pktline::write_flush(script).unwrap();
}

/// Drive one conversation to completion against a store that accepts nothing.
fn serve(script: Vec<u8>, operation: Operation) -> (Vec<u16>, gunnar_lfs::Summary) {
    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 summary = Session::new(&store, operation)
        .serve(reader, &mut output)
        .expect("the stream is in sync throughout; the session must not end");
    (statuses(&output), summary)
}

/// `version` ended with a delim-pkt. A delim promises a further section, so the
/// section is read out and the stream stays in sync.
#[test]
fn version_ending_in_a_delim_is_answered_and_the_session_continues() {
    let mut s = Vec::new();
    gunnar_pktline::write_text(&mut s, b"version 1").unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"a stray line").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    negotiated(&mut s);
    quit(&mut s);

    let (statuses, summary) = serve(s, Operation::Download);
    assert_eq!(
        statuses,
        vec![400, 200, 200],
        "the malformed version must be answered 400, then the well-formed one 200, then quit"
    );
    assert!(
        summary.clean_quit,
        "the session died instead of surviving to read `quit`"
    );
}

/// `batch` ended with a flush-pkt where a delim belongs. Nothing follows a
/// flush, so there is nothing to drain and the next packet is the next command.
#[test]
fn batch_ending_in_a_flush_is_answered_and_the_session_continues() {
    let mut s = Vec::new();
    negotiated(&mut s);
    gunnar_pktline::write_text(&mut s, b"batch").unwrap();
    gunnar_pktline::write_text(&mut s, b"hash-algo=sha256").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    quit(&mut s);

    let (statuses, summary) = serve(s, Operation::Upload);
    assert_eq!(statuses, vec![200, 400, 200]);
    assert!(summary.clean_quit);
    assert_eq!(summary.batches, 0, "no batch was actually served");
}

/// `get-object`, on a session that could otherwise have served it.
#[test]
fn get_object_ending_in_a_delim_is_answered_and_the_object_still_arrives_next() {
    let store = MemLfsStore::new();
    let body = b"the object body".to_vec();
    // The store computes the oid from the bytes, so this is the real one and
    // not a placeholder that would make `get-object` a 404 either way.
    let present = store.insert(&body);

    let mut s = Vec::new();
    negotiated(&mut s);
    // Malformed: delim where a flush belongs, plus the section it promised.
    gunnar_pktline::write_text(&mut s, format!("get-object {present}").as_bytes()).unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"stray").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    // Well-formed, immediately after. This is the assertion that matters: the
    // reader is still on a command boundary.
    gunnar_pktline::write_text(&mut s, format!("get-object {present}").as_bytes()).unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    quit(&mut s);

    let mut input = std::io::BufReader::new(std::io::Cursor::new(s));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    let summary = Session::new(&store, Operation::Download)
        .serve(reader, &mut output)
        .expect("the stream stays in sync");

    assert_eq!(
        statuses(&output),
        vec![200, 400, 200, 200],
        "version, the malformed get-object, the good get-object, quit"
    );
    assert_eq!(
        summary.objects_sent, 1,
        "the object after the malformed command did not come out"
    );
    assert_eq!(summary.bytes_sent, body.len() as u64);
    assert!(summary.clean_quit);
    assert!(
        output.windows(body.len()).any(|w| w == body),
        "the object body is not on the wire at all"
    );
}

/// `verify-object`, the fourth site.
#[test]
fn verify_object_ending_in_a_delim_is_answered_and_the_session_continues() {
    let mut s = Vec::new();
    negotiated(&mut s);
    gunnar_pktline::write_text(&mut s, format!("verify-object {}", oid()).as_bytes()).unwrap();
    gunnar_pktline::write_delim(&mut s).unwrap();
    gunnar_pktline::write_text(&mut s, b"size=1").unwrap();
    gunnar_pktline::write_flush(&mut s).unwrap();
    quit(&mut s);

    let (statuses, summary) = serve(s, Operation::Upload);
    assert_eq!(statuses, vec![200, 400, 200]);
    assert!(summary.clean_quit);
}

/// **The control.** A genuinely lost boundary must still be fatal, or all that
/// has been shown is that `Framing` was replaced everywhere rather than at the
/// four places where the stream was in sync.
#[test]
fn a_response_end_packet_where_a_command_belongs_is_still_fatal() {
    let store = DiscardingLfsStore::new();
    let mut s = Vec::new();
    negotiated(&mut s);
    gunnar_pktline::write_response_end(&mut s).unwrap();
    quit(&mut s);

    let mut input = std::io::BufReader::new(std::io::Cursor::new(s));
    let mut output = Vec::new();
    let reader: &mut dyn BufRead = &mut input;
    let err = Session::new(&store, Operation::Download)
        .serve(reader, &mut output)
        .expect_err("a response-end-pkt is not something this protocol can resynchronise from");
    assert!(
        matches!(err, gunnar_lfs::Error::Framing(_)),
        "the control failed for the wrong reason; got {err:?}"
    );
}