gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **`Refs.ReadLfsObject`: the bytes of a file that is a pointer in the tree.**
//!
//! A file kept in LFS is 131 bytes of pointer text in the tree, and every
//! reader that goes through `Refs.ReadObject` gets those 131 bytes. That is
//! what a console drawing gunnar's own README hands a browser when the README
//! links `docs/gunnar-fast-setup.pdf`: a "PDF" the browser calls broken.
//!
//! What is asserted here is the contract that makes the difference safe to
//! rely on:
//!
//! * the object comes back **whole and byte-identical**, out of the same
//!   `FsLfsStore` the SSH adapter and the HTTP batch API serve;
//! * an object over the request's ceiling is **refused, not truncated** — a
//!   prefix of an LFS object is not a smaller file, and a caller handed one
//!   could not tell;
//! * an oid that is not in the store is `NotFound`, and one that is not an oid
//!   at all is `InvalidArgument`, so a caller can tell "gone" from "you asked
//!   wrong".
//!
//! * **a ranged read** (`offset`/`length`, 2026-10-15) reads an object of any
//!   size a range at a time under the same ceiling, byte-exact; an offset past
//!   the end and a range over the ceiling are refused by name; and a request
//!   with neither field — every client before them — is the whole-or-refused
//!   read, unchanged.
//!
//! Authorisation is not here: it is in the service layer, which requires
//! `GrantLevel::Read` on the repository like every other verb on `Refs`, and
//! `gunnar-grpc/tests/auth.rs` sweeps that for every RPC in the census.

use std::io::Write as _;
use std::sync::Arc;

use gunnar_core::{NewRepo, RepoRef};
use gunnar_grpc::ControlPlane as _;
use gunnar_lfs::LfsStore as _;
use gunnar_proto::RepoPath;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;

/// An empty repository, and its git dir — which is where its LFS store lives.
fn repo(stores: &ServerStores, path: &RepoRef) -> std::path::PathBuf {
    let repo = stores
        .create_repo(NewRepo::new(path.clone()), false)
        .expect("create");
    let handle = stores.repo_store(&repo).expect("open");
    let git_dir = handle.git_dir().to_path_buf();
    drop(handle);
    git_dir
}

fn put(git_dir: &std::path::Path, body: &[u8]) -> gunnar_lfs::Oid {
    let store = gunnar_lfs::FsLfsStore::open_or_init(git_dir).expect("lfs store");
    let oid = gunnar_lfs::Oid::of(body);
    let mut write = store.begin_write(&oid, body.len() as u64).expect("begin");
    write.write_all(body).expect("write");
    write.commit().expect("commit");
    oid
}

fn wire(path: &RepoRef) -> RepoPath {
    RepoPath {
        namespace: path.namespace().to_owned(),
        repo: path.name().to_owned(),
    }
}

/// **The object comes back whole**, and it is the bytes that were stored — not
/// the pointer, and not a prefix.
#[test]
fn an_lfs_object_is_served_whole_and_byte_identical() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    stores
        .catalog()
        .create_namespace("team")
        .expect("namespace");
    let path = RepoRef::new("team", "one").expect("legal");
    let git_dir = repo(&stores, &path);
    // Not a uniform fill: a run of one byte would survive a truncation that
    // lost the tail, and this test would not notice.
    let body: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
    let oid = put(&git_dir, &body);
    let plane = CoreControlPlane::new(Arc::clone(&stores));

    let answer = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
        .expect("the object is served");

    assert_eq!(answer.oid, oid.to_string());
    assert_eq!(answer.size, body.len() as u64);
    assert_eq!(answer.data, body, "the bytes are the stored bytes");
    // And the sha256 the pointer names is the sha256 of what came back, which
    // is the property a client verifies a download by.
    assert_eq!(gunnar_lfs::Oid::of(&answer.data), oid);
}

/// **Over the ceiling is a refusal, never a prefix.** The size decides before
/// a byte is read, and the message carries the size so the caller knows to go
/// to `git lfs` instead of retrying smaller.
#[test]
fn an_object_over_the_ceiling_is_refused_rather_than_truncated() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    stores
        .catalog()
        .create_namespace("team")
        .expect("namespace");
    let path = RepoRef::new("team", "one").expect("legal");
    let git_dir = repo(&stores, &path);
    let body = vec![0x4cu8; 300_000];
    let oid = put(&git_dir, &body);
    let plane = CoreControlPlane::new(Arc::clone(&stores));

    let refused = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 100_000, None)
        .expect_err("100 KB is not enough for a 300 KB object");

    let message = format!("{refused:?}");
    assert!(message.contains("300000"), "the size is named: {message}");
    assert!(
        message.to_ascii_lowercase().contains("git lfs"),
        "the refusal says where the bytes are: {message}"
    );
    // The same object, asked for with room, still arrives whole: the refusal
    // was about the ceiling and not about the object.
    let answer = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 400_000, None)
        .expect("served under a ceiling that fits");
    assert_eq!(answer.data.len(), body.len());
}

/// "Not stored" and "not an oid" are different answers, because they need
/// different things done about them.
#[test]
fn a_missing_object_is_not_found_and_a_malformed_oid_is_invalid() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    stores
        .catalog()
        .create_namespace("team")
        .expect("namespace");
    let path = RepoRef::new("team", "one").expect("legal");
    repo(&stores, &path);
    let plane = CoreControlPlane::new(Arc::clone(&stores));

    let missing = plane
        .read_lfs_object(&wire(&path), &"f0".repeat(32), 0, None)
        .expect_err("nothing was stored");
    assert!(
        matches!(missing, gunnar_grpc::seam::ControlError::NotFound(_)),
        "{missing:?}"
    );

    let malformed = plane
        .read_lfs_object(&wire(&path), "not-an-oid", 0, None)
        .expect_err("that is not an LFS oid");
    assert!(
        matches!(
            malformed,
            gunnar_grpc::seam::ControlError::InvalidArgument(_)
        ),
        "{malformed:?}"
    );
}

/// A store with one object, and the plane over it.
fn one_object(
    body: &[u8],
) -> (
    tempfile::TempDir,
    CoreControlPlane,
    RepoRef,
    gunnar_lfs::Oid,
) {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    stores
        .catalog()
        .create_namespace("team")
        .expect("namespace");
    let path = RepoRef::new("team", "one").expect("legal");
    let git_dir = repo(&stores, &path);
    let oid = put(&git_dir, body);
    (dir, CoreControlPlane::new(Arc::clone(&stores)), path, oid)
}

/// ★ **An object larger than the per-answer ceiling, read whole a range at a
/// time** — the 6 MB README picture a console could only refuse (2026-10-15).
/// Every range is at most the ceiling, says where it starts and how large the
/// whole object is, and the ranges put together are the object, byte-exact.
#[test]
fn an_object_over_the_ceiling_is_read_whole_range_by_range() {
    let body: Vec<u8> = (0..5_968_091u32).map(|i| (i % 251) as u8).collect();
    let (_dir, plane, path, oid) = one_object(&body);

    let mut got = Vec::new();
    let mut ranges = 0;
    while (got.len() as u64) < body.len() as u64 {
        let offset = got.len() as u64;
        let answer = plane
            .read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((offset, 0)))
            .expect("a range is served");
        assert_eq!(answer.offset, offset, "the range says where it starts");
        assert_eq!(answer.size, body.len() as u64, "and how large the whole is");
        assert!(
            answer.data.len() as u64 <= 4 * 1024 * 1024,
            "at most the ceiling"
        );
        assert!(!answer.data.is_empty());
        got.extend_from_slice(&answer.data);
        ranges += 1;
    }
    assert_eq!(ranges, 2, "4 MiB, then the rest");
    assert_eq!(got, body, "the ranges are the object");
    assert_eq!(gunnar_lfs::Oid::of(&got), oid);

    // The same object asked for the old way — no range — is still refused,
    // never a prefix.
    let refused = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
        .expect_err("whole, it is over the ceiling");
    assert!(format!("{refused:?}").contains("5968091"), "{refused:?}");
}

/// A range in the middle, with an explicit length and a caller's own smaller
/// ceiling; a range that runs past the end ends AT the end.
#[test]
fn a_range_is_exactly_the_bytes_asked_for() {
    let body: Vec<u8> = (0..300_000u32).map(|i| (i % 241) as u8).collect();
    let (_dir, plane, path, oid) = one_object(&body);

    let mid = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 100_000, Some((1_000, 64)))
        .expect("an in-range read");
    assert_eq!(mid.data, &body[1_000..1_064]);
    assert_eq!((mid.offset, mid.size), (1_000, 300_000));

    let head = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((0, 24)))
        .expect("the header alone");
    assert_eq!(head.data, &body[..24]);

    let tail = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 100_000, Some((250_000, 0)))
        .expect("to the ceiling, which the end comes before");
    assert_eq!(tail.data, &body[250_000..]);
    assert_eq!(
        tail.offset + tail.data.len() as u64,
        tail.size,
        "the last range"
    );
}

/// **Refused by name**: an offset at or past the end, and a range larger than
/// the ceiling — never an empty or shortened answer a caller could mistake
/// for the object.
#[test]
fn an_offset_past_the_end_and_a_range_over_the_ceiling_are_refused() {
    let body = vec![7u8; 1_000];
    let (_dir, plane, path, oid) = one_object(&body);

    for offset in [1_000, 5_000] {
        let past = plane
            .read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((offset, 10)))
            .expect_err("past the end");
        assert!(
            matches!(past, gunnar_grpc::seam::ControlError::InvalidArgument(_)),
            "{past:?}"
        );
        let said = format!("{past:?}");
        assert!(
            said.contains(&offset.to_string()) && said.contains("1000"),
            "{said}"
        );
    }

    let over = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 500, Some((0, 501)))
        .expect_err("a range over the caller's ceiling");
    assert!(
        matches!(over, gunnar_grpc::seam::ControlError::ResourceExhausted(_)),
        "{over:?}"
    );
    let over = plane
        .read_lfs_object(
            &wire(&path),
            &oid.to_string(),
            0,
            Some((0, 4 * 1024 * 1024 + 1)),
        )
        .expect_err("a range over the server's ceiling");
    assert!(
        matches!(over, gunnar_grpc::seam::ControlError::ResourceExhausted(_)),
        "{over:?}"
    );
}

/// **An older client sends neither field**, and gets exactly what it always
/// got: the whole object, `offset` 0, `size` = its length.
#[test]
fn a_request_without_a_range_is_the_whole_read_it_always_was() {
    let body: Vec<u8> = (0..70_000u32).map(|i| (i % 13) as u8).collect();
    let (_dir, plane, path, oid) = one_object(&body);

    let whole = plane
        .read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
        .expect("whole");
    assert_eq!(whole.data, body);
    assert_eq!((whole.offset, whole.size), (0, body.len() as u64));
}