Skip to main content

zerodds_rtps/
submessage_header.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Submessage-Header (DDSI-RTPS 2.5 §8.3.4).
4//!
5//! Jedes Submessage in einem RTPS-Datagram beginnt mit einem 4-Byte-
6//! Header:
7//!
8//! ```text
9//!   0                   1                   2                   3
10//!   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
11//!  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12//!  | submessageId  |     flags     |       octetsToNextHeader      |
13//!  +---------------+---------------+---------------+---------------+
14//! ```
15//!
16//! `flags` traegt mindestens das **E-Flag** (Bit 0) fuer Endianness
17//! des Submessage-Bodies (1 = LE, 0 = BE). `octetsToNextHeader` ist
18//! die Body-Laenge in Bytes; `0` markiert ein Last-Submessage-Spezial-
19//! Verhalten (siehe Spec §8.3.4.2).
20
21use crate::error::WireError;
22
23/// Submessage-IDs, die in Phase 0 unterstuetzt sind. Werte aus
24/// Spec-Tabelle 8.13.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26#[repr(u8)]
27#[allow(missing_docs)]
28pub enum SubmessageId {
29    Pad = 0x01,
30    AckNack = 0x06,
31    Heartbeat = 0x07,
32    Gap = 0x08,
33    InfoTs = 0x09,
34    InfoSrc = 0x0A,
35    InfoReplyIp4 = 0x0C,
36    InfoDst = 0x0E,
37    InfoReply = 0x0F,
38    NackFrag = 0x12,
39    HeartbeatFrag = 0x13,
40    Data = 0x15,
41    DataFrag = 0x16,
42}
43
44impl SubmessageId {
45    /// Roher Wire-Wert.
46    #[must_use]
47    pub fn as_u8(self) -> u8 {
48        self as u8
49    }
50
51    /// Konvertiert ein Byte. Unbekannte IDs sind erlaubt — Spec
52    /// fordert, dass Reader unbekannte Submessages **skippen** (via
53    /// `octetsToNextHeader`). Dafuer nutzen wir die UnknownSubmessageId-
54    /// Variante des Errors nur bei expliziter Validation.
55    ///
56    /// # Errors
57    /// `UnknownSubmessageId`.
58    pub fn from_u8(byte: u8) -> Result<Self, WireError> {
59        match byte {
60            0x01 => Ok(Self::Pad),
61            0x06 => Ok(Self::AckNack),
62            0x07 => Ok(Self::Heartbeat),
63            0x08 => Ok(Self::Gap),
64            0x09 => Ok(Self::InfoTs),
65            0x0A => Ok(Self::InfoSrc),
66            0x0C => Ok(Self::InfoReplyIp4),
67            0x0E => Ok(Self::InfoDst),
68            0x0F => Ok(Self::InfoReply),
69            0x12 => Ok(Self::NackFrag),
70            0x13 => Ok(Self::HeartbeatFrag),
71            0x15 => Ok(Self::Data),
72            0x16 => Ok(Self::DataFrag),
73            other => Err(WireError::UnknownSubmessageId { id: other }),
74        }
75    }
76}
77
78/// E-Flag-Bit-Position im Flag-Byte.
79pub const FLAG_E_LITTLE_ENDIAN: u8 = 0x01;
80
81/// Submessage-Header.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub struct SubmessageHeader {
84    /// ID der Submessage-Klasse.
85    pub submessage_id: SubmessageId,
86    /// Flag-Byte (Bit 0 = E = Little-Endian-Body; weitere Bits
87    /// submessage-spezifisch).
88    pub flags: u8,
89    /// Body-Laenge in Bytes. `0` hat Spezial-Bedeutung (siehe
90    /// Spec §8.3.4.2): Reader liest "bis Ende des Datagrams".
91    pub octets_to_next_header: u16,
92}
93
94impl SubmessageHeader {
95    /// Wire-Size: 4 Bytes.
96    pub const WIRE_SIZE: usize = 4;
97
98    /// `true`, wenn das E-Flag gesetzt ist (LE-Body).
99    #[must_use]
100    pub fn is_little_endian(self) -> bool {
101        (self.flags & FLAG_E_LITTLE_ENDIAN) != 0
102    }
103
104    /// LE-Encoder. `octets_to_next_header` wird mit der durch
105    /// `is_little_endian()` gegebenen Endianness geschrieben — der
106    /// Sub-Header selbst nutzt dieselbe Endianness wie sein Body
107    /// (Spec §8.3.4.1).
108    #[must_use]
109    pub fn to_bytes(self) -> [u8; 4] {
110        let mut out = [0u8; 4];
111        out[0] = self.submessage_id.as_u8();
112        out[1] = self.flags;
113        let len_bytes = if self.is_little_endian() {
114            self.octets_to_next_header.to_le_bytes()
115        } else {
116            self.octets_to_next_header.to_be_bytes()
117        };
118        out[2..].copy_from_slice(&len_bytes);
119        out
120    }
121
122    /// Decoded einen 4-Byte-Slice.
123    ///
124    /// # Errors
125    /// `UnexpectedEof`, `UnknownSubmessageId`.
126    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
127        if bytes.len() < Self::WIRE_SIZE {
128            return Err(WireError::UnexpectedEof {
129                needed: Self::WIRE_SIZE,
130                offset: 0,
131            });
132        }
133        let id = SubmessageId::from_u8(bytes[0])?;
134        let flags = bytes[1];
135        let mut len_bytes = [0u8; 2];
136        len_bytes.copy_from_slice(&bytes[2..4]);
137        let octets_to_next_header = if (flags & FLAG_E_LITTLE_ENDIAN) != 0 {
138            u16::from_le_bytes(len_bytes)
139        } else {
140            u16::from_be_bytes(len_bytes)
141        };
142        Ok(Self {
143            submessage_id: id,
144            flags,
145            octets_to_next_header,
146        })
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    #![allow(clippy::expect_used, clippy::unwrap_used)]
153    use super::*;
154
155    #[test]
156    fn submessage_id_data_is_0x15() {
157        assert_eq!(SubmessageId::Data.as_u8(), 0x15);
158    }
159
160    #[test]
161    fn submessage_id_heartbeat_is_0x07() {
162        assert_eq!(SubmessageId::Heartbeat.as_u8(), 0x07);
163    }
164
165    #[test]
166    fn submessage_id_acknack_is_0x06() {
167        assert_eq!(SubmessageId::AckNack.as_u8(), 0x06);
168    }
169
170    #[test]
171    fn submessage_id_gap_is_0x08() {
172        assert_eq!(SubmessageId::Gap.as_u8(), 0x08);
173    }
174
175    #[test]
176    fn submessage_id_roundtrip_for_known_ids() {
177        for id in [
178            SubmessageId::Pad,
179            SubmessageId::AckNack,
180            SubmessageId::Heartbeat,
181            SubmessageId::Gap,
182            SubmessageId::InfoTs,
183            SubmessageId::InfoSrc,
184            SubmessageId::InfoReplyIp4,
185            SubmessageId::InfoDst,
186            SubmessageId::InfoReply,
187            SubmessageId::NackFrag,
188            SubmessageId::HeartbeatFrag,
189            SubmessageId::Data,
190            SubmessageId::DataFrag,
191        ] {
192            assert_eq!(SubmessageId::from_u8(id.as_u8()).unwrap(), id);
193        }
194    }
195
196    #[test]
197    fn submessage_id_rejects_unknown_byte() {
198        let res = SubmessageId::from_u8(0xFE);
199        assert!(matches!(
200            res,
201            Err(WireError::UnknownSubmessageId { id: 0xFE })
202        ));
203    }
204
205    #[test]
206    fn submessage_header_layout_le() {
207        let h = SubmessageHeader {
208            submessage_id: SubmessageId::Data,
209            flags: FLAG_E_LITTLE_ENDIAN,
210            octets_to_next_header: 0x0102,
211        };
212        let bytes = h.to_bytes();
213        assert_eq!(bytes[0], 0x15); // Data
214        assert_eq!(bytes[1], 0x01); // E-flag
215        // octets_to_next_header LE: 0x0102 → [0x02, 0x01]
216        assert_eq!(bytes[2], 0x02);
217        assert_eq!(bytes[3], 0x01);
218    }
219
220    #[test]
221    fn submessage_header_layout_be() {
222        let h = SubmessageHeader {
223            submessage_id: SubmessageId::Heartbeat,
224            flags: 0, // E-flag not set → BE body
225            octets_to_next_header: 0x0102,
226        };
227        let bytes = h.to_bytes();
228        assert_eq!(bytes[0], 0x07);
229        assert_eq!(bytes[1], 0);
230        // BE: 0x0102 → [0x01, 0x02]
231        assert_eq!(bytes[2], 0x01);
232        assert_eq!(bytes[3], 0x02);
233    }
234
235    #[test]
236    fn submessage_header_is_little_endian_flag() {
237        let le = SubmessageHeader {
238            submessage_id: SubmessageId::Data,
239            flags: FLAG_E_LITTLE_ENDIAN,
240            octets_to_next_header: 0,
241        };
242        assert!(le.is_little_endian());
243        let be = SubmessageHeader {
244            submessage_id: SubmessageId::Data,
245            flags: 0,
246            octets_to_next_header: 0,
247        };
248        assert!(!be.is_little_endian());
249    }
250
251    #[test]
252    fn submessage_header_roundtrip_le() {
253        let h = SubmessageHeader {
254            submessage_id: SubmessageId::Data,
255            flags: FLAG_E_LITTLE_ENDIAN,
256            octets_to_next_header: 0xABCD,
257        };
258        let bytes = h.to_bytes();
259        assert_eq!(SubmessageHeader::from_bytes(&bytes).unwrap(), h);
260    }
261
262    #[test]
263    fn submessage_header_roundtrip_be() {
264        let h = SubmessageHeader {
265            submessage_id: SubmessageId::Heartbeat,
266            flags: 0,
267            octets_to_next_header: 0x1234,
268        };
269        let bytes = h.to_bytes();
270        assert_eq!(SubmessageHeader::from_bytes(&bytes).unwrap(), h);
271    }
272
273    #[test]
274    fn submessage_header_decode_rejects_truncated() {
275        let bytes = [0x15u8, 0x01]; // nur 2 Byte
276        let res = SubmessageHeader::from_bytes(&bytes);
277        assert!(matches!(
278            res,
279            Err(WireError::UnexpectedEof { needed: 4, .. })
280        ));
281    }
282
283    #[test]
284    fn submessage_header_decode_rejects_unknown_id() {
285        let bytes = [0xFEu8, 0x01, 0, 0];
286        let res = SubmessageHeader::from_bytes(&bytes);
287        assert!(matches!(
288            res,
289            Err(WireError::UnknownSubmessageId { id: 0xFE })
290        ));
291    }
292}