diff --git a/v2/crates/wifi-densepose-bfld/src/frame.rs b/v2/crates/wifi-densepose-bfld/src/frame.rs index c9c68459..e922270e 100644 --- a/v2/crates/wifi-densepose-bfld/src/frame.rs +++ b/v2/crates/wifi-densepose-bfld/src/frame.rs @@ -47,6 +47,20 @@ pub mod flags { pub const PRIVACY_MODE: u16 = 1 << 1; /// ESP32-S3 self-only adapter (ADR-123 §2.5): no `identity_risk_score`. pub const SELF_ONLY: u16 = 1 << 3; + + /// Bitmask covering every named flag this version of the crate knows + /// about. Useful for "did the wire form set any flags I don't recognize?" + /// forward-compat checks. + pub const KNOWN_FLAGS_MASK: u16 = HAS_CSI_DELTA | PRIVACY_MODE | SELF_ONLY; + + /// Complement of [`KNOWN_FLAGS_MASK`] — every bit position not currently + /// assigned a meaning. Bits set in this mask MUST round-trip unchanged + /// per ADR-119 §2.1 ("Reserved flag bits 2-15 lock in future-extension + /// order; any new bit assignment is a version bump"). A future protocol + /// revision may light these up; today's parser preserves them so a node + /// running iter N can forward unknown bits to a peer running iter N+M + /// without losing information. + pub const RESERVED_FLAGS_MASK: u16 = !KNOWN_FLAGS_MASK; } /// On-the-wire BFLD frame header. 86 bytes, little-endian, packed. diff --git a/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs b/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs new file mode 100644 index 00000000..52d7acee --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs @@ -0,0 +1,95 @@ +//! ADR-119 §2.1 reserved-flag-bits forward-compat. The 16-bit `flags` field +//! currently uses bits 0 (HAS_CSI_DELTA), 1 (PRIVACY_MODE), and 3 (SELF_ONLY). +//! Bits 2 and 4..=15 are reserved. The parser must preserve any reserved bit +//! set by a future peer — otherwise round-tripping a frame through a node +//! running an older crate version silently drops information that a newer +//! peer might depend on. + +use wifi_densepose_bfld::frame::flags; +use wifi_densepose_bfld::{BfldFrameHeader, BFLD_HEADER_SIZE}; + +fn header_with_flags(flags_value: u16) -> BfldFrameHeader { + let mut h = BfldFrameHeader::empty(); + h.flags = flags_value; + h +} + +#[test] +fn known_flags_mask_covers_exactly_three_named_flags() { + assert_eq!( + flags::KNOWN_FLAGS_MASK, + flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | flags::SELF_ONLY, + ); + // The three currently-named flags occupy bits 0, 1, 3 — three bits set. + assert_eq!(flags::KNOWN_FLAGS_MASK.count_ones(), 3); +} + +#[test] +fn reserved_and_known_masks_are_complementary() { + assert_eq!(flags::KNOWN_FLAGS_MASK | flags::RESERVED_FLAGS_MASK, u16::MAX); + assert_eq!(flags::KNOWN_FLAGS_MASK & flags::RESERVED_FLAGS_MASK, 0); +} + +#[test] +fn known_flags_do_not_overlap_with_each_other() { + // Each named flag uses exactly one bit and no two of them share a bit. + let pairs = [ + (flags::HAS_CSI_DELTA, flags::PRIVACY_MODE), + (flags::HAS_CSI_DELTA, flags::SELF_ONLY), + (flags::PRIVACY_MODE, flags::SELF_ONLY), + ]; + for (a, b) in pairs { + assert_eq!(a & b, 0, "named flag overlap: 0x{a:04X} & 0x{b:04X}"); + } +} + +#[test] +fn header_preserves_reserved_flag_bits_through_round_trip() { + // Light bit 2 + bits 4..=15 — the full reserved space. + let reserved_set = flags::RESERVED_FLAGS_MASK; + let h = header_with_flags(reserved_set); + let bytes = h.to_le_bytes(); + let parsed = BfldFrameHeader::from_le_bytes(&bytes).expect("parse"); + assert_eq!( + { parsed.flags }, + reserved_set, + "reserved bits must round-trip unchanged for forward-compat", + ); + assert_eq!(bytes.len(), BFLD_HEADER_SIZE); +} + +#[test] +fn header_preserves_mixed_known_and_reserved_bits() { + let mixed = flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | (1 << 7) | (1 << 14); + let h = header_with_flags(mixed); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, mixed); + // Known flags still readable via the named constants. + assert_ne!(({ parsed.flags }) & flags::HAS_CSI_DELTA, 0); + assert_ne!(({ parsed.flags }) & flags::PRIVACY_MODE, 0); +} + +#[test] +fn reserved_bits_do_not_collide_with_self_only_bit_3() { + // SELF_ONLY uses bit 3 — bit 2 is the only unused bit in the 0..=3 range + // and IS part of the reserved mask. + assert_ne!(flags::SELF_ONLY & flags::RESERVED_FLAGS_MASK, flags::SELF_ONLY); + assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 2), 1 << 2); + assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 3), 0); +} + +#[test] +fn all_zero_flags_round_trip_cleanly() { + let h = header_with_flags(0); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, 0); +} + +#[test] +fn all_one_flags_round_trip_cleanly() { + // Stress: every bit set. The parser has no business interpreting this + // configuration but must preserve it. + let h = header_with_flags(u16::MAX); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, u16::MAX); +}