Skip to main content

zerodds_flatdata/
slot.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! SHM-Slot-Header (Spec §2 zerodds-flatdata-1.0).
4//!
5//! Layout:
6//!
7//! ```text
8//! +--------- SHM-Slot ---------+
9//! | 0x00 | u32 | sequence_number |
10//! | 0x04 | u32 | sample_size     |
11//! | 0x08 | u32 | reader_mask     |
12//! | 0x0c | u32 | _reserved       |
13//! | 0x10 | [u8]| FlatStruct-Daten |
14//! +-----------------------------+
15//! ```
16//!
17//! Header-Size: 16 byte. Der Header lebt **ausserhalb** des
18//! FlatStruct-Datenbereichs und ist nicht Teil von `FlatStruct::WIRE_SIZE`.
19
20/// Groesse des Slot-Headers in Bytes.
21pub const SLOT_HEADER_SIZE: usize = 16;
22
23/// Bitmap (32 bit) — pro Reader-Slot ein Bit. Bit gesetzt = Reader hat
24/// gelesen. Slot wird wieder reservierbar wenn alle aktiven Reader-Bits
25/// gesetzt sind, oder Timeout abgelaufen.
26pub type ReaderMask = u32;
27
28/// Slot-Header — wird vom Writer beim `commit_slot` gesetzt und vom
29/// Reader beim `read_flat` interpretiert.
30///
31/// Layout ist `repr(C, packed(4))` damit das Wire-Format byte-stable
32/// auf allen Plattformen ist.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(C, align(4))]
35pub struct SlotHeader {
36    /// Writer-lokale Sequenz-Nummer.
37    pub sequence_number: u32,
38    /// Sample-Size in Bytes (= `T::WIRE_SIZE` fuer `FlatStruct<T>`).
39    pub sample_size: u32,
40    /// Bitmap: welche Reader haben gelesen. 0 = neu, alle 1-Bits = frei.
41    pub reader_mask: ReaderMask,
42    /// Padding fuer Cache-Line-Alignment + zukuenftige Erweiterungen.
43    pub _reserved: u32,
44}
45
46impl SlotHeader {
47    /// Konstruktor fuer neuen Slot beim commit.
48    #[must_use]
49    pub const fn new(sn: u32, sample_size: u32) -> Self {
50        Self {
51            sequence_number: sn,
52            sample_size,
53            reader_mask: 0,
54            _reserved: 0,
55        }
56    }
57
58    /// `true` wenn alle Bits in `active_readers` gesetzt sind ⇒ Slot
59    /// ist frei (alle Reader haben gelesen).
60    #[must_use]
61    pub const fn all_read(&self, active_readers_mask: ReaderMask) -> bool {
62        // Slot ist frei wenn alle aktiven Reader ihren Bit gesetzt haben.
63        // Inactive-Reader (kein Bit in active_readers_mask) zaehlen
64        // automatisch als "gelesen".
65        (self.reader_mask & active_readers_mask) == active_readers_mask
66    }
67
68    /// Setzt Bit `reader_index` im reader_mask (Reader hat gelesen).
69    pub fn mark_read(&mut self, reader_index: u8) {
70        debug_assert!(reader_index < 32, "max 32 Reader pro Topic");
71        self.reader_mask |= 1u32 << reader_index;
72    }
73
74    /// Wire-Encoding: 16 byte little-endian.
75    #[must_use]
76    pub fn to_bytes_le(&self) -> [u8; SLOT_HEADER_SIZE] {
77        let mut out = [0u8; SLOT_HEADER_SIZE];
78        out[0..4].copy_from_slice(&self.sequence_number.to_le_bytes());
79        out[4..8].copy_from_slice(&self.sample_size.to_le_bytes());
80        out[8..12].copy_from_slice(&self.reader_mask.to_le_bytes());
81        out[12..16].copy_from_slice(&self._reserved.to_le_bytes());
82        out
83    }
84
85    /// Wire-Decoding aus 16 byte little-endian.
86    ///
87    /// # Errors
88    /// Liefert `None` wenn `bytes` zu kurz ist.
89    #[must_use]
90    pub fn from_bytes_le(bytes: &[u8]) -> Option<Self> {
91        if bytes.len() < SLOT_HEADER_SIZE {
92            return None;
93        }
94        Some(Self {
95            sequence_number: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
96            sample_size: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
97            reader_mask: u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
98            _reserved: u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
99        })
100    }
101}
102
103#[cfg(test)]
104#[allow(clippy::expect_used, clippy::unwrap_used)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn header_size_is_16() {
110        assert_eq!(SLOT_HEADER_SIZE, 16);
111        assert_eq!(core::mem::size_of::<SlotHeader>(), SLOT_HEADER_SIZE);
112    }
113
114    #[test]
115    fn new_header_has_zero_mask() {
116        let h = SlotHeader::new(7, 24);
117        assert_eq!(h.sequence_number, 7);
118        assert_eq!(h.sample_size, 24);
119        assert_eq!(h.reader_mask, 0);
120    }
121
122    #[test]
123    fn mark_read_sets_bit() {
124        let mut h = SlotHeader::new(1, 24);
125        h.mark_read(0);
126        h.mark_read(2);
127        assert_eq!(h.reader_mask, 0b101);
128    }
129
130    #[test]
131    fn all_read_with_two_active_readers() {
132        let mut h = SlotHeader::new(1, 24);
133        // Aktive Reader: Bit 0 + Bit 1.
134        let active = 0b011;
135        assert!(!h.all_read(active));
136        h.mark_read(0);
137        assert!(!h.all_read(active));
138        h.mark_read(1);
139        assert!(h.all_read(active));
140    }
141
142    #[test]
143    fn inactive_reader_bits_dont_block() {
144        let mut h = SlotHeader::new(1, 24);
145        // Nur Reader 0 ist aktiv. Reader 5 hat sein Bit auch nicht
146        // gesetzt — soll nicht blockieren.
147        let active = 0b001;
148        h.mark_read(0);
149        assert!(h.all_read(active));
150    }
151
152    #[test]
153    fn roundtrip_le() {
154        let h = SlotHeader {
155            sequence_number: 0xAABB_CCDD,
156            sample_size: 24,
157            reader_mask: 0b1111_0000,
158            _reserved: 0,
159        };
160        let bytes = h.to_bytes_le();
161        let h2 = SlotHeader::from_bytes_le(&bytes).expect("decode");
162        assert_eq!(h, h2);
163    }
164
165    #[test]
166    fn from_bytes_too_short_returns_none() {
167        assert!(SlotHeader::from_bytes_le(&[0u8; 15]).is_none());
168    }
169}