Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-wire/tests/receive_pack_wire_bytes.rs

//! The exact bytes `receive-pack` puts on the wire, pinned.
//!
//! # Why a byte pin and not another functional test
//!
//! The advertisement and the status report moved out of this crate and into
//! `gunnar-sendpack`, so that the encoder and the decoder of each are one
//! implementation instead of two that agree by inspection. Nothing
//! about that move is *supposed* to change what goes on the wire — and "supposed
//! to" is exactly the claim a functional suite cannot check.
//!
//! Every `real_git_interop` test would still pass if the advertisement grew a
//! trailing space, if the capability list reordered, or if a `ng` reason lost
//! its flattening: stock `git` tolerates all three, and the drift would surface
//! only against some other client, months later. So these are literals,
//! transcribed from what this crate emitted **before** the move (worktree
//! `008d74b`, 2026-08-05) and cross-read against `git receive-pack
//! --advertise-refs` on git 2.53.0.

mod common;

use gix_hash::Kind;
use gunnar_store::mem::MemGitServe;
use gunnar_wire::receive_pack::{AllowAll, PushFacts, PushGate, RefCommand};
use gunnar_wire::{ReceivePack, RefStatus};

use common::{empty_pack, push_bytes, validating_ingest, Fixture};

fn stable(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).into_owned()
}

/// The capability list, transcribed. **Order is part of the wire**: a client
/// that diffs two servers' advertisements reads a reorder as a change.
///
/// `agent=` is the one item taken from the crate rather than written out: it
/// carries the crate version, which changes on a release, and pinning it would
/// make these tests fail for the one reason that is not a wire change. It is
/// still inside the pinned **length prefix**, so an agent of the wrong length
/// still fails.
fn caps() -> String {
    format!(
        "report-status report-status-v2 delete-refs side-band-64k quiet atomic \
         ofs-delta push-options object-format=sha1 agent={}",
        gunnar_wire::caps::default_agent()
    )
}

fn advertisement(fixture: &Fixture) -> String {
    let rp = ReceivePack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    rp.write_ref_advertisement(&mut out).expect("advertise");
    stable(&out)
}

/// Everything after the reference advertisement.
fn report_of(out: &[u8]) -> String {
    let text = stable(out);
    let at = text.find("unpack").expect("an `unpack` line");
    text[at - 4..].to_owned()
}

#[test]
fn the_advertisement_of_a_populated_repository_is_byte_exact() {
    let f = Fixture::build(Kind::Sha1);
    let got = advertisement(&f);

    // First line: `<len><oid> <ref>\0<caps>\n`. Every later line carries no
    // capability list at all, which is the single most-repeated bug in this
    // format — and with only one reference the two rules would be
    // indistinguishable, which is why the fixture has three.
    let first = format!("{} refs/heads/main\0{}\n", f.four, caps());
    let mut expected = format!("{:04x}{first}", first.len() + 4);
    for (oid, name) in [(f.three, "refs/heads/side"), (f.tag, "refs/tags/v1")] {
        let line = format!("{oid} {name}\n");
        expected.push_str(&format!("{:04x}{line}", line.len() + 4));
    }
    expected.push_str("0000");
    assert_eq!(got, expected);
    assert_eq!(got.matches('\0').count(), 1, "exactly one NUL, on line one");
}

/// A tag is advertised by `receive-pack` **unpeeled**. `upload-pack` peels it
/// and adds a `^{}` line; `receive-pack` does not, because a pusher compares
/// against the ref's own value and a peeled line would be a second, phantom
/// reference to compare against.
#[test]
fn receive_pack_does_not_peel_tags_the_way_upload_pack_does() {
    let f = Fixture::build(Kind::Sha1);
    let got = advertisement(&f);
    assert!(got.contains(&format!("{} refs/tags/v1\n", f.tag)));
    assert!(
        !got.contains("^{}"),
        "receive-pack advertises no peeled lines: {got:?}"
    );
}

/// An empty repository advertises exactly one line, and it is a pseudo-ref. A
/// client that received nothing could not learn the hash algorithm; one that
/// recorded `capabilities^{}` as a reference would push against it.
#[test]
fn the_advertisement_of_an_empty_repository_is_byte_exact() {
    // A store with nothing in it at all. It used to be the fixture's objects
    // beside a second, empty `MemRefStore`; `GitServe` is one store, and the
    // advertisement reads only the ref namespace, so an empty store says the
    // same thing and says it without pretending two halves can disagree.
    let empty = MemGitServe::new(Kind::Sha1);
    let rp = ReceivePack::new(&empty, Kind::Sha1);
    let mut out = Vec::new();
    rp.write_ref_advertisement(&mut out).expect("advertise");

    let zero = "0".repeat(40);
    let line = format!("{zero} capabilities^{{}}\0{}\n", caps());
    assert_eq!(
        stable(&out),
        format!("{:04x}{line}0000", line.len() + 4),
        "an empty repository still has to say who it is"
    );
}

// ── the status report ───────────────────────────────────────────────────────

/// A gate that refuses one reference, with a reason spanning two lines.
struct RefuseBlocked;

impl PushGate for RefuseBlocked {
    fn check(
        &self,
        command: &RefCommand,
        _facts: &PushFacts<'_>,
    ) -> std::result::Result<(), String> {
        if command.name.as_bytes() == b"refs/heads/blocked" {
            Err("hook declined\nand said so on two lines".to_owned())
        } else {
            Ok(())
        }
    }
}

/// One accepted and one refused reference in one report, with the refusal
/// reason flattened.
///
/// The flattening is now `gunnar_sendpack::report::one_line`'s rather than a
/// private copy here, and a status reason is the one field on this wire that
/// carries text a pusher can influence — a hook's stderr, a gate's message.
#[test]
fn the_status_report_is_byte_exact_including_a_flattened_reason() {
    let f = Fixture::build(Kind::Sha1);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[
            (&zero, &f.merge.to_string(), "refs/heads/allowed"),
            (&zero, &f.merge.to_string(), "refs/heads/blocked"),
        ],
        // No sideband: the report then rides the connection unwrapped, which is
        // what makes it readable as literal bytes. The sidebanded form is
        // covered by `real_git_interop`.
        "report-status-v2 object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let rp = ReceivePack::new(&f.git, f.kind);
    let mut out = Vec::new();
    let outcome = rp
        .serve(
            &mut std::io::Cursor::new(input),
            &mut out,
            &validating_ingest(Kind::Sha1),
            &RefuseBlocked,
        )
        .expect("serve")
        .expect("a push happened");
    assert_eq!(outcome.statuses[0], RefStatus::Ok);
    assert!(matches!(outcome.statuses[1], RefStatus::Rejected(_)));

    assert_eq!(
        report_of(&out),
        "000eunpack ok\n\
         001aok refs/heads/allowed\n\
         0041ng refs/heads/blocked hook declined and said so on two lines\n\
         0000",
        "the newline in the gate's reason must have become a space, and each \
         status must be its own pkt-line"
    );

    // And the refused reference did not move.
    assert!(f.read_ref("refs/heads/blocked").is_none());
    assert_eq!(f.read_ref("refs/heads/allowed").unwrap(), f.merge);
}

#[test]
fn an_all_ok_report_is_byte_exact() {
    let f = Fixture::build(Kind::Sha1);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[(&zero, &f.merge.to_string(), "refs/heads/fresh")],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );
    let rp = ReceivePack::new(&f.git, f.kind);
    let mut out = Vec::new();
    rp.serve(
        &mut std::io::Cursor::new(input),
        &mut out,
        &validating_ingest(Kind::Sha1),
        &AllowAll,
    )
    .expect("serve");
    assert_eq!(
        report_of(&out),
        "000eunpack ok\n0018ok refs/heads/fresh\n0000"
    );
}

/// A reference name that is not UTF-8 must round-trip through the command-list
/// parser byte-for-byte.
///
/// `refs/heads/\xff` and `refs/heads/\xfe` are **indistinguishable** after
/// `String::from_utf8_lossy`, so a parser that went through one would let a
/// push to either land on whichever the store wrote first. The grammar splits
/// on bytes; this asserts the property from the outside, on the server.
#[test]
fn two_reference_names_differing_only_in_invalid_bytes_do_not_collapse() {
    let f = Fixture::build(Kind::Sha1);
    let zero = Kind::Sha1.null().to_string();
    let new = f.merge.to_string();

    let mut input = Vec::new();
    for (i, tail) in [b"\xff".as_slice(), b"\xfe".as_slice()].iter().enumerate() {
        let mut line = format!("{zero} {new} refs/heads/x").into_bytes();
        line.extend_from_slice(tail);
        if i == 0 {
            line.push(0);
            line.extend_from_slice(b"report-status object-format=sha1");
        }
        gunnar_pktline::write_text(&mut input, &line).unwrap();
    }
    gunnar_pktline::write_flush(&mut input).unwrap();
    input.extend_from_slice(&empty_pack(Kind::Sha1));

    let rp = ReceivePack::new(&f.git, f.kind);
    let mut out = Vec::new();
    let outcome = rp
        .serve(
            &mut std::io::Cursor::new(input),
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .expect("serve")
        .expect("a push happened");

    // 🔴 THE ASSERTION MOVED, AND WHAT IT COVERS MOVED WITH IT — 2026-08-10.
    //
    // Both names used to LAND, as two references, and the test read them back
    // out of the store. They cannot land any more and the reason is in the
    // contract, not in this crate: `RefRow::name` and `RefUpdate::name` on
    // `git-storage-trait` are `String`, so a reference name that git will
    // happily create but that is not UTF-8 has nothing to be carried in.
    // `ReceivePack::parse_command` refuses it by name — `UnnameableRef` — which
    // refuses THAT reference with an `ng` line the client can read and leaves
    // the rest of the push untouched. `locked_on_target.md` and this comment
    // record it as a real interop narrowing, deliberately not hidden.
    //
    // **The property under test survives intact**: the two spellings must not
    // collapse onto one. They are still told apart, byte for byte, and each is
    // refused on its own line — which is the same claim about the same parser,
    // asserted one step earlier on the same bytes. `assert_ne!` below is what
    // a `from_utf8_lossy` anywhere in the path would break, and it is the whole
    // point of the test.
    //
    // **What is no longer covered, said plainly**: that the *store* keeps two
    // such names apart. Nothing in gunnar can ask it to any more. That is a
    // question for the day `RefRow::name` widens to bytes, which is a
    // two-implementation decision and not one this test can force.
    assert!(
        outcome.commands.is_empty(),
        "a non-UTF-8 name was accepted as a RefCommand: {:?}",
        outcome.commands
    );
    assert_eq!(outcome.unnameable.len(), 2);
    assert_eq!(outcome.unnameable[0].name, b"refs/heads/x\xff");
    assert_eq!(outcome.unnameable[1].name, b"refs/heads/x\xfe");
    assert_ne!(
        outcome.unnameable[0].name, outcome.unnameable[1].name,
        "the two names collapsed onto one"
    );
    assert_eq!(outcome.unnameable[0].position, 0);
    assert_eq!(outcome.unnameable[1].position, 1);

    // And on the wire: two `ng` lines, each echoing its own bytes back, so a
    // client zipping the report against the list it sent can tell them apart.
    let report = stable(&out);
    assert!(
        report.contains("ng refs/heads/x\u{fffd}"),
        "the refusals did not reach the wire: {report:?}"
    );
    assert_eq!(
        out.windows(4).filter(|w| *w == b"ng r").count(),
        2,
        "two refusals must be two pkt-lines: {report:?}"
    );

    // Neither landed, and no third name was invented.
    assert!(f.read_ref("refs/heads/x\u{fffd}").is_none());
    assert!(
        !f.ref_names()
            .iter()
            .any(|name| name.starts_with("refs/heads/x")),
        "a refused name reached the store anyway"
    );
}