re_cdr 0.1.0

CDR (Common Data Representation) encoding and decoding for ROS 2 / DDS messages
Documentation
//! Bulk numeric array decoding.
//!
//! [`CdrReader::read_numeric_vec`] copies matching-endian arrays as bytes and reads other
//! arrays element by element.

use byteorder::{BigEndian, ByteOrder, LittleEndian};

use crate::error::Result;
use crate::primitives::CdrReader;

/// [`ByteOrder`] with known endianness.
pub trait CdrEndian: ByteOrder {
    /// `true` if this byte order is little-endian.
    const IS_LITTLE: bool;
}

impl CdrEndian for LittleEndian {
    const IS_LITTLE: bool = true;
}

impl CdrEndian for BigEndian {
    const IS_LITTLE: bool = false;
}

/// Numeric primitive supported by CDR.
///
/// # Soundness
///
/// The [`bytemuck::Pod`] bound permits byte-copy decoding when endianness matches.
pub trait CdrPod: bytemuck::Pod {
    /// CDR alignment of this type, in bytes (equal to its size for the primitives here).
    const CDR_ALIGN: usize;

    /// Read one value using the reader byte order.
    ///
    /// # Errors
    ///
    /// Errors on truncated input.
    fn read_one<BO: ByteOrder>(reader: &mut CdrReader<'_, BO>) -> Result<Self>;
}

macro_rules! impl_cdr_pod {
    ($ty:ty, $align:expr, $read:ident) => {
        impl CdrPod for $ty {
            const CDR_ALIGN: usize = $align;

            #[inline]
            fn read_one<BO: ByteOrder>(reader: &mut CdrReader<'_, BO>) -> Result<Self> {
                reader.$read()
            }
        }
    };
}

impl_cdr_pod!(i8, 1, read_i8);
impl_cdr_pod!(u8, 1, read_u8);
impl_cdr_pod!(i16, 2, read_i16);
impl_cdr_pod!(u16, 2, read_u16);
impl_cdr_pod!(i32, 4, read_i32);
impl_cdr_pod!(u32, 4, read_u32);
impl_cdr_pod!(i64, 8, read_i64);
impl_cdr_pod!(u64, 8, read_u64);
impl_cdr_pod!(f32, 4, read_f32);
impl_cdr_pod!(f64, 8, read_f64);

impl<BO: CdrEndian> CdrReader<'_, BO> {
    /// Read `count` numeric values.
    ///
    /// For CDR sequences, read the length prefix with [`CdrReader::read_sequence_length`]
    /// first.
    ///
    /// Uses byte-copy decoding for little-endian input on little-endian hosts.
    ///
    /// # Errors
    ///
    /// Errors on truncated input or byte-count overflow.
    #[inline]
    pub fn read_numeric_vec<T: CdrPod>(&mut self, count: usize) -> Result<Vec<T>> {
        if count == 0 {
            return Ok(Vec::new());
        }
        self.align(T::CDR_ALIGN)?;

        #[cfg(target_endian = "little")]
        {
            if BO::IS_LITTLE {
                let byte_count = count
                    .checked_mul(core::mem::size_of::<T>())
                    .ok_or(crate::error::Error::UnexpectedEof)?;
                let bytes = self.read_bytes(byte_count)?;
                return Ok(bytemuck::pod_collect_to_vec(bytes));
            }
        }

        let mut out = Vec::with_capacity(count);
        for _ in 0..count {
            out.push(T::read_one(self)?);
        }
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::{CdrReader, CdrWriter};
    use proptest::prelude::*;

    fn check_numeric_roundtrip<T, BO>(values: &[T], write: impl Fn(&mut CdrWriter<'_, BO>, T))
    where
        T: CdrPod + PartialEq + std::fmt::Debug + Copy,
        BO: CdrEndian,
    {
        let mut buf = Vec::new();
        {
            let mut w = CdrWriter::<BO>::new(&mut buf);
            for &v in values {
                write(&mut w, v);
            }
        }

        let mut bulk_reader = CdrReader::<BO>::new(&buf);
        let bulk = bulk_reader
            .read_numeric_vec::<T>(values.len())
            .expect("bulk decode failed");
        assert_eq!(bulk, values, "bulk decode mismatch");

        let mut elem_reader = CdrReader::<BO>::new(&buf);
        let mut elem = Vec::with_capacity(values.len());
        for _ in 0..values.len() {
            elem.push(T::read_one(&mut elem_reader).expect("element decode failed"));
        }
        assert_eq!(elem, values, "element decode mismatch");
        assert_eq!(
            bulk_reader.position(),
            elem_reader.position(),
            "consumed length differs"
        );
    }

    #[test]
    fn numeric_vec_f32_both_endian() {
        let v = vec![0.0f32, 1.5, -2.25, f32::MIN, f32::MAX, 1e30];
        check_numeric_roundtrip::<f32, LittleEndian>(&v, |w, x| w.write_f32(x));
        check_numeric_roundtrip::<f32, BigEndian>(&v, |w, x| w.write_f32(x));
    }

    #[test]
    fn numeric_vec_u32_both_endian() {
        let v = vec![0u32, 1, 2, 100, u32::MAX, 0xDEAD_BEEF];
        check_numeric_roundtrip::<u32, LittleEndian>(&v, |w, x| w.write_u32(x));
        check_numeric_roundtrip::<u32, BigEndian>(&v, |w, x| w.write_u32(x));
    }

    #[test]
    fn numeric_vec_i16_both_endian() {
        let v = vec![0i16, -1, 1, i16::MIN, i16::MAX, 12345];
        check_numeric_roundtrip::<i16, LittleEndian>(&v, |w, x| w.write_i16(x));
        check_numeric_roundtrip::<i16, BigEndian>(&v, |w, x| w.write_i16(x));
    }

    #[test]
    fn numeric_vec_empty_is_ok() {
        let buf: Vec<u8> = Vec::new();
        let mut reader = CdrReader::<LittleEndian>::new(&buf);
        let out = reader.read_numeric_vec::<f64>(0).unwrap();
        assert!(out.is_empty());
    }

    #[test]
    fn numeric_vec_respects_alignment() {
        let values = vec![10u32, 20, 30];
        let mut buf = Vec::new();
        {
            let mut w = CdrWriter::<LittleEndian>::new(&mut buf);
            w.write_u8(0xAB);
            for &v in &values {
                w.write_u32(v);
            }
        }
        assert_eq!(buf.len(), 16);

        let mut reader = CdrReader::<LittleEndian>::new(&buf);
        assert_eq!(reader.read_u8().unwrap(), 0xAB);
        let out = reader.read_numeric_vec::<u32>(values.len()).unwrap();
        assert_eq!(out, values);
        assert_eq!(reader.remaining(), 0);
    }

    #[test]
    fn cross_endian_values_match() {
        let v = vec![1.0f64, -2.0, 3.5, f64::MIN, f64::MAX];

        let mut le = Vec::new();
        {
            let mut w = CdrWriter::<LittleEndian>::new(&mut le);
            for &x in &v {
                w.write_f64(x);
            }
        }
        let mut be = Vec::new();
        {
            let mut w = CdrWriter::<BigEndian>::new(&mut be);
            for &x in &v {
                w.write_f64(x);
            }
        }

        let le_out = CdrReader::<LittleEndian>::new(&le)
            .read_numeric_vec::<f64>(v.len())
            .unwrap();
        let be_out = CdrReader::<BigEndian>::new(&be)
            .read_numeric_vec::<f64>(v.len())
            .unwrap();
        assert_eq!(le_out, v);
        assert_eq!(be_out, v);
        assert_eq!(le_out, be_out);
    }

    proptest! {
        #[test]
        fn prop_numeric_vec_f32_le(values in proptest::collection::vec(any::<u32>().prop_map(f32::from_bits), 0..256)) {
            let mut buf = Vec::new();
            {
                let mut w = CdrWriter::<LittleEndian>::new(&mut buf);
                for &x in &values { w.write_f32(x); }
            }
            let out = CdrReader::<LittleEndian>::new(&buf).read_numeric_vec::<f32>(values.len()).unwrap();
            let a: Vec<u32> = out.iter().map(|x| x.to_bits()).collect();
            let b: Vec<u32> = values.iter().map(|x| x.to_bits()).collect();
            prop_assert_eq!(a, b);
        }

        #[test]
        fn prop_numeric_vec_u64_be(values in proptest::collection::vec(any::<u64>(), 0..256)) {
            let mut buf = Vec::new();
            {
                let mut w = CdrWriter::<BigEndian>::new(&mut buf);
                for &x in &values { w.write_u64(x); }
            }
            let out = CdrReader::<BigEndian>::new(&buf).read_numeric_vec::<u64>(values.len()).unwrap();
            prop_assert_eq!(out, values);
        }
    }
}