feat(adr-118/p1.10): frame parser trailing-bytes contract (296/296 GREEN)

Iter 43. Pins BfldFrame::from_bytes behavior on buffers carrying bytes
past `BFLD_HEADER_SIZE + header.payload_len`. The parser currently
accepts these and silently slices to the declared length. Useful when
the transport (UDP MTU padding, ESP-NOW trailer alignment) adds noise
the application layer doesn't strip.

Pinning this behavior makes any future tightening (reject as
MalformedFrame) a deliberate, traceable policy change rather than
silent breakage.

Added (in tests/frame_trailing_bytes.rs, 6 named tests):
  parser_accepts_buffer_with_one_trailing_byte
    (smoke: one extra 0xFF byte tolerated; payload.last() != Some(0xFF))
  parser_accepts_many_trailing_bytes
    (256 trailing bytes — UDP MTU padding scale)
  parsed_payload_round_trips_back_to_typed_payload_with_trailing_bytes_present
    *** Sanity: trailing-bytes leniency must not corrupt the section
        parser downstream. from_bytes → parse_payload still yields
        the original BfldPayload byte-for-byte. ***
  header_only_buffer_at_exactly_header_size_with_zero_payload_len_succeeds
    (boundary: empty-payload frame is exactly 86 bytes)
  header_only_buffer_with_trailing_bytes_but_zero_payload_len_ignores_them
    (100 trailing bytes; parsed.payload stays empty)
  trailing_bytes_do_not_affect_crc_validation_when_payload_intact
    (CRC is over payload bytes only; 32 trailing bytes leave CRC
     intact and parse succeeds)

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged
  at 431 lines. SENSE-BRIDGE scope remains orthogonal.

ACs progressed:
- ADR-119 wire-format parser contract: trailing-bytes tolerance is
  now an explicit, tested behavior. Operators building stream-based
  frame readers (where multiple frames concatenate) know the parser
  treats `header.payload_len` as authoritative, not buffer.len().

Test config:
- cargo test --no-default-features → 90 passed (frame_trailing_bytes cfg-out)
- cargo test                       → 296 passed (290 + 6)

Out of scope (next iter target):
- PR-readiness pivot: CHANGELOG, witness bundle, AC closeout table.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 19:27:33 -04:00
parent d1bc3cfcf1
commit 08d5cce6ad
1 changed files with 105 additions and 0 deletions

View File

@ -0,0 +1,105 @@
//! `BfldFrame::from_bytes` trailing-bytes contract. Pins the current
//! behavior: the parser reads exactly `header.payload_len` bytes after the
//! header and silently ignores anything past `BFLD_HEADER_SIZE +
//! header.payload_len`. This matches how the parser is used in iter-4
//! through iter-15: callers hand a sliced buffer that may include framing
//! noise (UDP MTU padding, ESP-NOW trailer alignment), and the parser
//! extracts only what the header declares.
//!
//! If a future iter decides to tighten this (reject trailing bytes as
//! `MalformedFrame`), updating this test makes the policy change deliberate
//! and traceable rather than silent.
#![cfg(feature = "std")]
use wifi_densepose_bfld::{BfldFrame, BfldFrameHeader, BfldPayload, BFLD_HEADER_SIZE};
fn frame_with_typed_payload() -> BfldFrame {
let payload = BfldPayload {
compressed_angle_matrix: vec![0x11; 32],
amplitude_proxy: vec![0x22; 16],
phase_proxy: vec![0x33; 16],
snr_vector: vec![0x44; 8],
csi_delta: None,
vendor_extension: vec![],
};
BfldFrame::from_payload(BfldFrameHeader::empty(), &payload)
}
#[test]
fn parser_accepts_buffer_with_one_trailing_byte() {
let frame = frame_with_typed_payload();
let mut bytes = frame.to_bytes();
let canonical_len = bytes.len();
bytes.push(0xFF);
let parsed = BfldFrame::from_bytes(&bytes).expect("trailing byte must be tolerated");
assert_eq!(
parsed.payload.len(),
{ parsed.header.payload_len } as usize,
"parsed payload size must equal header.payload_len, not buffer.len() - HEADER",
);
// Implicit: the trailing 0xFF byte is NOT in parsed.payload.
assert_ne!(parsed.payload.last().copied(), Some(0xFF));
let _ = canonical_len; // sanity anchor
}
#[test]
fn parser_accepts_many_trailing_bytes() {
let frame = frame_with_typed_payload();
let mut bytes = frame.to_bytes();
bytes.extend_from_slice(&[0xCC; 256]);
let parsed = BfldFrame::from_bytes(&bytes).expect("256 trailing bytes must be tolerated");
assert_eq!(parsed.payload.len(), { parsed.header.payload_len } as usize);
}
#[test]
fn parsed_payload_round_trips_back_to_typed_payload_with_trailing_bytes_present() {
// The trailing-bytes parser leniency must not corrupt the section parser
// downstream. After from_bytes + parse_payload, the typed payload should
// match the original BfldPayload byte-for-byte.
let original_payload = BfldPayload {
compressed_angle_matrix: vec![0x11; 32],
amplitude_proxy: vec![0x22; 16],
phase_proxy: vec![0x33; 16],
snr_vector: vec![0x44; 8],
csi_delta: None,
vendor_extension: vec![],
};
let frame = BfldFrame::from_payload(BfldFrameHeader::empty(), &original_payload);
let mut bytes = frame.to_bytes();
bytes.extend_from_slice(&[0xEE; 64]);
let parsed_frame = BfldFrame::from_bytes(&bytes).unwrap();
let parsed_payload = parsed_frame.parse_payload().expect("typed payload parse");
assert_eq!(parsed_payload, original_payload);
}
#[test]
fn header_only_buffer_at_exactly_header_size_with_zero_payload_len_succeeds() {
let header = BfldFrameHeader::empty();
let frame = BfldFrame::new(header, Vec::new());
let bytes = frame.to_bytes();
assert_eq!(bytes.len(), BFLD_HEADER_SIZE, "empty-payload frame is exactly header size");
let parsed = BfldFrame::from_bytes(&bytes).expect("parse");
assert!(parsed.payload.is_empty());
}
#[test]
fn header_only_buffer_with_trailing_bytes_but_zero_payload_len_ignores_them() {
let header = BfldFrameHeader::empty();
let frame = BfldFrame::new(header, Vec::new());
let mut bytes = frame.to_bytes();
bytes.extend_from_slice(&[0xAA; 100]);
let parsed = BfldFrame::from_bytes(&bytes).expect("parse");
assert_eq!({ parsed.header.payload_len }, 0);
assert!(parsed.payload.is_empty(), "trailing bytes must not leak into payload");
}
#[test]
fn trailing_bytes_do_not_affect_crc_validation_when_payload_intact() {
let frame = frame_with_typed_payload();
let mut bytes = frame.to_bytes();
let crc_before_extension = { frame.header.payload_crc32 };
bytes.extend_from_slice(&[0xFF; 32]);
let parsed = BfldFrame::from_bytes(&bytes).expect("CRC over payload-only must still match");
assert_eq!({ parsed.header.payload_crc32 }, crc_before_extension);
}