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:
parent
d1bc3cfcf1
commit
08d5cce6ad
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue