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:
ruv
2026-05-24 14:16:54 -04:00
parent 73ba8d3b27
commit 5312e3c4a1
2 changed files with 123 additions and 0 deletions
@@ -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.