feat(adr-118/p1.6): BfldFrame <-> BfldPayload wire integration (39/39 GREEN)
Iter 6. Connects the typed payload parser (iter 5) to the framed
wire format (iter 4): the CRC32 now covers the section-prefixed
payload bytes per ADR-119 §2.2 ("CRC32 covers all section bytes
including length prefixes").
Added:
- BfldFrame::from_payload(header, &BfldPayload) -> Self
Auto-syncs header.flags HAS_CSI_DELTA bit from payload.csi_delta.is_some(),
serializes payload via to_bytes(), feeds BfldFrame::new() which computes
payload_len + payload_crc32 over the section-prefixed bytes.
- BfldFrame::parse_payload(&self) -> Result<BfldPayload, BfldError>
Reads HAS_CSI_DELTA bit from header.flags and dispatches to
BfldPayload::from_bytes(&self.payload, expect_csi_delta).
tests/frame_payload_integration.rs (7 named tests, all green):
from_payload_then_parse_payload_is_identity
from_payload_autosets_has_csi_delta_flag
from_payload_clears_has_csi_delta_flag_when_csi_absent
(verifies the flag is cleared when csi_delta is None even if caller
pre-set the bit; other flag bits like PRIVACY_MODE are preserved)
frame_crc_covers_section_prefixed_bytes
(mutating a byte inside section body trips CRC, not magic/length)
frame_crc_covers_section_length_prefixes
(mutating a section length-prefix byte trips CRC before parser ever runs)
empty_typed_payload_roundtrips
end_to_end_wire_roundtrip_via_bytes
(BfldPayload -> from_payload -> to_bytes -> from_bytes -> parse_payload
is the identity function modulo flag auto-set)
ACs progressed:
- AC5 ↑ — full payload round-trip through the framed bytes (closes
the round-trip leg from BfldPayload through wire and back).
- AC6 ↑ — same input produces same bytes through both layers.
- AC4 ↑ — CRC mismatch on tampered section bodies and tampered section
length prefixes both surface as BfldError::Crc, not as silent acceptance
or as a deeper parser error.
Test config:
- cargo test --no-default-features → 17 passed (integration tests cfg-out)
- cargo test → 39 passed (3 + 6 + 7 + 8 + 8 + 7)
Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 class transition
transformer with subtle::Zeroize on dropped fields.
- IdentityEmbedding newtype with no Serialize impl (ADR-120 §2.5 / I2).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
73ba8d3b27
commit
5312e3c4a1
|
|
@ -223,6 +223,34 @@ impl BfldFrame {
|
|||
Self { header, payload }
|
||||
}
|
||||
|
||||
/// Construct a frame from a typed `BfldPayload`. The header `flags`
|
||||
/// `HAS_CSI_DELTA` bit is auto-synced from `payload.csi_delta.is_some()`,
|
||||
/// then the payload is serialized via [`crate::payload::BfldPayload::to_bytes`]
|
||||
/// and the resulting bytes feed [`BfldFrame::new`]. The CRC therefore covers
|
||||
/// the **section-prefixed** wire bytes per ADR-119 §2.2.
|
||||
#[must_use]
|
||||
pub fn from_payload(
|
||||
mut header: BfldFrameHeader,
|
||||
payload: &crate::payload::BfldPayload,
|
||||
) -> Self {
|
||||
let include_csi_delta = payload.csi_delta.is_some();
|
||||
if include_csi_delta {
|
||||
header.flags |= flags::HAS_CSI_DELTA;
|
||||
} else {
|
||||
header.flags &= !flags::HAS_CSI_DELTA;
|
||||
}
|
||||
let bytes = payload.to_bytes(include_csi_delta);
|
||||
Self::new(header, bytes)
|
||||
}
|
||||
|
||||
/// Parse the opaque payload bytes back into a typed [`crate::payload::BfldPayload`].
|
||||
/// Consults `header.flags & HAS_CSI_DELTA` so the parser matches the
|
||||
/// originating encoder's framing.
|
||||
pub fn parse_payload(&self) -> Result<crate::payload::BfldPayload, BfldError> {
|
||||
let expect_csi_delta = (self.header.flags & flags::HAS_CSI_DELTA) != 0;
|
||||
crate::payload::BfldPayload::from_bytes(&self.payload, expect_csi_delta)
|
||||
}
|
||||
|
||||
/// Serialize to wire form: 86 header bytes + `payload_len` payload bytes.
|
||||
/// Always recomputes `payload_crc32` so the returned bytes are internally
|
||||
/// consistent even if the caller mutated `header.payload_crc32` directly.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
//! End-to-end wire integration: `BfldPayload` ↔ `BfldFrame` (ADR-119 §2.2).
|
||||
//!
|
||||
//! Validates that the frame CRC32 covers the section-prefixed payload bytes
|
||||
//! and that `from_payload` ↔ `parse_payload` are exact inverses.
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
use wifi_densepose_bfld::frame::flags;
|
||||
use wifi_densepose_bfld::{BfldError, BfldFrame, BfldFrameHeader, BfldPayload, BFLD_HEADER_SIZE};
|
||||
|
||||
fn typed_payload(with_csi: bool) -> BfldPayload {
|
||||
BfldPayload {
|
||||
compressed_angle_matrix: vec![0x10; 64],
|
||||
amplitude_proxy: vec![0x20; 32],
|
||||
phase_proxy: vec![0x30; 32],
|
||||
snr_vector: vec![0x40; 16],
|
||||
csi_delta: if with_csi { Some(vec![0x50; 48]) } else { None },
|
||||
vendor_extension: vec![0xAA, 0xBB],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_payload_then_parse_payload_is_identity() {
|
||||
let p_in = typed_payload(true);
|
||||
let frame = BfldFrame::from_payload(BfldFrameHeader::empty(), &p_in);
|
||||
let p_out = frame.parse_payload().expect("parse_payload must succeed");
|
||||
assert_eq!(p_out, p_in);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_payload_autosets_has_csi_delta_flag() {
|
||||
let with_csi = BfldFrame::from_payload(BfldFrameHeader::empty(), &typed_payload(true));
|
||||
assert!(({ with_csi.header.flags } & flags::HAS_CSI_DELTA) != 0);
|
||||
|
||||
let without_csi = BfldFrame::from_payload(BfldFrameHeader::empty(), &typed_payload(false));
|
||||
assert!(({ without_csi.header.flags } & flags::HAS_CSI_DELTA) == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_payload_clears_has_csi_delta_flag_when_csi_absent() {
|
||||
let mut header = BfldFrameHeader::empty();
|
||||
header.flags = flags::HAS_CSI_DELTA | flags::PRIVACY_MODE; // CSI bit forced on
|
||||
let frame = BfldFrame::from_payload(header, &typed_payload(false));
|
||||
// CSI bit cleared because payload had None, PRIVACY_MODE bit preserved.
|
||||
assert_eq!({ frame.header.flags } & flags::HAS_CSI_DELTA, 0);
|
||||
assert_ne!({ frame.header.flags } & flags::PRIVACY_MODE, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_crc_covers_section_prefixed_bytes() {
|
||||
// Flip a byte inside the second section's BODY — section length prefixes
|
||||
// are still intact, magic/version/header are intact, but the CRC must fail.
|
||||
let frame = BfldFrame::from_payload(BfldFrameHeader::empty(), &typed_payload(true));
|
||||
let mut bytes = frame.to_bytes();
|
||||
// First section: prefix at [86..90] (length 64), body at [90..154].
|
||||
// Second section: prefix at [154..158] (length 32), body at [158..190].
|
||||
bytes[170] ^= 0xFF; // inside second section body
|
||||
match BfldFrame::from_bytes(&bytes) {
|
||||
Err(BfldError::Crc { expected, actual }) => assert_ne!(expected, actual),
|
||||
other => panic!("expected Crc error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_crc_covers_section_length_prefixes() {
|
||||
let frame = BfldFrame::from_payload(BfldFrameHeader::empty(), &typed_payload(true));
|
||||
let mut bytes = frame.to_bytes();
|
||||
// Mutate the first section's length prefix high byte from 0 to 0xFF; the
|
||||
// length is now nonsense (would also break the section parser), but at
|
||||
// CRC-check time, the CRC mismatch must fire FIRST before section parsing.
|
||||
bytes[BFLD_HEADER_SIZE + 3] = 0xFF;
|
||||
match BfldFrame::from_bytes(&bytes) {
|
||||
Err(BfldError::Crc { .. }) => {} // expected
|
||||
other => panic!("expected Crc error from prefix tamper, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_typed_payload_roundtrips() {
|
||||
let p_in = BfldPayload::default();
|
||||
let frame = BfldFrame::from_payload(BfldFrameHeader::empty(), &p_in);
|
||||
let bytes = frame.to_bytes();
|
||||
let parsed = BfldFrame::from_bytes(&bytes).expect("frame parse");
|
||||
let p_out = parsed.parse_payload().expect("payload parse");
|
||||
assert_eq!(p_out, p_in);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_to_end_wire_roundtrip_via_bytes() {
|
||||
let p_in = typed_payload(true);
|
||||
let bytes = BfldFrame::from_payload(BfldFrameHeader::empty(), &p_in).to_bytes();
|
||||
let frame = BfldFrame::from_bytes(&bytes).expect("frame parse");
|
||||
let p_out = frame.parse_payload().expect("payload parse");
|
||||
assert_eq!(p_out, p_in);
|
||||
}
|
||||
Loading…
Reference in New Issue