AkurAI Build
Menu

AkurAI-Build

public

Latest change 23ae2d15410185bdd0a33888ddbbf8fc1a1d8fea - queue: integrate ref-guard, wire merge-queue worker/recovery, record CI run id by Ólafur Búi Ólafsson

//! Protected Git ref transaction guard.
//!
//! Owned slice: this file only. Not wired into `lib.rs` by this task —
//! the primary integration task (t_2578e75e) adds `mod ref_guard;` and
//! calls these functions from the git-receive-pack handler. Tests here
//! use `#[path]` to include the module directly so no other files need
//! editing.
//!
//! Two responsibilities:
//! - `protected_receive_commands`: parse a `git-receive-pack` request
//!   body (pkt-line framed ref update commands) and fail closed if any
//!   command targets a protected ref.
//! - `compare_and_swap`: perform an atomic `git update-ref` with an
//!   expected-old-value guard, so a ref only moves if it still points
//!   where the caller last observed.
//!
//! Wire format per gitprotocol-pack(5):
//!   update-requests = *shallow ( command-list | push-cert )
//!   shallow         = PKT-LINE("shallow" SP obj-id)
//!   command-list    = PKT-LINE(command NUL capability-list)
//!                     *PKT-LINE(command)
//!                     flush-pkt
//!   command         = old-id SP new-id SP name

use std::path::Path;
use std::process::Command;

/// Git protocol hard limit: a pkt-line's total length (4-byte hex
/// prefix + payload) MUST NOT exceed 65520 bytes (65516 payload max).
/// See gitprotocol-common(5) pkt-line format.
const PKT_MAX_LEN: usize = 65520;
/// Hard cap on number of ref-update commands accepted in one push.
/// Bounds parsing cost; fails closed rather than looping unbounded.
const MAX_COMMANDS: usize = 4096;
/// Hard cap on leading `shallow` lines, same rationale.
const MAX_SHALLOW_LINES: usize = 4096;
/// Bound on the raw capability-list text length attached to the first
/// command line, so a malicious/huge capability string can't be used
/// to inflate parsing cost.
const MAX_CAPABILITY_LIST_LEN: usize = 4096;
/// Bound on the number of individual capability tokens.
const MAX_CAPABILITY_TOKENS: usize = 64;
/// SHA-1 hex object id length.
const OID_LEN_SHA1: usize = 40;
/// SHA-256 hex object id length (future-proofing; git supports both).
const OID_LEN_SHA256: usize = 64;

/// Reject a receive-pack request if any ref-update command targets a
/// protected ref. `protected` holds full ref names (e.g.
/// "refs/heads/main"). Fails closed: any malformed input, including a
/// malformed entry in `protected` itself, is an error, never silently
/// accepted.
pub fn protected_receive_commands(body: &[u8], protected: &[String]) -> Result<(), String> {
    for reference in protected {
        validate_ref_name(reference)
            .map_err(|error| format!("protected ref config entry invalid: {error}"))?;
    }
    let commands = parse_receive_pack_commands(body)?;
    for command in &commands {
        if protected
            .iter()
            .any(|reference| reference == &command.reference)
        {
            return Err(format!(
                "ref '{}' is protected: push rejected",
                command.reference
            ));
        }
    }
    Ok(())
}

#[derive(Debug, Eq, PartialEq)]
struct ReceiveCommand {
    #[allow(dead_code)]
    old_oid: String,
    #[allow(dead_code)]
    new_oid: String,
    reference: String,
}

/// Parse `update-requests = *shallow (command-list | push-cert)` from
/// the start of a `git-receive-pack` request body, stopping at the
/// command-list's flush-pkt. Any pack data that follows is ignored.
/// Bounded and fail-closed: truncated frames, oversized/malformed
/// pkt-lines, missing/malformed capability framing, signed
/// (`push-cert`) pushes, and malformed command text all return `Err`
/// rather than silently accepting or skipping.
fn parse_receive_pack_commands(body: &[u8]) -> Result<Vec<ReceiveCommand>, String> {
    let mut offset = 0usize;

    // 1. *shallow: zero or more "shallow <oid>" pkt-lines.
    let mut shallow_count = 0usize;
    loop {
        let (data, next_offset, is_flush) = read_pkt_line(body, offset)?;
        if is_flush {
            // A bare flush here with no command-list is a client that
            // has nothing to push; treat as zero commands.
            return Ok(Vec::new());
        }
        if !data.starts_with(b"shallow ") {
            break;
        }
        let oid = std::str::from_utf8(&data[b"shallow ".len()..])
            .map_err(|_| "shallow line is not valid UTF-8".to_owned())?
            .trim_end_matches('\n');
        validate_oid(oid).map_err(|error| format!("invalid shallow line: {error}"))?;
        shallow_count += 1;
        if shallow_count > MAX_SHALLOW_LINES {
            return Err(format!("too many shallow lines (max {MAX_SHALLOW_LINES})"));
        }
        offset = next_offset;
    }

    // 2. command-list | push-cert. Fail closed on push-cert: this
    // guard does not parse signed-push certificates, so a client that
    // sends one is rejected outright rather than risk misreading its
    // embedded commands.
    let (first_data, mut offset, is_flush) = read_pkt_line(body, offset)?;
    if is_flush {
        return Ok(Vec::new());
    }
    if first_data.starts_with(b"push-cert") {
        return Err("signed push (push-cert) is not supported by this guard".to_owned());
    }

    // First command-list line MUST carry "command NUL capability-list".
    let nul_index = first_data.iter().position(|&b| b == 0).ok_or_else(|| {
        "first ref-update command missing required capability-list NUL".to_owned()
    })?;
    let (command_bytes, capability_bytes) =
        (&first_data[..nul_index], &first_data[nul_index + 1..]);
    validate_capability_list(capability_bytes)?;
    let first_command = parse_command_bytes(command_bytes)?;

    let mut commands = vec![first_command];

    // 3. Remaining command lines: MUST NOT carry a NUL (capabilities
    // are only ever attached to the first line), terminated by
    // flush-pkt.
    loop {
        let (data, next_offset, is_flush) = read_pkt_line(body, offset)?;
        if is_flush {
            return Ok(commands);
        }
        if data.contains(&0) {
            return Err("unexpected capability-list NUL on a non-first command line".to_owned());
        }
        commands.push(parse_command_bytes(data)?);
        if commands.len() > MAX_COMMANDS {
            return Err(format!("too many ref-update commands (max {MAX_COMMANDS})"));
        }
        offset = next_offset;
    }
}

/// Read one pkt-line at `offset`. Returns (payload, offset-after,
/// is_flush). Bounded and fail-closed: truncated length prefixes,
/// non-hex lengths, lengths under the 4-byte minimum, lengths over
/// the protocol's 65520-byte maximum, and truncated payloads all
/// return `Err`.
fn read_pkt_line(body: &[u8], offset: usize) -> Result<(&[u8], usize, bool), String> {
    if offset + 4 > body.len() {
        return Err("truncated pkt-line length prefix".to_owned());
    }
    let length = parse_pkt_length(&body[offset..offset + 4])?;
    if length == 0 {
        return Ok((&body[offset..offset], offset + 4, true));
    }
    if length < 4 {
        return Err(format!("invalid pkt-line length {length}"));
    }
    if length > PKT_MAX_LEN {
        return Err(format!(
            "pkt-line length {length} exceeds max {PKT_MAX_LEN}"
        ));
    }
    let data_len = length - 4;
    let data_start = offset + 4;
    let data_end = data_start
        .checked_add(data_len)
        .ok_or_else(|| "pkt-line length overflow".to_owned())?;
    if data_end > body.len() {
        return Err("pkt-line data exceeds body bounds".to_owned());
    }
    Ok((&body[data_start..data_end], data_end, false))
}

fn parse_pkt_length(bytes: &[u8]) -> Result<usize, String> {
    let text = std::str::from_utf8(bytes).map_err(|_| "pkt-line length is not ASCII".to_owned())?;
    usize::from_str_radix(text, 16).map_err(|_| format!("invalid pkt-line length hex '{text}'"))
}

/// Validate a capability-list per gitprotocol-common(5) and
/// gitprotocol-capabilities(5):
///   capability-list = capability *(SP capability)
///   capability      = 1*(LC_ALPHA / DIGIT / "-" / "_")
/// A handful of documented capabilities carry a value attached with
/// `=` (`agent=<value>`, `session-id=<session-id>`, `push-cert=<nonce>`).
/// The base name before any `=` MUST still match the strict grammar
/// above; the value after `=` must be non-empty, printable, and
/// contain neither whitespace nor a further `=` (documented values
/// like session-id are explicitly required to avoid whitespace and
/// non-printable characters, and none of the documented value forms
/// nest another `=`, so a second `=` is treated as malformed framing
/// rather than silently accepted).
/// The list itself must be non-empty: an absent/blank capability-list
/// after the required NUL is malformed framing, not an implicit "no
/// capabilities" declaration.
/// At most one trailing LF is permitted (per pkt-line's optional
/// trailing-LF convention); a second trailing LF is rejected rather
/// than silently stripped.
fn validate_capability_list(bytes: &[u8]) -> Result<(), String> {
    if bytes.len() > MAX_CAPABILITY_LIST_LEN {
        return Err(format!(
            "capability-list exceeds max length {MAX_CAPABILITY_LIST_LEN}"
        ));
    }
    let text =
        std::str::from_utf8(bytes).map_err(|_| "capability-list is not valid UTF-8".to_owned())?;
    // Strip at most one trailing LF; a second one is malformed framing.
    let text = match text.strip_suffix('\n') {
        Some(rest) => {
            if rest.ends_with('\n') {
                return Err("capability-list has more than one trailing LF".to_owned());
            }
            rest
        }
        None => text,
    };
    // Stock send-pack inserts one separator space immediately after NUL.
    let text = text.strip_prefix(' ').unwrap_or(text);
    if text.is_empty() {
        return Err("capability-list must not be empty".to_owned());
    }
    let tokens: Vec<&str> = text.split(' ').collect();
    if tokens.len() > MAX_CAPABILITY_TOKENS {
        return Err(format!(
            "capability-list has too many tokens (max {MAX_CAPABILITY_TOKENS})"
        ));
    }
    for token in tokens {
        validate_capability_token(token)?;
    }
    Ok(())
}

#[test]
fn stock_git_capability_separator() {
    assert!(
        validate_capability_list(
            b" report-status-v2 side-band-64k quiet object-format=sha1 agent=git/2.55.0-Linux"
        )
        .is_ok()
    );
    assert!(validate_capability_list(b"  report-status").is_err());
    assert!(validate_capability_list(b" ").is_err());
}

fn validate_capability_token(token: &str) -> Result<(), String> {
    if token.is_empty() {
        return Err("capability-list has an empty token".to_owned());
    }
    let (name, value) = match token.split_once('=') {
        Some((name, value)) => (name, Some(value)),
        None => (token, None),
    };
    if name.is_empty() || !name.bytes().all(is_capability_name_byte) {
        return Err(format!("capability has an invalid name: '{token}'"));
    }
    if let Some(value) = value {
        if value.is_empty() {
            return Err(format!("capability value must not be empty: '{token}'"));
        }
        if value.contains('=') {
            return Err(format!(
                "capability value must not contain a second '=': '{token}'"
            ));
        }
        if !value.bytes().all(|b| b.is_ascii_graphic()) {
            return Err(format!(
                "capability value must be printable, non-whitespace ASCII: '{token}'"
            ));
        }
    }
    Ok(())
}

/// Strict `capability` grammar byte class: lowercase ASCII letter,
/// digit, hyphen, or underscore only.
fn is_capability_name_byte(b: u8) -> bool {
    b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'-' | b'_')
}

fn parse_command_bytes(data: &[u8]) -> Result<ReceiveCommand, String> {
    let text = std::str::from_utf8(data)
        .map_err(|_| "pkt-line command is not valid UTF-8".to_owned())?
        .trim_end_matches('\n');
    parse_command_line(text)
}

fn parse_command_line(text: &str) -> Result<ReceiveCommand, String> {
    let mut parts = text.splitn(3, ' ');
    let old_oid = parts.next().ok_or("missing old-oid in command")?;
    let new_oid = parts.next().ok_or("missing new-oid in command")?;
    let reference = parts.next().ok_or("missing ref-name in command")?;
    if parts.next().is_some() {
        return Err("malformed ref-update command: too many fields".to_owned());
    }
    validate_oid(old_oid)?;
    validate_oid(new_oid)?;
    validate_ref_name(reference)?;
    Ok(ReceiveCommand {
        old_oid: old_oid.to_owned(),
        new_oid: new_oid.to_owned(),
        reference: reference.to_owned(),
    })
}

/// Strict hex object-id validation: exact SHA-1 or SHA-256 length,
/// lowercase-or-uppercase hex only. The all-zero id (ref creation or
/// deletion sentinel) is valid.
fn validate_oid(oid: &str) -> Result<(), String> {
    let len = oid.len();
    if len != OID_LEN_SHA1 && len != OID_LEN_SHA256 {
        return Err(format!("invalid object id length: '{oid}'"));
    }
    if !oid.bytes().all(|b| b.is_ascii_hexdigit()) {
        return Err(format!("invalid object id (not hex): '{oid}'"));
    }
    Ok(())
}

/// Validate a full ref name using `git check-ref-format`, the
/// authoritative parser for ref syntax. Never shells out with the
/// name interpolated into a string; passed as a single argv element.
fn validate_ref_name(reference: &str) -> Result<(), String> {
    if reference.is_empty() || reference.len() > 1024 {
        return Err("ref name has invalid length".to_owned());
    }
    let status = Command::new("git")
        .args(["check-ref-format", reference])
        .status()
        .map_err(|error| format!("failed to invoke git check-ref-format: {error}"))?;
    if !status.success() {
        return Err(format!("ref name fails check-ref-format: '{reference}'"));
    }
    Ok(())
}

/// Atomically update `reference` in `repo` from `expected` to `next`
/// using `git update-ref`'s built-in compare-and-swap (the old value
/// is passed as an argv element, not interpolated into a shell
/// string, so there is no injection surface). Fails closed if the
/// ref no longer matches `expected`, if any argument fails strict
/// validation, or if the underlying git invocation errors. Passing
/// the all-zero oid as `next` deletes the ref (git update-ref's
/// normal deletion form), still gated on `expected` matching.
pub fn compare_and_swap(
    repo: &Path,
    reference: &str,
    expected: &str,
    next: &str,
) -> Result<(), String> {
    validate_ref_name(reference)?;
    validate_oid(expected)?;
    validate_oid(next)?;

    let output = Command::new("git")
        .current_dir(repo)
        .args(["update-ref", reference, next, expected])
        .output()
        .map_err(|error| format!("failed to invoke git update-ref: {error}"))?;

    if !output.status.success() {
        return Err(format!(
            "git update-ref failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pkt_line(data: &str) -> Vec<u8> {
        let len = data.len() + 4;
        let mut out = format!("{len:04x}").into_bytes();
        out.extend_from_slice(data.as_bytes());
        out
    }

    fn pkt_line_bytes(data: &[u8]) -> Vec<u8> {
        let len = data.len() + 4;
        let mut out = format!("{len:04x}").into_bytes();
        out.extend_from_slice(data);
        out
    }

    fn flush() -> Vec<u8> {
        b"0000".to_vec()
    }

    const ZERO: &str = "0000000000000000000000000000000000000000";
    const SHA_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    const SHA_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";

    #[test]
    fn accepts_unprotected_ref_update() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/feature\0report-status\n"
        ));
        body.extend(flush());
        let protected = vec!["refs/heads/main".to_owned()];
        assert!(protected_receive_commands(&body, &protected).is_ok());
    }

    #[test]
    fn rejects_protected_ref_update() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/main\0report-status\n"));
        body.extend(flush());
        let protected = vec!["refs/heads/main".to_owned()];
        let result = protected_receive_commands(&body, &protected);
        assert!(result.is_err(), "protected ref update should be rejected");
    }

    #[test]
    fn rejects_protected_ref_deletion() {
        let mut body = pkt_line(&format!("{SHA_A} {ZERO} refs/heads/main\0report-status\n"));
        body.extend(flush());
        let protected = vec!["refs/heads/main".to_owned()];
        let result = protected_receive_commands(&body, &protected);
        assert!(result.is_err(), "protected ref deletion should be rejected");
    }

    #[test]
    fn accepts_multiple_commands_only_first_has_capabilities() {
        let mut body = pkt_line(&format!("{ZERO} {SHA_A} refs/heads/a\0report-status\n"));
        body.extend(pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/b\n")));
        body.extend(flush());
        let protected = vec!["refs/heads/main".to_owned()];
        assert!(protected_receive_commands(&body, &protected).is_ok());
    }

    #[test]
    fn rejects_truncated_length_prefix() {
        let body = b"00".to_vec();
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_non_hex_length_prefix() {
        let mut body = b"zzzz".to_vec();
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_pkt_line_declaring_more_data_than_present() {
        let mut body = b"fff0".to_vec();
        body.extend_from_slice(b"short");
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_oversized_pkt_line_length() {
        // 65521 > protocol max 65520.
        let body = b"fff1".to_vec();
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn accepts_boundary_pkt_line_length_of_65520() {
        // Exactly the protocol maximum total pkt-line length must be
        // accepted as a valid *length*, even though this particular
        // payload is nonsense and fails command parsing downstream
        // (proves the length gate itself doesn't reject 65520).
        let mut body = b"fff0".to_vec(); // 65520 total length
        body.extend(vec![b'x'; 65520 - 4]);
        let result = parse_receive_pack_commands(&body);
        assert!(
            result.is_err(),
            "content is garbage so parsing still fails downstream"
        );
        match result {
            Err(message) => assert!(
                !message.contains("exceeds max"),
                "65520 must not be rejected as oversized: {message}"
            ),
            Ok(_) => panic!("expected an error from garbage command content"),
        }
    }

    #[test]
    fn rejects_malformed_command_missing_fields() {
        let mut body = pkt_line("onlyonefield\0report-status\n");
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_invalid_object_id() {
        let mut body = pkt_line("not-hex not-hex-either refs/heads/x\0report-status\n");
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_invalid_ref_name_via_check_ref_format() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} not..a..valid..ref\0report-status\n"
        ));
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn empty_body_with_only_flush_is_ok() {
        let body = flush();
        assert!(protected_receive_commands(&body, &["refs/heads/main".to_owned()]).is_ok());
    }

    #[test]
    fn too_many_commands_fails_closed() {
        let mut body = pkt_line(&format!("{ZERO} {SHA_A} refs/heads/first\0report-status\n"));
        for _ in 0..MAX_COMMANDS {
            body.extend(pkt_line(&format!("{ZERO} {SHA_A} refs/heads/x\n")));
        }
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn accepts_valid_shallow_lines_before_command_list() {
        let mut body = pkt_line(&format!("shallow {SHA_A}"));
        body.extend(pkt_line(&format!("shallow {SHA_B}")));
        body.extend(pkt_line(&format!(
            "{ZERO} {SHA_A} refs/heads/x\0report-status\n"
        )));
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_ok());
    }

    #[test]
    fn rejects_shallow_line_with_invalid_oid() {
        let mut body = pkt_line("shallow not-a-valid-oid");
        body.extend(pkt_line(&format!(
            "{ZERO} {SHA_A} refs/heads/x\0report-status\n"
        )));
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_push_cert_as_unsupported() {
        let mut body = pkt_line_bytes(b"push-cert\0report-status\n");
        body.extend(flush());
        let result = protected_receive_commands(&body, &[]);
        assert!(result.is_err(), "push-cert should be rejected outright");
    }

    #[test]
    fn rejects_first_command_missing_capability_nul() {
        // No NUL byte at all on the first (and only) command line.
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/feature\n"));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "missing capability-list NUL on first command must fail closed"
        );
    }

    #[test]
    fn rejects_capability_list_with_invalid_token() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/feature\0bad token!!\n"
        ));
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_err());
    }

    #[test]
    fn rejects_non_first_command_with_capability_nul() {
        let mut body = pkt_line(&format!("{ZERO} {SHA_A} refs/heads/a\0report-status\n"));
        body.extend(pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/b\0report-status\n"
        )));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "a NUL on a later command line is malformed framing"
        );
    }

    #[test]
    fn rejects_malformed_protected_ref_entry() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/main\0report-status\n"));
        body.extend(flush());
        // "main" (short name, not a full ref) must not silently fail
        // to protect refs/heads/main.
        let protected = vec!["main".to_owned()];
        assert!(
            protected_receive_commands(&body, &protected).is_err(),
            "malformed protected-ref config must fail closed, not silently disable protection"
        );
    }

    #[test]
    fn rejects_empty_capability_list() {
        // NUL present but nothing after it: an absent capability-list
        // is malformed framing, not an implicit "no capabilities".
        let mut body = pkt_line_bytes(format!("{SHA_A} {SHA_B} refs/heads/feature\0").as_bytes());
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "empty capability-list must be rejected, not accepted"
        );
    }

    #[test]
    fn rejects_empty_capability_list_with_trailing_lf() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/feature\0\n"));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "empty capability-list (even with just a trailing LF) must be rejected"
        );
    }

    #[test]
    fn rejects_uppercase_capability_name() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/feature\0Report-status\n"
        ));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "capability grammar is lowercase-only: uppercase name must be rejected"
        );
    }

    #[test]
    fn rejects_capability_with_leading_equals() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/feature\0=bad\n"));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "a capability token with no name before '=' must be rejected"
        );
    }

    #[test]
    fn rejects_capability_with_repeated_equals() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/feature\0agent==bad\n"));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "a capability value containing a second '=' must be rejected"
        );
    }

    #[test]
    fn rejects_capability_with_empty_value() {
        let mut body = pkt_line(&format!("{SHA_A} {SHA_B} refs/heads/feature\0agent=\n"));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "a capability with an empty value after '=' must be rejected"
        );
    }

    #[test]
    fn rejects_colon_bearing_token() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/feature\0weird:token\n"
        ));
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "colon is not in the capability-name grammar and must be rejected"
        );
    }

    #[test]
    fn rejects_duplicate_trailing_lf_on_capability_list() {
        let mut body = pkt_line_bytes(
            format!("{SHA_A} {SHA_B} refs/heads/feature\0report-status\n\n").as_bytes(),
        );
        body.extend(flush());
        assert!(
            protected_receive_commands(&body, &[]).is_err(),
            "at most one trailing LF is permitted on the capability-list line"
        );
    }

    #[test]
    fn accepts_valid_capability_names_and_documented_value_forms() {
        let mut body = pkt_line(&format!(
            "{SHA_A} {SHA_B} refs/heads/feature\0report-status delete-refs side-band-64k agent=git/2.43.0\n"
        ));
        body.extend(flush());
        assert!(protected_receive_commands(&body, &[]).is_ok());
    }
}