1use crate::{RuntimeError, RuntimeResult};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Ros2MessageHint {
12 pub type_name: String,
14 pub spatial_topic: String,
16}
17
18pub trait Ros2Adapter {
20 fn supported_types(&self) -> &[Ros2MessageHint];
22
23 fn negotiate(&self, ros_type: &str) -> Option<&Ros2MessageHint>;
25}
26
27#[derive(Clone, Debug, Default)]
29pub struct CatalogRos2Adapter {
30 hints: Vec<Ros2MessageHint>,
31}
32
33impl CatalogRos2Adapter {
34 #[must_use]
36 pub fn new(hints: Vec<Ros2MessageHint>) -> Self {
37 Self { hints }
38 }
39
40 #[must_use]
42 pub fn point_cloud2_xyz() -> Self {
43 Self::new(vec![Ros2MessageHint {
44 type_name: POINT_CLOUD2_TYPE.into(),
45 spatial_topic: "point/xyz".into(),
46 }])
47 }
48}
49
50impl Ros2Adapter for CatalogRos2Adapter {
51 fn supported_types(&self) -> &[Ros2MessageHint] {
52 &self.hints
53 }
54
55 fn negotiate(&self, ros_type: &str) -> Option<&Ros2MessageHint> {
56 self.hints.iter().find(|hint| hint.type_name == ros_type)
57 }
58}
59
60pub const POINT_CLOUD2_TYPE: &str = "sensor_msgs/msg/PointCloud2";
62pub const TF_MESSAGE_TYPE: &str = "tf2_msgs/msg/TFMessage";
64
65const CDR_LE_ENCAP: [u8; 4] = [0x00, 0x01, 0x00, 0x00];
67
68#[derive(Clone, Debug, PartialEq)]
70pub struct PointCloud2Xyz {
71 pub frame_id: String,
73 pub stamp_sec: i32,
75 pub stamp_nanosec: u32,
77 pub xyz: Vec<f32>,
79 pub intensity: Option<Vec<f32>>,
81}
82
83#[derive(Clone, Debug, PartialEq)]
85pub struct TfTransform {
86 pub stamp_sec: i32,
88 pub stamp_nanosec: u32,
90 pub frame_id: String,
92 pub child_frame_id: String,
94 pub translation: [f64; 3],
96 pub rotation_xyzw: [f64; 4],
98}
99
100impl PointCloud2Xyz {
101 pub fn try_new(
103 frame_id: impl Into<String>,
104 stamp_sec: i32,
105 stamp_nanosec: u32,
106 xyz: Vec<f32>,
107 ) -> RuntimeResult<Self> {
108 if xyz.len() % 3 != 0 {
109 return Err(RuntimeError::InvalidConfiguration(
110 "xyz length must be a multiple of 3".into(),
111 ));
112 }
113 Ok(Self { frame_id: frame_id.into(), stamp_sec, stamp_nanosec, xyz, intensity: None })
114 }
115
116 pub fn try_new_with_intensity(
118 frame_id: impl Into<String>,
119 stamp_sec: i32,
120 stamp_nanosec: u32,
121 xyz: Vec<f32>,
122 intensity: Vec<f32>,
123 ) -> RuntimeResult<Self> {
124 Self::try_new_with_optional_intensity(
125 frame_id,
126 stamp_sec,
127 stamp_nanosec,
128 xyz,
129 Some(intensity),
130 )
131 }
132
133 fn try_new_with_optional_intensity(
134 frame_id: impl Into<String>,
135 stamp_sec: i32,
136 stamp_nanosec: u32,
137 xyz: Vec<f32>,
138 intensity: Option<Vec<f32>>,
139 ) -> RuntimeResult<Self> {
140 if xyz.len() % 3 != 0 {
141 return Err(RuntimeError::InvalidConfiguration(
142 "xyz length must be a multiple of 3".into(),
143 ));
144 }
145 if intensity.as_ref().is_some_and(|values| values.len() != xyz.len() / 3) {
146 return Err(RuntimeError::InvalidConfiguration(
147 "intensity length must match the XYZ point count".into(),
148 ));
149 }
150 Ok(Self { frame_id: frame_id.into(), stamp_sec, stamp_nanosec, xyz, intensity })
151 }
152
153 #[must_use]
155 pub fn point_count(&self) -> usize {
156 self.xyz.len() / 3
157 }
158}
159
160pub fn encode_point_cloud2_xyz(msg: &PointCloud2Xyz) -> RuntimeResult<Vec<u8>> {
162 let mut w = CdrWriter::new();
163 w.write_encap();
164 w.write_i32(msg.stamp_sec);
165 w.write_u32(msg.stamp_nanosec);
166 w.write_string(&msg.frame_id)?;
167 let width = msg.point_count() as u32;
168 w.write_u32(1); w.write_u32(width);
170 let intensity = msg.intensity.as_deref();
171 if intensity.is_some_and(|values| values.len() != msg.point_count()) {
172 return Err(RuntimeError::InvalidConfiguration(
173 "intensity length must match the XYZ point count".into(),
174 ));
175 }
176 w.write_u32(if intensity.is_some() { 4 } else { 3 }); write_point_field(&mut w, "x", 0)?;
178 write_point_field(&mut w, "y", 4)?;
179 write_point_field(&mut w, "z", 8)?;
180 if intensity.is_some() {
181 write_point_field(&mut w, "intensity", 12)?;
182 }
183 w.write_bool(false); let point_step = if intensity.is_some() { 16 } else { 12 };
185 w.write_u32(point_step); w.write_u32(width.saturating_mul(point_step)); let mut data = Vec::with_capacity(msg.point_count() * point_step as usize);
188 for (index, point) in msg.xyz.chunks_exact(3).enumerate() {
189 for value in point {
190 data.extend_from_slice(&value.to_le_bytes());
191 }
192 if let Some(intensity) = intensity {
193 data.extend_from_slice(&intensity[index].to_le_bytes());
194 }
195 }
196 let data_len = u32::try_from(data.len())
197 .map_err(|_| RuntimeError::InvalidConfiguration("PointCloud2 data is too large".into()))?;
198 w.write_u32(data_len);
199 w.write_bytes(&data);
200 w.write_bool(true); Ok(w.into_bytes())
202}
203
204pub fn point_cloud2_has_intensity(bytes: &[u8]) -> RuntimeResult<bool> {
206 let mut r = CdrReader::new(bytes)?;
207 r.expect_encap()?;
208 let _stamp_sec = r.read_i32()?;
209 let _stamp_nanosec = r.read_u32()?;
210 let _frame_id = r.read_string()?;
211 let _height = r.read_u32()?;
212 let _width = r.read_u32()?;
213 let field_count = r.read_u32()?;
214 let mut has_intensity = false;
215 for _ in 0..field_count {
216 let name = r.read_string()?;
217 let _offset = r.read_u32()?;
218 let datatype = r.read_u8()?;
219 r.align(4);
220 let count = r.read_u32()?;
221 if name.eq_ignore_ascii_case("intensity") {
222 if count != 1 || datatype != 7 {
223 return Err(RuntimeError::InvalidConfiguration(
224 "PointCloud2 intensity must be a scalar float32 field".into(),
225 ));
226 }
227 has_intensity = true;
228 }
229 }
230 Ok(has_intensity)
231}
232
233pub fn decode_tf_message(bytes: &[u8]) -> RuntimeResult<Vec<TfTransform>> {
240 let mut reader = CdrReader::new(bytes)?;
241 reader.expect_encap()?;
242 let count = reader.read_u32()? as usize;
243 let minimum_bytes_per_transform = 8_usize;
244 if count > bytes.len().saturating_sub(8) / minimum_bytes_per_transform {
245 return Err(RuntimeError::InvalidConfiguration(
246 "TFMessage transform count exceeds the payload bound".into(),
247 ));
248 }
249 let mut transforms = Vec::with_capacity(count);
250 for _ in 0..count {
251 let stamp_sec = reader.read_i32()?;
252 let stamp_nanosec = reader.read_u32()?;
253 if stamp_nanosec >= 1_000_000_000 {
254 return Err(RuntimeError::InvalidConfiguration(
255 "TF transform nanosecond field is outside [0, 1_000_000_000)".into(),
256 ));
257 }
258 let frame_id = reader.read_string()?;
259 let child_frame_id = reader.read_string()?;
260 if frame_id.is_empty() || child_frame_id.is_empty() {
261 return Err(RuntimeError::InvalidConfiguration(
262 "TF transform frame identifiers must not be empty".into(),
263 ));
264 }
265 let translation =
266 [reader.read_f64_from(4)?, reader.read_f64_from(4)?, reader.read_f64_from(4)?];
267 let rotation_xyzw = [
268 reader.read_f64_from(4)?,
269 reader.read_f64_from(4)?,
270 reader.read_f64_from(4)?,
271 reader.read_f64_from(4)?,
272 ];
273 transforms.push(TfTransform {
274 stamp_sec,
275 stamp_nanosec,
276 frame_id,
277 child_frame_id,
278 translation,
279 rotation_xyzw,
280 });
281 }
282 reader.require_end()?;
283 Ok(transforms)
284}
285
286pub fn decode_point_cloud2_xyz(bytes: &[u8]) -> RuntimeResult<PointCloud2Xyz> {
288 let mut r = CdrReader::new(bytes)?;
289 r.expect_encap()?;
290 let stamp_sec = r.read_i32()?;
291 let stamp_nanosec = r.read_u32()?;
292 let frame_id = r.read_string()?;
293 let height = r.read_u32()?;
294 let width = r.read_u32()?;
295 let field_count = r.read_u32()?;
296 let mut x_offset = None;
297 let mut y_offset = None;
298 let mut z_offset = None;
299 let mut intensity_offset = None;
300 for _ in 0..field_count {
301 let name = r.read_string()?;
302 let offset = r.read_u32()?;
303 let datatype = r.read_u8()?;
304 r.align(4);
305 let count = r.read_u32()?;
306 match name.to_ascii_lowercase().as_str() {
307 "x" if count == 1 && datatype == 7 => x_offset = Some(offset),
308 "y" if count == 1 && datatype == 7 => y_offset = Some(offset),
309 "z" if count == 1 && datatype == 7 => z_offset = Some(offset),
310 "intensity" => {
311 if count != 1 || datatype != 7 {
312 return Err(RuntimeError::InvalidConfiguration(
313 "PointCloud2 intensity must be a scalar float32 field".into(),
314 ));
315 }
316 intensity_offset = Some(offset);
317 }
318 _ => {}
319 }
320 }
321 let is_bigendian = r.read_bool()?;
322 let point_step = r.read_u32()?;
323 let row_step = r.read_u32()?;
324 let data_len = r.read_u32()? as usize;
325 let data = r.read_bytes(data_len)?;
326 let _is_dense = r.read_bool()?;
327 if height == 0 || width == 0 {
328 return PointCloud2Xyz::try_new_with_optional_intensity(
329 frame_id,
330 stamp_sec,
331 stamp_nanosec,
332 Vec::new(),
333 intensity_offset.map(|_| Vec::new()),
334 );
335 }
336 let x_offset = x_offset.ok_or_else(|| {
337 RuntimeError::InvalidConfiguration("PointCloud2 is missing float32 x field".into())
338 })?;
339 let y_offset = y_offset.ok_or_else(|| {
340 RuntimeError::InvalidConfiguration("PointCloud2 is missing float32 y field".into())
341 })?;
342 let z_offset = z_offset.ok_or_else(|| {
343 RuntimeError::InvalidConfiguration("PointCloud2 is missing float32 z field".into())
344 })?;
345 let last_offset = [x_offset, y_offset, z_offset]
346 .into_iter()
347 .chain(intensity_offset)
348 .max()
349 .expect("XYZ offsets are present");
350 if u64::from(point_step) < u64::from(last_offset) + 4 {
351 return Err(RuntimeError::InvalidConfiguration(
352 "PointCloud2 point_step does not contain the declared fields".into(),
353 ));
354 }
355 let height = height as usize;
356 let width = width as usize;
357 let point_step = point_step as usize;
358 let row_step = row_step as usize;
359 let row_bytes = width.checked_mul(point_step).ok_or_else(|| {
360 RuntimeError::InvalidConfiguration("PointCloud2 row size overflow".into())
361 })?;
362 if row_step < row_bytes {
363 return Err(RuntimeError::InvalidConfiguration(
364 "PointCloud2 row_step is shorter than one row".into(),
365 ));
366 }
367 let required_bytes = row_step.checked_mul(height).ok_or_else(|| {
368 RuntimeError::InvalidConfiguration("PointCloud2 data size overflow".into())
369 })?;
370 if required_bytes > data.len() {
371 return Err(RuntimeError::InvalidConfiguration(
372 "PointCloud2 data is shorter than row_step × height".into(),
373 ));
374 }
375 let points = height.checked_mul(width).ok_or_else(|| {
376 RuntimeError::InvalidConfiguration("PointCloud2 point count overflow".into())
377 })?;
378 let xyz_capacity = points.checked_mul(3).ok_or_else(|| {
379 RuntimeError::InvalidConfiguration("PointCloud2 XYZ capacity overflow".into())
380 })?;
381 let mut xyz = Vec::with_capacity(xyz_capacity);
382 let mut intensity = intensity_offset.map(|_| Vec::with_capacity(points));
383 for row in 0..height {
384 for column in 0..width {
385 let base = row
386 .checked_mul(row_step)
387 .and_then(|value| value.checked_add(column.checked_mul(point_step)?))
388 .ok_or_else(|| {
389 RuntimeError::InvalidConfiguration("PointCloud2 point offset overflow".into())
390 })?;
391 for offset in [x_offset, y_offset, z_offset] {
392 xyz.push(read_point_f32(data, base, offset, is_bigendian)?);
393 }
394 if let (Some(intensity), Some(offset)) = (&mut intensity, intensity_offset) {
395 intensity.push(read_point_f32(data, base, offset, is_bigendian)?);
396 }
397 }
398 }
399 PointCloud2Xyz::try_new_with_optional_intensity(
400 frame_id,
401 stamp_sec,
402 stamp_nanosec,
403 xyz,
404 intensity,
405 )
406}
407
408fn read_point_f32(data: &[u8], base: usize, offset: u32, is_bigendian: bool) -> RuntimeResult<f32> {
409 let start = base.checked_add(offset as usize).ok_or_else(|| {
410 RuntimeError::InvalidConfiguration("PointCloud2 field offset overflow".into())
411 })?;
412 let end = start.checked_add(4).ok_or_else(|| {
413 RuntimeError::InvalidConfiguration("PointCloud2 field end overflow".into())
414 })?;
415 if end > data.len() {
416 return Err(RuntimeError::InvalidConfiguration(
417 "PointCloud2 data shorter than field layout".into(),
418 ));
419 }
420 let bytes = data[start..end].try_into().unwrap();
421 Ok(if is_bigendian { f32::from_be_bytes(bytes) } else { f32::from_le_bytes(bytes) })
422}
423
424fn write_point_field(w: &mut CdrWriter, name: &str, offset: u32) -> RuntimeResult<()> {
425 w.write_string(name)?;
426 w.write_u32(offset);
427 w.write_u8(7); w.align(4);
429 w.write_u32(1);
430 Ok(())
431}
432
433#[derive(Clone, Debug, Default)]
435pub struct LoopbackRos2Node {
436 topics: std::collections::BTreeMap<String, Vec<u8>>,
437}
438
439impl LoopbackRos2Node {
440 #[must_use]
442 pub fn new() -> Self {
443 Self::default()
444 }
445
446 pub fn publish(&mut self, topic: impl Into<String>, payload: Vec<u8>) {
448 self.topics.insert(topic.into(), payload);
449 }
450
451 pub fn take(&mut self, topic: &str) -> Option<Vec<u8>> {
453 self.topics.remove(topic)
454 }
455
456 #[must_use]
458 pub fn has_topic(&self, topic: &str) -> bool {
459 self.topics.contains_key(topic)
460 }
461}
462
463struct CdrWriter {
464 buf: Vec<u8>,
465}
466
467impl CdrWriter {
468 fn new() -> Self {
469 Self { buf: Vec::new() }
470 }
471
472 fn into_bytes(self) -> Vec<u8> {
473 self.buf
474 }
475
476 fn write_encap(&mut self) {
477 self.buf.extend_from_slice(&CDR_LE_ENCAP);
478 }
479
480 fn align(&mut self, n: usize) {
481 while self.buf.len() % n != 0 {
482 self.buf.push(0);
483 }
484 }
485
486 #[cfg(test)]
487 fn align_from(&mut self, n: usize, origin: usize) {
488 let relative = self.buf.len().saturating_sub(origin);
489 let rem = relative % n;
490 if rem != 0 {
491 self.buf.resize(self.buf.len() + n - rem, 0);
492 }
493 }
494
495 fn write_u8(&mut self, v: u8) {
496 self.buf.push(v);
497 }
498
499 fn write_bool(&mut self, v: bool) {
500 self.align(1);
501 self.buf.push(u8::from(v));
502 }
503
504 fn write_i32(&mut self, v: i32) {
505 self.align(4);
506 self.buf.extend_from_slice(&v.to_le_bytes());
507 }
508
509 fn write_u32(&mut self, v: u32) {
510 self.align(4);
511 self.buf.extend_from_slice(&v.to_le_bytes());
512 }
513
514 #[cfg(test)]
515 fn write_f64(&mut self, v: f64) {
516 self.buf.extend_from_slice(&v.to_le_bytes());
517 }
518
519 fn write_bytes(&mut self, bytes: &[u8]) {
520 self.buf.extend_from_slice(bytes);
521 }
522
523 fn write_string(&mut self, value: &str) -> RuntimeResult<()> {
524 if value.len() >= u32::MAX as usize {
525 return Err(RuntimeError::InvalidConfiguration("string too long".into()));
526 }
527 self.align(4);
528 let len = (value.len() + 1) as u32;
530 self.write_u32(len);
531 self.buf.extend_from_slice(value.as_bytes());
532 self.buf.push(0);
533 Ok(())
534 }
535}
536
537struct CdrReader<'a> {
538 buf: &'a [u8],
539 pos: usize,
540}
541
542impl<'a> CdrReader<'a> {
543 fn new(buf: &'a [u8]) -> RuntimeResult<Self> {
544 if buf.len() < 4 {
545 return Err(RuntimeError::InvalidConfiguration("CDR buffer too short".into()));
546 }
547 Ok(Self { buf, pos: 0 })
548 }
549
550 fn expect_encap(&mut self) -> RuntimeResult<()> {
551 if self.buf.len() < 4 || self.buf[..4] != CDR_LE_ENCAP {
552 return Err(RuntimeError::InvalidConfiguration(
553 "expected ROS 2 CDR little-endian encapsulation".into(),
554 ));
555 }
556 self.pos = 4;
557 Ok(())
558 }
559
560 fn align(&mut self, n: usize) {
561 let rem = self.pos % n;
562 if rem != 0 {
563 self.pos += n - rem;
564 }
565 }
566
567 fn align_from(&mut self, n: usize, origin: usize) {
568 let relative = self.pos.saturating_sub(origin);
569 let rem = relative % n;
570 if rem != 0 {
571 self.pos += n - rem;
572 }
573 }
574
575 fn read_u8(&mut self) -> RuntimeResult<u8> {
576 let v = *self
577 .buf
578 .get(self.pos)
579 .ok_or_else(|| RuntimeError::InvalidConfiguration("CDR truncated".into()))?;
580 self.pos += 1;
581 Ok(v)
582 }
583
584 fn read_bool(&mut self) -> RuntimeResult<bool> {
585 Ok(self.read_u8()? != 0)
586 }
587
588 fn read_i32(&mut self) -> RuntimeResult<i32> {
589 self.align(4);
590 let bytes = self.read_exact(4)?;
591 Ok(i32::from_le_bytes(bytes.try_into().unwrap()))
592 }
593
594 fn read_u32(&mut self) -> RuntimeResult<u32> {
595 self.align(4);
596 let bytes = self.read_exact(4)?;
597 Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
598 }
599
600 fn read_f64_from(&mut self, origin: usize) -> RuntimeResult<f64> {
601 self.align_from(8, origin);
602 let bytes = self.read_exact(8)?;
603 Ok(f64::from_le_bytes(bytes.try_into().unwrap()))
604 }
605
606 fn read_bytes(&mut self, len: usize) -> RuntimeResult<&'a [u8]> {
607 let end = self
608 .pos
609 .checked_add(len)
610 .ok_or_else(|| RuntimeError::InvalidConfiguration("CDR overflow".into()))?;
611 if end > self.buf.len() {
612 return Err(RuntimeError::InvalidConfiguration("CDR truncated".into()));
613 }
614 let slice = &self.buf[self.pos..end];
615 self.pos = end;
616 Ok(slice)
617 }
618
619 fn read_exact(&mut self, len: usize) -> RuntimeResult<&'a [u8]> {
620 self.read_bytes(len)
621 }
622
623 fn read_string(&mut self) -> RuntimeResult<String> {
624 let len = self.read_u32()? as usize;
625 if len == 0 {
626 return Err(RuntimeError::InvalidConfiguration(
627 "CDR string length must include NUL".into(),
628 ));
629 }
630 let bytes = self.read_bytes(len)?;
631 if bytes.last().copied() != Some(0) {
632 return Err(RuntimeError::InvalidConfiguration(
633 "CDR string missing NUL terminator".into(),
634 ));
635 }
636 String::from_utf8(bytes[..len - 1].to_vec())
637 .map_err(|_| RuntimeError::InvalidConfiguration("CDR string is not UTF-8".into()))
638 }
639
640 fn require_end(&self) -> RuntimeResult<()> {
641 if self.pos != self.buf.len() {
642 return Err(RuntimeError::InvalidConfiguration(format!(
643 "CDR payload has {} trailing bytes",
644 self.buf.len().saturating_sub(self.pos)
645 )));
646 }
647 Ok(())
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::{
654 decode_point_cloud2_xyz, decode_tf_message, encode_point_cloud2_xyz,
655 point_cloud2_has_intensity, write_point_field, CatalogRos2Adapter, CdrWriter,
656 LoopbackRos2Node, PointCloud2Xyz, Ros2Adapter, TfTransform, POINT_CLOUD2_TYPE,
657 };
658
659 #[test]
660 fn negotiates_point_cloud2() {
661 let adapter = CatalogRos2Adapter::point_cloud2_xyz();
662 assert_eq!(adapter.negotiate(POINT_CLOUD2_TYPE).unwrap().spatial_topic, "point/xyz");
663 }
664
665 #[test]
666 fn roundtrips_xyz_cdr_and_loopback() {
667 let msg =
668 PointCloud2Xyz::try_new("lidar", 1, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
669 let bytes = encode_point_cloud2_xyz(&msg).unwrap();
670 assert!(!point_cloud2_has_intensity(&bytes).unwrap());
671 let mut node = LoopbackRos2Node::new();
672 node.publish("/points", bytes.clone());
673 let taken = node.take("/points").unwrap();
674 let decoded = decode_point_cloud2_xyz(&taken).unwrap();
675 assert_eq!(decoded, msg);
676 }
677
678 #[test]
679 fn roundtrips_xyzi_cdr_and_loopback() {
680 let msg = PointCloud2Xyz::try_new_with_intensity(
681 "lidar",
682 1,
683 2,
684 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
685 vec![10.0, 20.0],
686 )
687 .unwrap();
688 let bytes = encode_point_cloud2_xyz(&msg).unwrap();
689 assert!(point_cloud2_has_intensity(&bytes).unwrap());
690 let decoded = decode_point_cloud2_xyz(&bytes).unwrap();
691 assert_eq!(decoded, msg);
692 }
693
694 #[test]
695 fn decodes_xyz_offsets_and_row_padding() {
696 let mut writer = CdrWriter::new();
697 writer.write_encap();
698 writer.write_i32(3);
699 writer.write_u32(4);
700 writer.write_string("padded_lidar").unwrap();
701 writer.write_u32(2); writer.write_u32(2); writer.write_u32(4); write_point_field(&mut writer, "z", 8).unwrap();
705 write_point_field(&mut writer, "intensity", 12).unwrap();
706 write_point_field(&mut writer, "x", 0).unwrap();
707 write_point_field(&mut writer, "y", 4).unwrap();
708 writer.write_bool(false);
709 writer.write_u32(16); writer.write_u32(40); let mut data = Vec::new();
713 for row in 0..2 {
714 for column in 0..2 {
715 let base = (row * 2 + column) as f32;
716 for value in [base + 1.0, base + 2.0, base + 3.0, base + 4.0] {
717 data.extend_from_slice(&value.to_le_bytes());
718 }
719 }
720 data.extend_from_slice(&[0; 8]);
721 }
722 writer.write_u32(data.len() as u32);
723 writer.write_bytes(&data);
724 writer.write_bool(true);
725
726 let decoded = decode_point_cloud2_xyz(&writer.into_bytes()).unwrap();
727 assert_eq!(decoded.frame_id, "padded_lidar");
728 assert_eq!(decoded.stamp_sec, 3);
729 assert_eq!(decoded.stamp_nanosec, 4);
730 assert_eq!(decoded.xyz, vec![1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0, 4.0, 5.0, 6.0,]);
731 assert_eq!(decoded.intensity, Some(vec![4.0, 5.0, 6.0, 7.0]));
732 }
733
734 fn encode_tf_message(transforms: &[TfTransform]) -> Vec<u8> {
735 let mut writer = CdrWriter::new();
736 writer.write_encap();
737 writer.write_u32(transforms.len() as u32);
738 for transform in transforms {
739 writer.write_i32(transform.stamp_sec);
740 writer.write_u32(transform.stamp_nanosec);
741 writer.write_string(&transform.frame_id).unwrap();
742 writer.write_string(&transform.child_frame_id).unwrap();
743 writer.align_from(8, 4);
744 for value in transform.translation.into_iter().chain(transform.rotation_xyzw) {
745 writer.write_f64(value);
746 }
747 }
748 writer.into_bytes()
749 }
750
751 #[test]
752 fn decodes_tf_message_with_nested_float64_alignment() {
753 let expected = vec![TfTransform {
754 stamp_sec: 12,
755 stamp_nanosec: 34,
756 frame_id: "base_link".into(),
757 child_frame_id: "lidar_front".into(),
758 translation: [1.0, -2.0, 3.5],
759 rotation_xyzw: [0.0, 0.0, 0.707, 0.707],
760 }];
761 assert_eq!(decode_tf_message(&encode_tf_message(&expected)).unwrap(), expected);
762 }
763
764 #[test]
765 fn rejects_invalid_tf_message_timestamp_and_trailing_bytes() {
766 let transform = TfTransform {
767 stamp_sec: 0,
768 stamp_nanosec: 1_000_000_000,
769 frame_id: "base".into(),
770 child_frame_id: "sensor".into(),
771 translation: [0.0; 3],
772 rotation_xyzw: [0.0, 0.0, 0.0, 1.0],
773 };
774 let error = decode_tf_message(&encode_tf_message(&[transform])).unwrap_err();
775 assert!(error.to_string().contains("nanosecond"));
776
777 let valid = TfTransform {
778 stamp_sec: 0,
779 stamp_nanosec: 0,
780 frame_id: "base".into(),
781 child_frame_id: "sensor".into(),
782 translation: [0.0; 3],
783 rotation_xyzw: [0.0, 0.0, 0.0, 1.0],
784 };
785 let mut bytes = encode_tf_message(&[valid]);
786 bytes.push(1);
787 let error = decode_tf_message(&bytes).unwrap_err();
788 assert!(error.to_string().contains("trailing"));
789 }
790}