1extern crate alloc;
31use alloc::rc::Rc;
32use alloc::vec::Vec;
33
34use crate::header::RtpsHeader;
35use crate::submessage_header::{FLAG_E_LITTLE_ENDIAN, SubmessageHeader, SubmessageId};
36use crate::wire_types::Locator;
37
38pub const DEFAULT_MTU: usize = 1472;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OutboundDatagram {
48 pub bytes: Vec<u8>,
50 pub targets: Rc<Vec<Locator>>,
52}
53
54impl OutboundDatagram {
55 #[must_use]
65 pub fn into_shared_bytes(self) -> alloc::sync::Arc<[u8]> {
66 alloc::sync::Arc::from(self.bytes.into_boxed_slice())
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum AddError {
73 WouldExceedMtu {
75 needed: usize,
77 remaining: usize,
79 },
80 BodyTooLarge,
82}
83
84#[derive(Debug)]
90pub struct MessageBuilder {
91 bytes: Vec<u8>,
92 targets: Rc<Vec<Locator>>,
93 mtu: usize,
94 submsg_count: usize,
95}
96
97impl MessageBuilder {
98 #[must_use]
103 pub fn open(header: RtpsHeader, targets: Rc<Vec<Locator>>, mtu: usize) -> Self {
104 assert!(
105 mtu >= 20,
106 "MTU must accommodate at least the 20-byte RTPS header"
107 );
108 let mut bytes = Vec::with_capacity(mtu);
109 bytes.extend_from_slice(&header.to_bytes());
110 Self {
111 bytes,
112 targets,
113 mtu,
114 submsg_count: 0,
115 }
116 }
117
118 #[must_use]
120 pub fn submsg_count(&self) -> usize {
121 self.submsg_count
122 }
123
124 #[must_use]
126 pub fn is_empty(&self) -> bool {
127 self.submsg_count == 0
128 }
129
130 #[must_use]
133 pub fn len(&self) -> usize {
134 self.bytes.len()
135 }
136
137 #[must_use]
139 pub fn remaining(&self) -> usize {
140 self.mtu.saturating_sub(self.bytes.len())
141 }
142
143 pub fn try_add_submessage(
155 &mut self,
156 id: SubmessageId,
157 flags: u8,
158 body: &[u8],
159 ) -> Result<(), AddError> {
160 let body_len = u16::try_from(body.len()).map_err(|_| AddError::BodyTooLarge)?;
172 let needed = SubmessageHeader::WIRE_SIZE + body.len();
173 if self.bytes.len() + needed > self.mtu {
174 return Err(AddError::WouldExceedMtu {
175 needed,
176 remaining: self.remaining(),
177 });
178 }
179 let sh = SubmessageHeader {
180 submessage_id: id,
181 flags: flags | FLAG_E_LITTLE_ENDIAN,
182 octets_to_next_header: body_len,
183 };
184 self.bytes.extend_from_slice(&sh.to_bytes());
185 self.bytes.extend_from_slice(body);
186 self.submsg_count += 1;
187 Ok(())
188 }
189
190 pub fn try_add_submessage_split(
203 &mut self,
204 id: SubmessageId,
205 flags: u8,
206 header_body: &[u8],
207 payload_tail: &[u8],
208 ) -> Result<(), AddError> {
209 let total_body = header_body.len() + payload_tail.len();
210 let body_len = u16::try_from(total_body).map_err(|_| AddError::BodyTooLarge)?;
211 let needed = SubmessageHeader::WIRE_SIZE + total_body;
212 if self.bytes.len() + needed > self.mtu {
213 return Err(AddError::WouldExceedMtu {
214 needed,
215 remaining: self.remaining(),
216 });
217 }
218 let sh = SubmessageHeader {
219 submessage_id: id,
220 flags: flags | FLAG_E_LITTLE_ENDIAN,
221 octets_to_next_header: body_len,
222 };
223 self.bytes.extend_from_slice(&sh.to_bytes());
224 self.bytes.extend_from_slice(header_body);
225 self.bytes.extend_from_slice(payload_tail);
226 self.submsg_count += 1;
227 Ok(())
228 }
229
230 #[must_use]
237 pub fn finish(self) -> Option<OutboundDatagram> {
238 if self.submsg_count == 0 {
239 return None;
240 }
241 Some(OutboundDatagram {
242 bytes: self.bytes,
243 targets: self.targets,
244 })
245 }
246}
247
248#[cfg(test)]
249#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
250mod tests {
251 use super::*;
252 use crate::datagram::{ParsedSubmessage, decode_datagram};
253 use crate::submessages::{DataSubmessage, HeartbeatSubmessage};
254 use crate::wire_types::{EntityId, GuidPrefix, Locator, SequenceNumber, VendorId};
255
256 fn sample_header() -> RtpsHeader {
257 RtpsHeader::new(VendorId::ZERODDS, GuidPrefix::from_bytes([1; 12]))
258 }
259
260 fn sample_data(sn: i64, payload_len: usize) -> DataSubmessage {
261 DataSubmessage {
262 extra_flags: 0,
263 reader_id: EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
264 writer_id: EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
265 writer_sn: SequenceNumber(sn),
266 inline_qos: None,
267 key_flag: false,
268 non_standard_flag: false,
269 serialized_payload: alloc::sync::Arc::from(alloc::vec![0xAB; payload_len]),
270 }
271 }
272
273 fn targets() -> Rc<Vec<Locator>> {
274 Rc::new(alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7400)])
275 }
276
277 #[test]
278 fn fresh_builder_contains_only_rtps_header() {
279 let b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
280 assert!(b.is_empty());
281 assert_eq!(b.len(), 20, "only RTPS header");
282 assert_eq!(b.submsg_count(), 0);
283 assert_eq!(b.remaining(), DEFAULT_MTU - 20);
284 }
285
286 #[test]
290 fn into_shared_bytes_preserves_wire_content_and_shares_via_arc() {
291 let mut b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
292 let (body, flags) = sample_data(7, 16).write_body(true);
293 b.try_add_submessage(SubmessageId::Data, flags, &body)
294 .unwrap();
295 let dg = b.finish().unwrap();
296 let bytes_copy = dg.bytes.clone();
297 let shared = dg.into_shared_bytes();
298 assert_eq!(&shared[..], &bytes_copy[..]);
300 let c1 = alloc::sync::Arc::clone(&shared);
302 let c2 = alloc::sync::Arc::clone(&shared);
303 assert_eq!(alloc::sync::Arc::strong_count(&shared), 3);
304 assert_eq!(&c1[..], &c2[..]);
305 }
306
307 #[test]
308 fn single_data_submessage_fits_and_decodes() {
309 let mut b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
310 let (body, flags) = sample_data(1, 10).write_body(true);
311 b.try_add_submessage(SubmessageId::Data, flags, &body)
312 .unwrap();
313 let dg = b.finish().unwrap();
314 assert_eq!(dg.targets.len(), 1);
315 let parsed = decode_datagram(&dg.bytes).unwrap();
316 assert_eq!(parsed.submessages.len(), 1);
317 assert!(matches!(&parsed.submessages[0], ParsedSubmessage::Data(_)));
318 }
319
320 #[test]
321 fn four_small_datas_aggregate_into_one_datagram() {
322 let mut b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
323 for sn in 1..=4i64 {
324 let (body, flags) = sample_data(sn, 10).write_body(true);
325 b.try_add_submessage(SubmessageId::Data, flags, &body)
326 .unwrap();
327 }
328 let dg = b.finish().unwrap();
329 let parsed = decode_datagram(&dg.bytes).unwrap();
330 let data_count = parsed
332 .submessages
333 .iter()
334 .filter(|s| matches!(s, ParsedSubmessage::Data(_)))
335 .count();
336 assert_eq!(data_count, 4);
337 }
338
339 #[test]
340 fn overflow_rejects_with_would_exceed_mtu() {
341 let mtu = 100; let mut b = MessageBuilder::open(sample_header(), targets(), mtu);
343 let (body, flags) = sample_data(1, 50).write_body(true);
345 b.try_add_submessage(SubmessageId::Data, flags, &body)
346 .unwrap();
347 let (body2, flags2) = sample_data(2, 50).write_body(true);
349 let res = b.try_add_submessage(SubmessageId::Data, flags2, &body2);
350 assert!(matches!(res, Err(AddError::WouldExceedMtu { .. })));
351 assert_eq!(b.submsg_count(), 1, "first add must still be counted");
352 }
353
354 #[test]
355 fn overflow_allows_caller_to_open_new_builder() {
356 let mtu = 100;
357 let (body, flags) = sample_data(1, 50).write_body(true);
358 let mut out: Vec<OutboundDatagram> = Vec::new();
359 let mut b = MessageBuilder::open(sample_header(), targets(), mtu);
360
361 for sn in 1..=3i64 {
362 let (body_n, flags_n) = sample_data(sn, 50).write_body(true);
363 if b.try_add_submessage(SubmessageId::Data, flags_n, &body_n)
364 .is_err()
365 {
366 out.push(b.finish().unwrap());
367 b = MessageBuilder::open(sample_header(), targets(), mtu);
368 b.try_add_submessage(SubmessageId::Data, flags_n, &body_n)
369 .unwrap();
370 }
371 }
372 if !b.is_empty() {
373 out.push(b.finish().unwrap());
374 }
375 let _ = flags;
376 let _ = body;
377 assert_eq!(out.len(), 3);
379 }
380
381 #[test]
382 fn finish_on_empty_builder_returns_none() {
383 let b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
384 assert!(b.finish().is_none());
385 }
386
387 #[test]
388 fn piggyback_heartbeat_after_data_aggregates() {
389 let mut b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
390 let (body, flags) = sample_data(1, 10).write_body(true);
391 b.try_add_submessage(SubmessageId::Data, flags, &body)
392 .unwrap();
393 let hb = HeartbeatSubmessage {
394 reader_id: EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
395 writer_id: EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
396 first_sn: SequenceNumber(1),
397 last_sn: SequenceNumber(1),
398 count: 1,
399 final_flag: true,
400 liveliness_flag: false,
401 group_info: None,
402 };
403 let (hb_body, hb_flags) = hb.write_body(true);
404 b.try_add_submessage(SubmessageId::Heartbeat, hb_flags, &hb_body)
405 .unwrap();
406 let dg = b.finish().unwrap();
407 let parsed = decode_datagram(&dg.bytes).unwrap();
408 assert_eq!(parsed.submessages.len(), 2);
409 assert!(matches!(&parsed.submessages[0], ParsedSubmessage::Data(_)));
410 assert!(matches!(
411 &parsed.submessages[1],
412 ParsedSubmessage::Heartbeat(h) if h.final_flag
413 ));
414 }
415
416 #[test]
417 fn builder_propagates_little_endian_flag_e() {
418 let mut b = MessageBuilder::open(sample_header(), targets(), DEFAULT_MTU);
421 let (body, _flags_from_write) = sample_data(1, 10).write_body(true);
422 b.try_add_submessage(SubmessageId::Data, 0, &body).unwrap();
424 let dg = b.finish().unwrap();
425 let sub_header_flags = dg.bytes[21]; assert_eq!(
428 sub_header_flags & FLAG_E_LITTLE_ENDIAN,
429 FLAG_E_LITTLE_ENDIAN
430 );
431 }
432
433 #[test]
434 #[should_panic(expected = "MTU must accommodate")]
435 fn open_panics_on_mtu_below_header() {
436 let _ = MessageBuilder::open(sample_header(), targets(), 10);
437 }
438
439 #[test]
440 fn body_too_large_rejected() {
441 let mut b = MessageBuilder::open(sample_header(), targets(), 100_000);
442 let oversize = alloc::vec![0u8; u16::MAX as usize + 1];
443 let res = b.try_add_submessage(SubmessageId::Data, 0, &oversize);
444 assert!(matches!(res, Err(AddError::BodyTooLarge)));
445 }
446}