Menu
AkurAI-Build
publicLatest 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());
}
}