1use crate::error::WireError;
27use crate::wire_types::{GuidPrefix, ProtocolVersion, VendorId};
28
29pub const RTPS_MAGIC: [u8; 4] = [b'R', b'T', b'P', b'S'];
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct RtpsHeader {
35 pub protocol_version: ProtocolVersion,
37 pub vendor_id: VendorId,
39 pub guid_prefix: GuidPrefix,
41}
42
43impl RtpsHeader {
44 pub const WIRE_SIZE: usize = 20;
46
47 #[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 #[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 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]); }
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]; 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 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}