gunnar-sendpack 1.1.0

git's receive-pack wire format, both ends: the send-pack conversation gitoxide does not have, plus the server-side encoders for the same grammar. Plumbing only, no gunnar types.
Documentation
//! The driver's own refusals, and the framing, asserted without a remote.
//!
//! `tests/against_real_git.rs` proves the happy paths against the real
//! `git receive-pack`. What it cannot show is the requests this crate declines
//! to put on the wire at all: by construction, a refused push produces no
//! bytes, so a remote never sees it and cannot report on it.

use std::io::{Cursor, Write};

use gix_hash::{Kind, ObjectId};
use gunnar_sendpack::driver;
use gunnar_sendpack::{advertisement, send_pack, PushCommand, SendPackOptions, Transport};

fn oid(c: char) -> ObjectId {
    ObjectId::from_hex(c.to_string().repeat(40).as_bytes()).unwrap()
}

/// A transport that answers nothing and records everything written to it.
struct Recorder {
    reader: Cursor<Vec<u8>>,
    written: Vec<u8>,
}

impl Recorder {
    fn new() -> Self {
        Recorder {
            reader: Cursor::new(Vec::new()),
            written: Vec::new(),
        }
    }
}

impl Transport for Recorder {
    fn io(&mut self) -> (&mut dyn std::io::Read, &mut dyn Write) {
        (&mut self.reader, &mut self.written)
    }
}

fn adv(caps: &str) -> gunnar_sendpack::Advertisement {
    advertisement::parse(&[format!("{} refs/heads/main\0{caps}\n", oid('1')).into_bytes()]).unwrap()
}

const CAPS: &str = "report-status-v2 delete-refs side-band-64k atomic object-format=sha1";

fn pack_writer(w: &mut dyn Write) -> gunnar_sendpack::Result<()> {
    w.write_all(b"PACK-not-really")?;
    Ok(())
}

/// An all-deletes push introduces no objects. Sending a pack for it is a lie
/// about what the client did, and the remote would index it and find nothing.
#[test]
fn an_all_deletes_push_carrying_a_pack_is_refused_before_a_byte_is_written() {
    let mut t = Recorder::new();
    let err = send_pack(
        &mut t,
        &adv(CAPS),
        &[PushCommand::delete("refs/heads/main", oid('1'))],
        Kind::Sha1,
        Some(pack_writer),
        &SendPackOptions::default(),
    )
    .unwrap_err();
    assert!(format!("{err}").contains("must not carry a pack"), "{err}");
    assert!(
        t.written.is_empty(),
        "a refused push must put NOTHING on the wire; it wrote {} bytes",
        t.written.len()
    );
}

/// The other direction, which is the one that corrupts a repository rather than
/// merely wasting bytes: a create or an update with no pack behind it moves a
/// reference to an object the remote does not have.
#[test]
fn an_update_with_no_pack_is_refused_before_a_byte_is_written() {
    let mut t = Recorder::new();
    let err = send_pack(
        &mut t,
        &adv(CAPS),
        &[PushCommand::update("refs/heads/main", oid('1'), oid('2'))],
        Kind::Sha1,
        None::<fn(&mut dyn Write) -> gunnar_sendpack::Result<()>>,
        &SendPackOptions::default(),
    )
    .unwrap_err();
    assert!(format!("{err}").contains("must carry a pack"), "{err}");
    assert!(t.written.is_empty(), "a refused push writes nothing");
}

/// A guarantee the remote does not offer is refused, and — the part that is
/// easy to get wrong — refused *before* the command list is sent, not after.
#[test]
fn an_unofferable_guarantee_is_refused_before_the_command_list_goes_out() {
    let mut t = Recorder::new();
    let err = send_pack(
        &mut t,
        &adv("report-status object-format=sha1"),
        &[PushCommand::update("refs/heads/main", oid('1'), oid('2'))],
        Kind::Sha1,
        Some(pack_writer),
        &SendPackOptions {
            atomic: true,
            ..Default::default()
        },
    )
    .unwrap_err();
    assert!(format!("{err}").contains("atomic"), "{err}");
    assert!(t.written.is_empty());
}

/// The packfile follows the flush-pkt **raw**. Framing it produces a corrupt
/// push whose checksum error points nowhere near the framing layer.
#[test]
fn the_packfile_is_written_unframed_after_the_command_lists_flush() {
    let mut t = Recorder::new();
    // No report is expected back, so the driver returns without reading — which
    // is what lets this run against a transport that answers nothing.
    let _ = send_pack(
        &mut t,
        &adv("delete-refs object-format=sha1"),
        &[PushCommand::update("refs/heads/main", oid('1'), oid('2'))],
        Kind::Sha1,
        Some(pack_writer),
        &SendPackOptions::default(),
    )
    .expect("a remote offering no report-status is legal");

    let expected_commands = driver::encode_command_list(
        &[PushCommand::update("refs/heads/main", oid('1'), oid('2'))],
        &["object-format=sha1".to_string()],
    )
    .unwrap();
    assert!(
        t.written.starts_with(&expected_commands),
        "the command list must lead, framed and flush-terminated"
    );
    assert_eq!(
        &t.written[expected_commands.len()..],
        b"PACK-not-really",
        "the pack follows the flush-pkt with NO length prefix in front of it"
    );
}

/// Text framing: `<4-hex-length><payload>\n`, and the newline is the framer's.
/// A payload that carried its own would be double-terminated by one end of this
/// crate and single-terminated by the other.
#[test]
fn a_framed_section_is_byte_exact_and_the_newline_belongs_to_the_framer() {
    let mut out = Vec::new();
    driver::frame_section(
        &[b"unpack ok".to_vec(), b"ok refs/heads/main".to_vec()],
        &mut out,
    )
    .unwrap();
    assert_eq!(
        out,
        b"000e"
            .to_vec() // 4 + 9 + 1
            .into_iter()
            .chain(b"unpack ok\n".iter().copied())
            .chain(b"0017".iter().copied()) // 4 + 18 + 1
            .chain(b"ok refs/heads/main\n".iter().copied())
            .chain(b"0000".iter().copied())
            .collect::<Vec<u8>>()
    );
}

#[test]
fn push_options_are_one_pkt_line_each_and_a_newline_in_one_is_refused() {
    let out =
        driver::encode_push_options(&["ci.skip".to_string(), "reviewer=r".to_string()]).unwrap();
    assert_eq!(out, b"000cci.skip\n000freviewer=r\n0000".to_vec());
    assert!(driver::encode_push_options(&["bad\nline".to_string()]).is_err());
}

/// **The client says `ofs-delta` exactly when it will write `OBJ_OFS_DELTA`.**
///
/// # The class, on the push side
///
/// `ofs-delta` is a thing the *client* says: it tells receive-pack the pack may
/// name a delta's base by distance backwards instead of by object id. gunnar's
/// client never said it. `negotiate` had no arm for it at all, while
/// `gunnar-client::push` built `WriteOptions::new(kind)` — whose `ofs_delta`
/// defaults to `true` — so every push gunnar sent used an encoding the wire
/// never declared.
///
/// Nothing broke, and that is the point. `git index-pack` decodes
/// `OBJ_OFS_DELTA` whether or not the capability arrived, so this is capability
/// dishonesty rather than a measured wrong answer — the same class as a policy
/// no capability names, seen from the other end of the connection. A
/// receive-pack that refused what was never offered would be within its rights
/// and gunnar would be the one at fault.
///
/// # What is asserted where, said plainly
///
/// * **Here**, the applied output is the capability string gunnar puts on the
///   wire, read back out of the recorded bytes: present when the remote offers
///   `ofs-delta`, absent when it does not, with the rest of the line unchanged
///   so that "absent" is not "the line collapsed".
/// * The **encoding** half — that `WriteOptions::ofs_delta` really changes the
///   entry headers — is measured against stock `git verify-pack -v`'s entry
///   type column in `gunnar-pack/tests/ofs_delta_off.rs`, and is not re-measured
///   here.
/// * The join is `options::ofs_delta_agreed`, which is the only thing either
///   half reads, and the last arm below pins that both really consult it.
///
/// Seen red: with the `ofs_delta_agreed(adv)` arm removed from `negotiate` —
/// the state this shipped in — the first arm fails with *"the client used
/// `OBJ_OFS_DELTA` … and did not say `ofs-delta`"*.
#[test]
fn the_client_declares_ofs_delta_exactly_when_it_will_use_it() {
    /// The capability string of the first command line, off the wire.
    fn capabilities_on_the_wire(written: &[u8]) -> String {
        let text = String::from_utf8_lossy(written);
        let nul = text
            .find('\0')
            .expect("the first command line carries a NUL and a capability list");
        let rest = &text[nul + 1..];
        rest[..rest.find('\n').unwrap_or(rest.len())].to_owned()
    }

    fn push_to(caps: &str) -> String {
        let mut t = Recorder::new();
        let _ = send_pack(
            &mut t,
            &adv(caps),
            &[PushCommand::update("refs/heads/main", oid('1'), oid('2'))],
            Kind::Sha1,
            Some(pack_writer),
            &SendPackOptions::default(),
        )
        .expect("a remote offering no report-status is legal");
        capabilities_on_the_wire(&t.written)
    }

    // ── the remote offers it: gunnar says it ───────────────────────────────
    let offered = push_to("ofs-delta side-band-64k object-format=sha1");
    assert!(
        offered.split(' ').any(|c| c == "ofs-delta"),
        "the client used `OBJ_OFS_DELTA` in the pack it writes and did not say `ofs-delta` on \
         the wire. The remote is entitled to refuse an encoding it never offered to accept. \
         The capability line was {offered:?}"
    );

    // ── the remote does not: gunnar does not ───────────────────────────────
    let withheld = push_to("side-band-64k object-format=sha1");
    assert!(
        !withheld.split(' ').any(|c| c == "ofs-delta"),
        "the client claimed `ofs-delta` against a remote that never offered it: {withheld:?}"
    );
    // The control. Without it, a client that sent an EMPTY capability list
    // would satisfy the assertion above.
    assert!(
        withheld.split(' ').any(|c| c == "side-band-64k")
            && withheld.contains("object-format=sha1"),
        "withholding one capability emptied the whole line: {withheld:?}"
    );
    // …and the two lines really differ in exactly `ofs-delta`.
    let difference: Vec<&str> = offered
        .split(' ')
        .filter(|c| !withheld.split(' ').any(|w| w == *c))
        .collect();
    assert_eq!(
        difference,
        vec!["ofs-delta"],
        "the two pushes differ in more than the capability under test: {offered:?} against \
         {withheld:?}"
    );

    // ── and the writer is driven by the SAME predicate, not by a second read
    // of the advertisement that could drift from this one.
    assert!(gunnar_sendpack::options::ofs_delta_agreed(&adv(
        "ofs-delta object-format=sha1"
    )));
    assert!(!gunnar_sendpack::options::ofs_delta_agreed(&adv(
        "object-format=sha1"
    )));
}