Skip to main content

zerodds_cdr/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `CdrEncode` / `CdrDecode` traits + primitive implementations
4//! (W1.3).
5//!
6//! Trait-based instead of free functions, so that composite types in
7//! W2/W3 can recursively encode their children uniformly without large
8//! type switches.
9//!
10//! # Alignment convention
11//!
12//! - `u8`/`i8`/`bool`: 1
13//! - `u16`/`i16`: 2
14//! - `u32`/`i32`/`f32`/`char`: 4
15//! - `u64`/`i64`/`f64`: 8
16//!
17//! Char is encoded as XCDR2 `wchar32` (4 bytes) (OMG XTypes §7.4.7);
18//! that covers full Unicode. The XCDR1 `char` variant (1 byte ASCII)
19//! lives in the separate `xcdr1` module (see the crate header).
20
21use crate::buffer::BufferReader;
22use crate::error::DecodeError;
23
24#[cfg(feature = "alloc")]
25use crate::buffer::BufferWriter;
26#[cfg(feature = "alloc")]
27use crate::error::EncodeError;
28
29// ============================================================================
30// Traits
31// ============================================================================
32
33/// Value can be encoded into a [`BufferWriter`].
34#[cfg(feature = "alloc")]
35pub trait CdrEncode {
36    /// XCDR2 primitive classification (OMG XTypes 1.3 §7.4.3.5): `true`
37    /// only for primitives (bool, octet, int8/16/32/64, uint8/16/32/64,
38    /// float, double, char/wchar). `false` for aggregate/variable
39    /// types (string, struct, union, enum, sequence, array, map).
40    /// Controls DHEADER prepending for `sequence<T>`/`T[N]`:
41    /// collections with NON-primitive elements need a DHEADER (uint32 =
42    /// byte length of the following content) under XCDR2.
43    const IS_PRIMITIVE: bool = false;
44
45    /// Writes this value into the writer (alignment-aware).
46    ///
47    /// # Errors
48    /// [`EncodeError`].
49    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError>;
50}
51
52/// Stub trait for no-alloc builds: unusable, since [`BufferWriter`]
53/// only exists with the `alloc` feature. Will be provided with a
54/// slice-based writer in Phase 1.
55#[cfg(not(feature = "alloc"))]
56pub trait CdrEncode {}
57
58/// Value can be decoded from a [`BufferReader`].
59pub trait CdrDecode: Sized {
60    /// XCDR2 primitive classification, symmetric to
61    /// [`CdrEncode::IS_PRIMITIVE`]. Controls DHEADER reading when
62    /// decoding `sequence<T>`/`T[N]` with non-primitive `T`.
63    const IS_PRIMITIVE: bool = false;
64
65    /// Reads this value from the reader (alignment-aware).
66    ///
67    /// # Errors
68    /// [`DecodeError`].
69    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError>;
70}
71
72// ============================================================================
73// Primitive Encoder/Decoder
74// ============================================================================
75
76#[cfg(feature = "alloc")]
77impl CdrEncode for u8 {
78    const IS_PRIMITIVE: bool = true;
79    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
80        writer.write_u8(*self)
81    }
82}
83
84impl CdrDecode for u8 {
85    const IS_PRIMITIVE: bool = true;
86    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
87        reader.read_u8()
88    }
89}
90
91#[cfg(feature = "alloc")]
92impl CdrEncode for i8 {
93    const IS_PRIMITIVE: bool = true;
94    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
95        writer.write_u8(*self as u8)
96    }
97}
98
99impl CdrDecode for i8 {
100    const IS_PRIMITIVE: bool = true;
101    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
102        Ok(reader.read_u8()? as i8)
103    }
104}
105
106#[cfg(feature = "alloc")]
107impl CdrEncode for u16 {
108    const IS_PRIMITIVE: bool = true;
109    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
110        writer.write_u16(*self)
111    }
112}
113
114impl CdrDecode for u16 {
115    const IS_PRIMITIVE: bool = true;
116    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
117        reader.read_u16()
118    }
119}
120
121#[cfg(feature = "alloc")]
122impl CdrEncode for i16 {
123    const IS_PRIMITIVE: bool = true;
124    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
125        writer.write_u16(*self as u16)
126    }
127}
128
129impl CdrDecode for i16 {
130    const IS_PRIMITIVE: bool = true;
131    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
132        Ok(reader.read_u16()? as i16)
133    }
134}
135
136#[cfg(feature = "alloc")]
137impl CdrEncode for u32 {
138    const IS_PRIMITIVE: bool = true;
139    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
140        writer.write_u32(*self)
141    }
142}
143
144impl CdrDecode for u32 {
145    const IS_PRIMITIVE: bool = true;
146    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
147        reader.read_u32()
148    }
149}
150
151#[cfg(feature = "alloc")]
152impl CdrEncode for i32 {
153    const IS_PRIMITIVE: bool = true;
154    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
155        writer.write_u32(*self as u32)
156    }
157}
158
159impl CdrDecode for i32 {
160    const IS_PRIMITIVE: bool = true;
161    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
162        Ok(reader.read_u32()? as i32)
163    }
164}
165
166#[cfg(feature = "alloc")]
167impl CdrEncode for u64 {
168    const IS_PRIMITIVE: bool = true;
169    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
170        writer.write_u64(*self)
171    }
172}
173
174impl CdrDecode for u64 {
175    const IS_PRIMITIVE: bool = true;
176    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
177        reader.read_u64()
178    }
179}
180
181#[cfg(feature = "alloc")]
182impl CdrEncode for i64 {
183    const IS_PRIMITIVE: bool = true;
184    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
185        writer.write_u64(*self as u64)
186    }
187}
188
189impl CdrDecode for i64 {
190    const IS_PRIMITIVE: bool = true;
191    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
192        Ok(reader.read_u64()? as i64)
193    }
194}
195
196#[cfg(feature = "alloc")]
197impl CdrEncode for f32 {
198    const IS_PRIMITIVE: bool = true;
199    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
200        writer.write_u32(self.to_bits())
201    }
202}
203
204impl CdrDecode for f32 {
205    const IS_PRIMITIVE: bool = true;
206    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
207        Ok(Self::from_bits(reader.read_u32()?))
208    }
209}
210
211#[cfg(feature = "alloc")]
212impl CdrEncode for f64 {
213    const IS_PRIMITIVE: bool = true;
214    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
215        writer.write_u64(self.to_bits())
216    }
217}
218
219impl CdrDecode for f64 {
220    const IS_PRIMITIVE: bool = true;
221    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
222        Ok(Self::from_bits(reader.read_u64()?))
223    }
224}
225
226#[cfg(feature = "alloc")]
227impl CdrEncode for bool {
228    const IS_PRIMITIVE: bool = true;
229    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
230        writer.write_u8(u8::from(*self))
231    }
232}
233
234impl CdrDecode for bool {
235    const IS_PRIMITIVE: bool = true;
236    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
237        let offset = reader.position();
238        let byte = reader.read_u8()?;
239        match byte {
240            0 => Ok(false),
241            1 => Ok(true),
242            other => Err(DecodeError::InvalidBool {
243                value: other,
244                offset,
245            }),
246        }
247    }
248}
249
250#[cfg(feature = "alloc")]
251impl CdrEncode for char {
252    const IS_PRIMITIVE: bool = true;
253    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
254        // XCDR2 wchar32 (UTF-32 codepoint).
255        writer.write_u32(*self as u32)
256    }
257}
258
259impl CdrDecode for char {
260    const IS_PRIMITIVE: bool = true;
261    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
262        let offset = reader.position();
263        let value = reader.read_u32()?;
264        char::from_u32(value).ok_or(DecodeError::InvalidChar { value, offset })
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
271    use super::*;
272    use crate::Endianness;
273
274    #[cfg(feature = "alloc")]
275    fn roundtrip_alloc<T>(value: T)
276    where
277        T: CdrEncode + CdrDecode + PartialEq + core::fmt::Debug,
278    {
279        let mut w = BufferWriter::new(Endianness::Little);
280        value.encode(&mut w).expect("encode");
281        let bytes = w.into_bytes();
282        let mut r = BufferReader::new(&bytes, Endianness::Little);
283        let decoded = T::decode(&mut r).expect("decode");
284        assert_eq!(decoded, value);
285        assert_eq!(r.remaining(), 0);
286    }
287
288    #[cfg(feature = "alloc")]
289    fn roundtrip_be_alloc<T>(value: T)
290    where
291        T: CdrEncode + CdrDecode + PartialEq + core::fmt::Debug,
292    {
293        let mut w = BufferWriter::new(Endianness::Big);
294        value.encode(&mut w).expect("encode");
295        let bytes = w.into_bytes();
296        let mut r = BufferReader::new(&bytes, Endianness::Big);
297        let decoded = T::decode(&mut r).expect("decode");
298        assert_eq!(decoded, value);
299    }
300
301    #[cfg(feature = "alloc")]
302    #[test]
303    fn u8_roundtrip() {
304        roundtrip_alloc(0u8);
305        roundtrip_alloc(0xABu8);
306        roundtrip_alloc(0xFFu8);
307    }
308
309    #[cfg(feature = "alloc")]
310    #[test]
311    fn i8_roundtrip() {
312        roundtrip_alloc(0i8);
313        roundtrip_alloc(-1i8);
314        roundtrip_alloc(127i8);
315        roundtrip_alloc(-128i8);
316    }
317
318    #[cfg(feature = "alloc")]
319    #[test]
320    fn u16_roundtrip_le_and_be() {
321        roundtrip_alloc(0x1234u16);
322        roundtrip_be_alloc(0x1234u16);
323        roundtrip_alloc(u16::MAX);
324    }
325
326    #[cfg(feature = "alloc")]
327    #[test]
328    fn i16_roundtrip() {
329        roundtrip_alloc(-1i16);
330        roundtrip_alloc(i16::MIN);
331        roundtrip_alloc(i16::MAX);
332    }
333
334    #[cfg(feature = "alloc")]
335    #[test]
336    fn u32_roundtrip() {
337        roundtrip_alloc(0xDEAD_BEEFu32);
338        roundtrip_be_alloc(0xCAFE_BABEu32);
339    }
340
341    #[cfg(feature = "alloc")]
342    #[test]
343    fn i32_roundtrip() {
344        roundtrip_alloc(-1i32);
345        roundtrip_alloc(i32::MIN);
346        roundtrip_alloc(i32::MAX);
347    }
348
349    #[cfg(feature = "alloc")]
350    #[test]
351    fn u64_roundtrip() {
352        roundtrip_alloc(0x0102_0304_0506_0708u64);
353        roundtrip_be_alloc(0x0102_0304_0506_0708u64);
354    }
355
356    #[cfg(feature = "alloc")]
357    #[test]
358    fn i64_roundtrip() {
359        roundtrip_alloc(-1i64);
360        roundtrip_alloc(i64::MIN);
361        roundtrip_alloc(i64::MAX);
362    }
363
364    #[cfg(feature = "alloc")]
365    #[test]
366    fn f32_roundtrip_normal_values() {
367        roundtrip_alloc(0.0f32);
368        roundtrip_alloc(1.5f32);
369        roundtrip_alloc(-2.5f32);
370        roundtrip_alloc(f32::MIN_POSITIVE);
371    }
372
373    #[cfg(feature = "alloc")]
374    #[test]
375    fn f32_roundtrip_special() {
376        // NaN is not self-equal — use a to_bits comparison.
377        let mut w = BufferWriter::new(Endianness::Little);
378        f32::NAN.encode(&mut w).unwrap();
379        let bytes = w.into_bytes();
380        let mut r = BufferReader::new(&bytes, Endianness::Little);
381        let decoded = f32::decode(&mut r).unwrap();
382        assert!(decoded.is_nan());
383    }
384
385    #[cfg(feature = "alloc")]
386    #[test]
387    fn f64_roundtrip() {
388        roundtrip_alloc(0.0f64);
389        roundtrip_alloc(core::f64::consts::PI);
390        roundtrip_alloc(-1.0e-308f64);
391    }
392
393    #[cfg(feature = "alloc")]
394    #[test]
395    fn bool_roundtrip() {
396        roundtrip_alloc(true);
397        roundtrip_alloc(false);
398    }
399
400    #[test]
401    fn bool_decode_rejects_other_values() {
402        let bytes = [0xFFu8];
403        let mut r = BufferReader::new(&bytes, Endianness::Little);
404        let res = bool::decode(&mut r);
405        assert!(matches!(
406            res,
407            Err(DecodeError::InvalidBool {
408                value: 0xFF,
409                offset: 0
410            })
411        ));
412    }
413
414    #[cfg(feature = "alloc")]
415    #[test]
416    fn char_roundtrip_ascii_and_unicode() {
417        roundtrip_alloc('A');
418        roundtrip_alloc('z');
419        roundtrip_alloc('0');
420        roundtrip_alloc('🦀'); // Unicode code point above the BMP
421        roundtrip_alloc('ä');
422    }
423
424    #[test]
425    fn char_decode_rejects_surrogate() {
426        // 0xD800 is a surrogate-pair marker, not a valid codepoint.
427        let bytes = [0x00, 0xD8, 0x00, 0x00];
428        let mut r = BufferReader::new(&bytes, Endianness::Little);
429        let res = char::decode(&mut r);
430        assert!(matches!(
431            res,
432            Err(DecodeError::InvalidChar { value: 0xD800, .. })
433        ));
434    }
435
436    #[cfg(feature = "alloc")]
437    #[test]
438    fn primitives_align_correctly_when_mixed() {
439        // u8 + u16 + u32 + u64 -> expected 16 bytes (1 + 1pad + 2 + 4 + 8)
440        let mut w = BufferWriter::new(Endianness::Little);
441        1u8.encode(&mut w).unwrap();
442        2u16.encode(&mut w).unwrap();
443        3u32.encode(&mut w).unwrap();
444        4u64.encode(&mut w).unwrap();
445        assert_eq!(w.position(), 16);
446
447        let bytes = w.into_bytes();
448        let mut r = BufferReader::new(&bytes, Endianness::Little);
449        assert_eq!(u8::decode(&mut r).unwrap(), 1);
450        assert_eq!(u16::decode(&mut r).unwrap(), 2);
451        assert_eq!(u32::decode(&mut r).unwrap(), 3);
452        assert_eq!(u64::decode(&mut r).unwrap(), 4);
453    }
454}