From 5312e3c4a1a9a13796912e7defb5ff8233a5bfaa Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 24 May 2026 14:16:54 -0400 Subject: [PATCH] feat(adr-118/p1.6): BfldFrame <-> BfldPayload wire integration (39/39 GREEN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- v2/crates/wifi-densepose-bfld/src/frame.rs | 28 ++++++ .../tests/frame_payload_integration.rs | 95 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 v2/crates/wifi-densepose-bfld/tests/frame_payload_integration.rs diff --git a/v2/crates/wifi-densepose-bfld/src/frame.rs b/v2/crates/wifi-densepose-bfld/src/frame.rs index 6fedeb24..c9c68459 100644 --- a/v2/crates/wifi-densepose-bfld/src/frame.rs +++ b/v2/crates/wifi-densepose-bfld/src/frame.rs @@ -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 { + 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. diff --git a/v2/crates/wifi-densepose-bfld/tests/frame_payload_integration.rs b/v2/crates/wifi-densepose-bfld/tests/frame_payload_integration.rs new file mode 100644 index 00000000..7c2fdb67 --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/frame_payload_integration.rs @@ -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); +}