Skip to main content

zerodds_rtps/
header.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! RTPS-Header (DDSI-RTPS 2.5 §8.3.3).
4//!
5//! Der RTPS-Header bildet das Outer-Envelope eines RTPS-Datagrams. Er
6//! ist 20 Byte lang (4 magic + 2 version + 2 vendor + 12 prefix), fest
7//! layoutiert, und nicht endianness-getagged (alle Felder sind Byte-
8//! Arrays).
9//!
10//! ```text
11//!   0                   1                   2                   3
12//!   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
13//!  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14//!  |   'R' = 0x52  |   'T' = 0x54  |   'P' = 0x50  |   'S' = 0x53  |
15//!  +---------------+---------------+---------------+---------------+
16//!  | major version | minor version |   vendor.major| vendor.minor  |
17//!  +---------------+---------------+---------------+---------------+
18//!  |                                                               |
19//!  +                                                               +
20//!  |                          guidPrefix                           |
21//!  +                                                               +
22//!  |                                                               |
23//!  +---------------+---------------+---------------+---------------+
24//! ```
25
26use crate::error::WireError;
27use crate::wire_types::{GuidPrefix, ProtocolVersion, VendorId};
28
29/// RTPS-Magic-Bytes "RTPS".
30pub const RTPS_MAGIC: [u8; 4] = [b'R', b'T', b'P', b'S'];
31
32/// RTPS-Header (20 Byte fix).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct RtpsHeader {
35    /// Protokoll-Version (default: 2.5).
36    pub protocol_version: ProtocolVersion,
37    /// Vendor-Identifier.
38    pub vendor_id: VendorId,
39    /// Participant-Prefix.
40    pub guid_prefix: GuidPrefix,
41}
42
43impl RtpsHeader {
44    /// Wire-Size: 20 Bytes (4 magic + 2 version + 2 vendor + 12 prefix).
45    pub const WIRE_SIZE: usize = 20;
46
47    /// Konstruktor mit Defaults fuer Version (2.5).
48    #[must_use]
49    pub fn new(vendor_id: VendorId, guid_prefix: GuidPrefix) -> Self {
50        Self {
51            protocol_version: ProtocolVersion::V2_5,
52            vendor_id,
53            guid_prefix,
54        }
55    }
56
57    /// Encoded den Header in einen 20-byte-Array.
58    #[must_use]
59    pub fn to_bytes(self) -> [u8; 20] {
60        let mut out = [0u8; 20];
61        out[..4].copy_from_slice(&RTPS_MAGIC);
62        out[4..6].copy_from_slice(&self.protocol_version.to_bytes());
63        out[6..8].copy_from_slice(&self.vendor_id.to_bytes());
64        out[8..20].copy_from_slice(&self.guid_prefix.to_bytes());
65        out
66    }
67
68    /// Decoded einen 20-Byte-Slice. Pruefte Magic-Bytes.
69    ///
70    /// # Errors
71    /// `InvalidMagic` bei falschem Prefix; `UnexpectedEof` bei zu kurzer
72    /// Eingabe.
73    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
74        if bytes.len() < Self::WIRE_SIZE {
75            return Err(WireError::UnexpectedEof {
76                needed: Self::WIRE_SIZE,
77                offset: 0,
78            });
79        }
80        let mut magic = [0u8; 4];
81        magic.copy_from_slice(&bytes[..4]);
82        if magic != RTPS_MAGIC {
83            return Err(WireError::InvalidMagic { found: magic });
84        }
85        let mut pv = [0u8; 2];
86        pv.copy_from_slice(&bytes[4..6]);
87        let mut vid = [0u8; 2];
88        vid.copy_from_slice(&bytes[6..8]);
89        let mut gp = [0u8; 12];
90        gp.copy_from_slice(&bytes[8..20]);
91        Ok(Self {
92            protocol_version: ProtocolVersion::from_bytes(pv),
93            vendor_id: VendorId::from_bytes(vid),
94            guid_prefix: GuidPrefix::from_bytes(gp),
95        })
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    #![allow(clippy::expect_used, clippy::unwrap_used)]
102    use super::*;
103
104    #[test]
105    fn header_layout_first_four_bytes_are_rtps_magic() {
106        let h = RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::UNKNOWN);
107        let bytes = h.to_bytes();
108        assert_eq!(&bytes[..4], &RTPS_MAGIC);
109        assert_eq!(&bytes[..4], b"RTPS");
110    }
111
112    #[test]
113    fn header_protocol_version_at_bytes_4_5() {
114        let h = RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::UNKNOWN);
115        let bytes = h.to_bytes();
116        assert_eq!(&bytes[4..6], &[2, 5]); // 2.5
117    }
118
119    #[test]
120    fn header_vendor_id_at_bytes_6_7() {
121        let h = RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::UNKNOWN);
122        let bytes = h.to_bytes();
123        assert_eq!(&bytes[6..8], &[0x01, 0xF0]);
124    }
125
126    #[test]
127    fn header_guid_prefix_at_bytes_8_to_19() {
128        let prefix = GuidPrefix::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
129        let h = RtpsHeader::new(VendorId::ZERODDS, prefix);
130        let bytes = h.to_bytes();
131        assert_eq!(&bytes[8..20], &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
132    }
133
134    #[test]
135    fn header_total_size_is_20_bytes() {
136        let h = RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::UNKNOWN);
137        assert_eq!(h.to_bytes().len(), 20);
138        assert_eq!(RtpsHeader::WIRE_SIZE, 20);
139    }
140
141    #[test]
142    fn header_roundtrip() {
143        let h = RtpsHeader::new(VendorId([0xAB, 0xCD]), GuidPrefix::from_bytes([42; 12]));
144        let bytes = h.to_bytes();
145        let decoded = RtpsHeader::from_bytes(&bytes).unwrap();
146        assert_eq!(decoded, h);
147    }
148
149    #[test]
150    fn header_decode_rejects_invalid_magic() {
151        let mut bytes = [0u8; 20];
152        bytes[..4].copy_from_slice(b"XXXX");
153        let res = RtpsHeader::from_bytes(&bytes);
154        assert!(matches!(
155            res,
156            Err(WireError::InvalidMagic { found }) if &found == b"XXXX"
157        ));
158    }
159
160    #[test]
161    fn header_decode_rejects_truncated_input() {
162        let bytes = [b'R', b'T', b'P', b'S', 2, 5, 0, 0]; // nur 8 Byte
163        let res = RtpsHeader::from_bytes(&bytes);
164        assert!(matches!(
165            res,
166            Err(WireError::UnexpectedEof { needed: 20, .. })
167        ));
168    }
169
170    #[test]
171    fn header_decode_accepts_extra_trailing_bytes() {
172        // Decoder konsumiert nur 20 Byte; trailing-Bytes (z.B. erste
173        // Submessage) ueberlebt die Eingabe.
174        let h = RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::UNKNOWN);
175        let mut bytes = [0u8; 36];
176        bytes[..20].copy_from_slice(&h.to_bytes());
177        bytes[20..].copy_from_slice(&[0xAB; 16]);
178        let decoded = RtpsHeader::from_bytes(&bytes).unwrap();
179        assert_eq!(decoded, h);
180    }
181}