Skip to main content

nros_serdes/
cdr.rs

1//! CDR encoder/decoder with alignment handling
2
3use crate::{
4    CDR_LE_HEADER, CDR2_DELIMITED_LE_HEADER,
5    error::{DeserError, SerError},
6};
7
8/// CDR writer for serialization.
9///
10/// Handles alignment and endianness for ROS 2 CDR encoding.
11/// Alignment is computed relative to `origin` — when a 4-byte CDR
12/// header is present, `origin = 4` so that fields align correctly
13/// within the payload portion of the buffer.
14/// CDR encoding version. XCDR1 is the historical default (PLAIN_CDR, no
15/// DHEADER, 8-byte primitives align to 8). XCDR2 (phase-303 W2 / RFC-0055 /
16/// #0267) is DELIMITED_CDR for APPENDABLE types: every struct is wrapped in a
17/// 4-byte DHEADER and 8-byte primitives align to 4.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum EncodingVersion {
20    /// PLAIN_CDR (encapsulation `0x0001`). The default; byte-identical to every
21    /// pre-W2 stream.
22    #[default]
23    Xcdr1,
24    /// DELIMITED_CDR2 (encapsulation `0x0009`) — appendable + DHEADER.
25    Xcdr2,
26}
27
28/// Opaque marker returned by [`CdrWriter::begin_dheader`], passed back to
29/// [`CdrWriter::end_dheader`]. Under XCDR1 it carries nothing (the DHEADER calls
30/// are no-ops); under XCDR2 it holds the reserved DHEADER position.
31#[derive(Debug, Clone, Copy)]
32pub struct DHeaderMark(Option<usize>);
33
34impl DHeaderMark {
35    /// The reserved DHEADER byte offset (XCDR2), or `None` under XCDR1. Lets the
36    /// C FFI carry the mark across the `extern "C"` boundary (phase-303 W4).
37    #[inline]
38    pub fn raw(self) -> Option<usize> {
39        self.0
40    }
41
42    /// Reconstruct a mark from its [`raw`](Self::raw) offset (FFI round-trip).
43    #[inline]
44    pub fn from_raw(at: Option<usize>) -> Self {
45        DHeaderMark(at)
46    }
47}
48
49/// Opaque scope returned by [`CdrReader::begin_dheader`], passed back to
50/// [`CdrReader::end_dheader`]. Under XCDR1 it carries nothing (no-op); under
51/// XCDR2 it holds the absolute buffer offset of the delimited struct's end.
52#[derive(Debug, Clone, Copy)]
53pub struct DHeaderScope(Option<usize>);
54
55impl DHeaderScope {
56    /// The delimited struct's end offset (XCDR2), or `None` under XCDR1 (FFI).
57    #[inline]
58    pub fn raw(self) -> Option<usize> {
59        self.0
60    }
61
62    /// Reconstruct a scope from its [`raw`](Self::raw) offset (FFI round-trip).
63    #[inline]
64    pub fn from_raw(end: Option<usize>) -> Self {
65        DHeaderScope(end)
66    }
67}
68
69pub struct CdrWriter<'a> {
70    buf: &'a mut [u8],
71    pos: usize,
72    /// Byte offset where payload data begins (0 for raw, 4 after CDR header).
73    /// Alignment padding is calculated as `(pos - origin) % alignment`.
74    origin: usize,
75    /// CDR encoding version — drives DHEADER emission + the alignment cap.
76    version: EncodingVersion,
77    /// Phase 380 W3 — MEASURE mode: advance `pos` exactly as a real write
78    /// would, and copy nothing.
79    ///
80    /// This is how `serialized_size` stays exact: it is not a second
81    /// implementation that could disagree with the writer, it IS the writer,
82    /// with the stores turned off. Every alignment rule, every DHEADER, every
83    /// `len + 1` string prefix is therefore counted by the same code that emits
84    /// it, and a change to one cannot drift from the other.
85    measure: bool,
86}
87
88impl<'a> CdrWriter<'a> {
89    /// Create a new CDR writer
90    pub fn new(buf: &'a mut [u8]) -> Self {
91        Self {
92            buf,
93            pos: 0,
94            origin: 0,
95            version: EncodingVersion::Xcdr1,
96            measure: false,
97        }
98    }
99
100    /// Create a CDR writer positioned at `pos` bytes into `buf`.
101    ///
102    /// `origin` stays at 0, so alignment is computed relative to the start
103    /// of `buf`. Used by FFI bridges that hand us a `(origin, cursor, end)`
104    /// triple where `buf = origin..end` and the caller's cursor is `pos`.
105    pub fn new_at(buf: &'a mut [u8], pos: usize) -> Result<Self, SerError> {
106        if pos > buf.len() {
107            return Err(SerError::BufferTooSmall);
108        }
109        Ok(Self {
110            buf,
111            pos,
112            origin: 0,
113            version: EncodingVersion::Xcdr1,
114            measure: false,
115        })
116    }
117
118    /// Like [`new_at`](Self::new_at) but XCDR2 (DELIMITED_CDR2) — 8-byte
119    /// primitives align to 4 and [`begin_dheader`](Self::begin_dheader) emits a
120    /// DHEADER. Used by the C FFI (`nros-c`) tx path, which manages the
121    /// encapsulation header + cursor itself (phase-303 W4 / #0267).
122    pub fn new_at_xcdr2(buf: &'a mut [u8], pos: usize) -> Result<Self, SerError> {
123        if pos > buf.len() {
124            return Err(SerError::BufferTooSmall);
125        }
126        Ok(Self {
127            buf,
128            pos,
129            origin: 0,
130            version: EncodingVersion::Xcdr2,
131            measure: false,
132        })
133    }
134
135    /// Create a new CDR writer with the 4-byte encapsulation header.
136    ///
137    /// Writes `[0x00, 0x01, 0x00, 0x00]` (CDR little-endian) at the start
138    /// and sets `origin = 4` so subsequent alignment is relative to the
139    /// payload, not the header. This is the normal entry point for
140    /// serialising ROS 2 messages.
141    pub fn new_with_header(buf: &'a mut [u8]) -> Result<Self, SerError> {
142        if buf.len() < 4 {
143            return Err(SerError::BufferTooSmall);
144        }
145        buf[0..4].copy_from_slice(&CDR_LE_HEADER);
146        Ok(Self {
147            buf,
148            pos: 4,
149            origin: 4,
150            version: EncodingVersion::Xcdr1,
151            measure: false,
152        })
153    }
154
155    /// Create an XCDR2 (DELIMITED_CDR2) writer with the `0x0009` encapsulation
156    /// header (phase-303 W2 / #0267). Each struct — top-level and nested — must
157    /// be wrapped in [`begin_dheader`](Self::begin_dheader) /
158    /// [`end_dheader`](Self::end_dheader); 8-byte primitives align to 4.
159    pub fn new_with_header_xcdr2(buf: &'a mut [u8]) -> Result<Self, SerError> {
160        if buf.len() < 4 {
161            return Err(SerError::BufferTooSmall);
162        }
163        buf[0..4].copy_from_slice(&CDR2_DELIMITED_LE_HEADER);
164        Ok(Self {
165            buf,
166            pos: 4,
167            origin: 4,
168            version: EncodingVersion::Xcdr2,
169            measure: false,
170        })
171    }
172
173    /// Phase 380 W3 — a writer that counts bytes and stores none.
174    ///
175    /// `serialized_size` needs the EXACT size of one message, which a type-level
176    /// bound cannot give for an unbounded type and which a second walk of the
177    /// schema could only approximate. Running the real writer with its stores
178    /// disabled makes the count exact by construction: same alignment, same
179    /// DHEADERs, same `len + 1` string prefix, because it is the same code.
180    ///
181    /// Pass an empty slice — the buffer is never touched:
182    ///
183    /// ```ignore
184    /// let mut w = CdrWriter::measuring(&mut [], EncodingVersion::Xcdr1);
185    /// value.serialize(&mut w)?;
186    /// let bytes = w.position();
187    /// ```
188    ///
189    /// The encapsulation header is NOT counted: like the real constructors'
190    /// `origin`, a measuring writer starts at 0. Add
191    /// [`crate::size::ENCAPSULATION_HEADER_BYTES`] for the payload a publisher
192    /// hands the transport.
193    pub fn measuring(buf: &'a mut [u8], version: EncodingVersion) -> Self {
194        Self {
195            buf,
196            pos: 0,
197            origin: 0,
198            version,
199            measure: true,
200        }
201    }
202
203    /// The CDR encoding version this writer emits.
204    #[inline]
205    pub fn version(&self) -> EncodingVersion {
206        self.version
207    }
208
209    /// Begin a DHEADER-delimited struct. Under XCDR2, aligns to 4, reserves a
210    /// 4-byte size slot (backpatched by [`end_dheader`](Self::end_dheader)), and
211    /// returns its position. Under XCDR1 this is a NO-OP (returns an empty mark),
212    /// so generated `serialize` bodies can wrap every struct unconditionally
213    /// while XCDR1 output stays byte-identical.
214    #[inline]
215    pub fn begin_dheader(&mut self) -> Result<DHeaderMark, SerError> {
216        match self.version {
217            EncodingVersion::Xcdr1 => Ok(DHeaderMark(None)),
218            EncodingVersion::Xcdr2 => {
219                self.align(4)?;
220                if self.remaining() < 4 {
221                    return Err(SerError::BufferTooSmall);
222                }
223                let at = self.pos;
224                if !self.measure {
225                    self.buf[at..at + 4].copy_from_slice(&[0, 0, 0, 0]);
226                }
227                self.pos += 4;
228                Ok(DHeaderMark(Some(at)))
229            }
230        }
231    }
232
233    /// Finish a DHEADER-delimited struct: backpatch the reserved slot with the
234    /// serialized size of the member block written since
235    /// [`begin_dheader`](Self::begin_dheader). No-op under XCDR1.
236    #[inline]
237    pub fn end_dheader(&mut self, mark: DHeaderMark) -> Result<(), SerError> {
238        if let Some(at) = mark.0 {
239            let size = (self.pos - (at + 4)) as u32;
240            if !self.measure {
241                self.buf[at..at + 4].copy_from_slice(&size.to_le_bytes());
242            }
243        }
244        Ok(())
245    }
246
247    /// Get current position in buffer
248    #[inline]
249    pub fn position(&self) -> usize {
250        self.pos
251    }
252
253    /// Get remaining capacity
254    #[inline]
255    pub fn remaining(&self) -> usize {
256        if self.measure {
257            // Nothing is stored, so nothing can overflow — a measuring pass must
258            // never report BufferTooSmall or it would stop counting early and
259            // under-report, which is the dangerous direction.
260            return usize::MAX;
261        }
262        self.buf.len().saturating_sub(self.pos)
263    }
264
265    /// Get the written bytes
266    pub fn as_slice(&self) -> &[u8] {
267        &self.buf[..self.pos]
268    }
269
270    /// Align to the given boundary (relative to origin). Under XCDR2 the
271    /// alignment is capped at 4 — 8-byte primitives align to 4, not 8 (the one
272    /// primitive-layout difference from XCDR1).
273    #[inline]
274    pub fn align(&mut self, alignment: usize) -> Result<(), SerError> {
275        let alignment = match self.version {
276            EncodingVersion::Xcdr2 => alignment.min(4),
277            EncodingVersion::Xcdr1 => alignment,
278        };
279        let offset = self.pos - self.origin;
280        let padding = (alignment - (offset % alignment)) % alignment;
281        if self.remaining() < padding {
282            return Err(SerError::BufferTooSmall);
283        }
284        // Fill padding with zeros
285        for i in 0..padding {
286            if !self.measure {
287                self.buf[self.pos + i] = 0;
288            }
289        }
290        self.pos += padding;
291        Ok(())
292    }
293
294    /// Write a single byte without alignment
295    #[inline]
296    pub fn write_u8(&mut self, value: u8) -> Result<(), SerError> {
297        if self.remaining() < 1 {
298            return Err(SerError::BufferTooSmall);
299        }
300        if !self.measure {
301            self.buf[self.pos] = value;
302        }
303        self.pos += 1;
304        Ok(())
305    }
306
307    /// Write a boolean (serialized as a single byte: 0 = false, 1 = true)
308    #[inline]
309    pub fn write_bool(&mut self, value: bool) -> Result<(), SerError> {
310        self.write_u8(value as u8)
311    }
312
313    /// Write i8 without alignment
314    #[inline]
315    pub fn write_i8(&mut self, value: i8) -> Result<(), SerError> {
316        self.write_u8(value as u8)
317    }
318
319    /// Write bytes without alignment
320    #[inline]
321    pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), SerError> {
322        if self.remaining() < bytes.len() {
323            return Err(SerError::BufferTooSmall);
324        }
325        if !self.measure {
326            self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
327        }
328        self.pos += bytes.len();
329        Ok(())
330    }
331
332    /// Write u16 with alignment (little-endian)
333    #[inline]
334    pub fn write_u16(&mut self, value: u16) -> Result<(), SerError> {
335        self.align(2)?;
336        if self.remaining() < 2 {
337            return Err(SerError::BufferTooSmall);
338        }
339        if !self.measure {
340            self.buf[self.pos..self.pos + 2].copy_from_slice(&value.to_le_bytes());
341        }
342        self.pos += 2;
343        Ok(())
344    }
345
346    /// Write u32 with alignment (little-endian)
347    #[inline]
348    pub fn write_u32(&mut self, value: u32) -> Result<(), SerError> {
349        self.align(4)?;
350        if self.remaining() < 4 {
351            return Err(SerError::BufferTooSmall);
352        }
353        if !self.measure {
354            self.buf[self.pos..self.pos + 4].copy_from_slice(&value.to_le_bytes());
355        }
356        self.pos += 4;
357        Ok(())
358    }
359
360    /// Write u64 with alignment (little-endian)
361    #[inline]
362    pub fn write_u64(&mut self, value: u64) -> Result<(), SerError> {
363        self.align(8)?;
364        if self.remaining() < 8 {
365            return Err(SerError::BufferTooSmall);
366        }
367        if !self.measure {
368            self.buf[self.pos..self.pos + 8].copy_from_slice(&value.to_le_bytes());
369        }
370        self.pos += 8;
371        Ok(())
372    }
373
374    /// Write i16 with alignment (little-endian)
375    #[inline]
376    pub fn write_i16(&mut self, value: i16) -> Result<(), SerError> {
377        self.write_u16(value as u16)
378    }
379
380    /// Write i32 with alignment (little-endian)
381    #[inline]
382    pub fn write_i32(&mut self, value: i32) -> Result<(), SerError> {
383        self.write_u32(value as u32)
384    }
385
386    /// Write i64 with alignment (little-endian)
387    #[inline]
388    pub fn write_i64(&mut self, value: i64) -> Result<(), SerError> {
389        self.write_u64(value as u64)
390    }
391
392    /// Write f32 with alignment (little-endian)
393    #[inline]
394    pub fn write_f32(&mut self, value: f32) -> Result<(), SerError> {
395        self.write_u32(value.to_bits())
396    }
397
398    /// Write f64 with alignment (little-endian)
399    #[inline]
400    pub fn write_f64(&mut self, value: f64) -> Result<(), SerError> {
401        self.write_u64(value.to_bits())
402    }
403
404    /// Write a CDR string (4-byte length including null + data + null terminator)
405    pub fn write_string(&mut self, s: &str) -> Result<(), SerError> {
406        let len = s.len() + 1; // Include null terminator
407        if len > u32::MAX as usize {
408            return Err(SerError::StringTooLong);
409        }
410        self.write_u32(len as u32)?;
411        self.write_bytes(s.as_bytes())?;
412        self.write_u8(0)?; // Null terminator
413        Ok(())
414    }
415
416    /// Write a sequence length (4-byte count)
417    #[inline]
418    pub fn write_sequence_len(&mut self, len: usize) -> Result<(), SerError> {
419        if len > u32::MAX as usize {
420            return Err(SerError::SequenceTooLong);
421        }
422        self.write_u32(len as u32)
423    }
424}
425
426/// CDR reader for deserialization
427///
428/// Handles alignment and endianness for CDR decoding.
429pub struct CdrReader<'a> {
430    buf: &'a [u8],
431    pos: usize,
432    origin: usize,
433    /// CDR encoding version — parsed from the encapsulation header; drives
434    /// DHEADER handling + the alignment cap.
435    version: EncodingVersion,
436}
437
438impl<'a> CdrReader<'a> {
439    /// Create a new CDR reader
440    pub fn new(buf: &'a [u8]) -> Self {
441        Self {
442            buf,
443            pos: 0,
444            origin: 0,
445            version: EncodingVersion::Xcdr1,
446        }
447    }
448
449    /// Create a CDR reader positioned at `pos` bytes into `buf`.
450    ///
451    /// `origin` stays at 0, so alignment is computed relative to the start
452    /// of `buf`. Used by FFI bridges that hand us a `(origin, cursor, end)`
453    /// triple where `buf = origin..end` and the caller's cursor is `pos`.
454    pub fn new_at(buf: &'a [u8], pos: usize) -> Result<Self, DeserError> {
455        if pos > buf.len() {
456            return Err(DeserError::UnexpectedEof);
457        }
458        Ok(Self {
459            buf,
460            pos,
461            origin: 0,
462            version: EncodingVersion::Xcdr1,
463        })
464    }
465
466    /// Like [`new_at`](Self::new_at) but XCDR2 (DELIMITED_CDR2) — the align cap +
467    /// [`begin_dheader`](Self::begin_dheader) read the DHEADER. For the C FFI rx
468    /// path, which strips the encapsulation header itself (phase-303 W4).
469    pub fn new_at_xcdr2(buf: &'a [u8], pos: usize) -> Result<Self, DeserError> {
470        if pos > buf.len() {
471            return Err(DeserError::UnexpectedEof);
472        }
473        Ok(Self {
474            buf,
475            pos,
476            origin: 0,
477            version: EncodingVersion::Xcdr2,
478        })
479    }
480
481    /// Create a new CDR reader, parsing and validating the encapsulation header
482    ///
483    /// Expects a 4-byte CDR header at the start of the buffer.
484    pub fn new_with_header(buf: &'a [u8]) -> Result<Self, DeserError> {
485        if buf.len() < 4 {
486            return Err(DeserError::UnexpectedEof);
487        }
488        // Parse the encapsulation id. XCDR1 PLAIN_CDR (0x0000 BE / 0x0001 LE)
489        // and XCDR2 DELIMITED_CDR2 (0x0008 BE / 0x0009 LE — appendable, the
490        // form nano-ros emits/expects). We decode little-endian regardless.
491        if buf[0] != 0x00 {
492            return Err(DeserError::InvalidHeader);
493        }
494        let version = match buf[1] {
495            0x00 | 0x01 => EncodingVersion::Xcdr1,
496            0x08 | 0x09 => EncodingVersion::Xcdr2,
497            _ => return Err(DeserError::InvalidHeader),
498        };
499        Ok(Self {
500            buf,
501            pos: 4,
502            origin: 4,
503            version,
504        })
505    }
506
507    /// Get current position in buffer
508    #[inline]
509    pub fn position(&self) -> usize {
510        self.pos
511    }
512
513    /// Whether nothing has been read yet — the reader still sits on its
514    /// alignment origin.
515    ///
516    /// CDR alignment is computed as `pos - origin`, so a caller that re-reads a
517    /// request by taking the remaining bytes and building a fresh reader over
518    /// them only gets identical alignment when the slice STARTS at the origin.
519    /// That is a real precondition with no visible symptom when broken — the
520    /// padding silently shifts — and `origin` is private, so this is how a
521    /// caller checks it. See `parameter_services::rewind` (phase-382 W1').
522    #[inline]
523    pub fn is_at_origin(&self) -> bool {
524        self.pos == self.origin
525    }
526
527    /// Get remaining bytes
528    #[inline]
529    pub fn remaining(&self) -> usize {
530        self.buf.len().saturating_sub(self.pos)
531    }
532
533    /// Check if reader is at end of buffer
534    #[inline]
535    pub fn is_empty(&self) -> bool {
536        self.remaining() == 0
537    }
538
539    /// Align to the given boundary (relative to origin). Under XCDR2 the
540    /// alignment is capped at 4 (mirrors [`CdrWriter::align`]).
541    #[inline]
542    pub fn align(&mut self, alignment: usize) -> Result<(), DeserError> {
543        let alignment = match self.version {
544            EncodingVersion::Xcdr2 => alignment.min(4),
545            EncodingVersion::Xcdr1 => alignment,
546        };
547        let offset = self.pos - self.origin;
548        let padding = (alignment - (offset % alignment)) % alignment;
549        if self.remaining() < padding {
550            return Err(DeserError::UnexpectedEof);
551        }
552        self.pos += padding;
553        Ok(())
554    }
555
556    /// The CDR encoding version parsed from the header.
557    #[inline]
558    pub fn version(&self) -> EncodingVersion {
559        self.version
560    }
561
562    /// Begin reading a DHEADER-delimited struct. Under XCDR2, aligns to 4, reads
563    /// the 4-byte DHEADER size, and returns the absolute buffer position of the
564    /// struct's END (so trailing unknown members can be skipped for forward
565    /// compat). Under XCDR1 this is a NO-OP (returns `None`) — generated
566    /// `deserialize` bodies wrap every struct unconditionally.
567    #[inline]
568    pub fn begin_dheader(&mut self) -> Result<DHeaderScope, DeserError> {
569        match self.version {
570            EncodingVersion::Xcdr1 => Ok(DHeaderScope(None)),
571            EncodingVersion::Xcdr2 => {
572                let size = self.read_u32()? as usize;
573                let end = self
574                    .pos
575                    .checked_add(size)
576                    .ok_or(DeserError::UnexpectedEof)?;
577                if end > self.buf.len() {
578                    return Err(DeserError::UnexpectedEof);
579                }
580                Ok(DHeaderScope(Some(end)))
581            }
582        }
583    }
584
585    /// Finish a DHEADER-delimited struct: skip any trailing bytes the writer
586    /// included beyond the members we read (unknown appended members — XCDR2
587    /// forward compatibility). No-op under XCDR1. Errors if we OVER-read past the
588    /// declared end (a corrupt/mismatched stream).
589    #[inline]
590    pub fn end_dheader(&mut self, scope: DHeaderScope) -> Result<(), DeserError> {
591        if let Some(end) = scope.0 {
592            if self.pos > end {
593                return Err(DeserError::DHeaderOverrun);
594            }
595            self.pos = end;
596        }
597        Ok(())
598    }
599
600    /// Read a single byte without alignment
601    #[inline]
602    pub fn read_u8(&mut self) -> Result<u8, DeserError> {
603        if self.remaining() < 1 {
604            return Err(DeserError::UnexpectedEof);
605        }
606        let value = self.buf[self.pos];
607        self.pos += 1;
608        Ok(value)
609    }
610
611    /// Read a boolean (deserialized from a single byte: 0 = false, non-zero = true)
612    #[inline]
613    pub fn read_bool(&mut self) -> Result<bool, DeserError> {
614        Ok(self.read_u8()? != 0)
615    }
616
617    /// Read i8 without alignment
618    #[inline]
619    pub fn read_i8(&mut self) -> Result<i8, DeserError> {
620        Ok(self.read_u8()? as i8)
621    }
622
623    /// Read bytes without alignment
624    #[inline]
625    pub fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], DeserError> {
626        if self.remaining() < len {
627            return Err(DeserError::UnexpectedEof);
628        }
629        let bytes = &self.buf[self.pos..self.pos + len];
630        self.pos += len;
631        Ok(bytes)
632    }
633
634    /// Read u16 with alignment (little-endian)
635    #[inline]
636    pub fn read_u16(&mut self) -> Result<u16, DeserError> {
637        self.align(2)?;
638        if self.remaining() < 2 {
639            return Err(DeserError::UnexpectedEof);
640        }
641        let value = u16::from_le_bytes([self.buf[self.pos], self.buf[self.pos + 1]]);
642        self.pos += 2;
643        Ok(value)
644    }
645
646    /// Read u32 with alignment (little-endian)
647    #[inline]
648    pub fn read_u32(&mut self) -> Result<u32, DeserError> {
649        self.align(4)?;
650        if self.remaining() < 4 {
651            return Err(DeserError::UnexpectedEof);
652        }
653        let value = u32::from_le_bytes([
654            self.buf[self.pos],
655            self.buf[self.pos + 1],
656            self.buf[self.pos + 2],
657            self.buf[self.pos + 3],
658        ]);
659        self.pos += 4;
660        Ok(value)
661    }
662
663    /// Read u64 with alignment (little-endian)
664    #[inline]
665    pub fn read_u64(&mut self) -> Result<u64, DeserError> {
666        self.align(8)?;
667        if self.remaining() < 8 {
668            return Err(DeserError::UnexpectedEof);
669        }
670        let value = u64::from_le_bytes([
671            self.buf[self.pos],
672            self.buf[self.pos + 1],
673            self.buf[self.pos + 2],
674            self.buf[self.pos + 3],
675            self.buf[self.pos + 4],
676            self.buf[self.pos + 5],
677            self.buf[self.pos + 6],
678            self.buf[self.pos + 7],
679        ]);
680        self.pos += 8;
681        Ok(value)
682    }
683
684    /// Read i16 with alignment (little-endian)
685    #[inline]
686    pub fn read_i16(&mut self) -> Result<i16, DeserError> {
687        Ok(self.read_u16()? as i16)
688    }
689
690    /// Read i32 with alignment (little-endian)
691    #[inline]
692    pub fn read_i32(&mut self) -> Result<i32, DeserError> {
693        Ok(self.read_u32()? as i32)
694    }
695
696    /// Read i64 with alignment (little-endian)
697    #[inline]
698    pub fn read_i64(&mut self) -> Result<i64, DeserError> {
699        Ok(self.read_u64()? as i64)
700    }
701
702    /// Read f32 with alignment (little-endian)
703    #[inline]
704    pub fn read_f32(&mut self) -> Result<f32, DeserError> {
705        Ok(f32::from_bits(self.read_u32()?))
706    }
707
708    /// Read f64 with alignment (little-endian)
709    #[inline]
710    pub fn read_f64(&mut self) -> Result<f64, DeserError> {
711        Ok(f64::from_bits(self.read_u64()?))
712    }
713
714    /// Read a CDR string (4-byte length including null + data + null terminator)
715    ///
716    /// Returns a string slice pointing into the buffer (zero-copy).
717    pub fn read_string(&mut self) -> Result<&'a str, DeserError> {
718        let len = self.read_u32()? as usize;
719        if len == 0 {
720            return Err(DeserError::InvalidData);
721        }
722        if self.remaining() < len {
723            return Err(DeserError::UnexpectedEof);
724        }
725        // Length includes null terminator, so actual string is len - 1 bytes
726        let bytes = &self.buf[self.pos..self.pos + len - 1];
727        self.pos += len;
728        core::str::from_utf8(bytes).map_err(|_| DeserError::InvalidUtf8)
729    }
730
731    /// Read a sequence length (4-byte count)
732    #[inline]
733    pub fn read_sequence_len(&mut self) -> Result<usize, DeserError> {
734        Ok(self.read_u32()? as usize)
735    }
736
737    // ── Borrowed slice readers (zero-copy for primitive sequences) ──
738
739    /// Read a `uint8[]` / `byte[]` sequence as a borrowed slice.
740    ///
741    /// Returns `&'a [u8]` pointing directly into the CDR buffer. Zero-copy.
742    /// Reads the 4-byte length prefix, then returns a slice of that length.
743    pub fn read_slice_u8(&mut self) -> Result<&'a [u8], DeserError> {
744        let len = self.read_u32()? as usize;
745        self.read_bytes(len)
746    }
747
748    /// Read an `int8[]` sequence as a borrowed slice.
749    pub fn read_slice_i8(&mut self) -> Result<&'a [u8], DeserError> {
750        // i8 and u8 have identical CDR encoding (1 byte, no alignment)
751        self.read_slice_u8()
752    }
753
754    /// Read a `bool[]` sequence as a borrowed `&[u8]` slice.
755    ///
756    /// CDR encodes booleans as single bytes (0/1). The returned slice
757    /// contains raw bytes; the caller interprets 0 as false, non-zero as true.
758    pub fn read_slice_bool(&mut self) -> Result<&'a [u8], DeserError> {
759        self.read_slice_u8()
760    }
761
762    /// Read a multi-byte numeric sequence (`float32[]`, `uint16[]`, …) as a
763    /// borrowed [`LeSliceView`] — the alignment-agnostic borrowed reader for
764    /// RFC-0033 `borrowed` mode (Phase 229.6, issue 0007).
765    ///
766    /// Unlike a `&'a [T]` cast, this never requires the source buffer to be
767    /// `T`-aligned: the view borrows the raw little-endian bytes zero-copy and
768    /// decodes each element on access via `from_le_bytes`. Reads the 4-byte
769    /// length prefix, aligns the reader to `T` within the CDR stream, then
770    /// returns a view over `len * size_of::<T>()` bytes.
771    pub fn read_le_slice<T: LeDecode>(&mut self) -> Result<LeSliceView<'a, T>, DeserError> {
772        let len = self.read_u32()? as usize;
773        self.align(T::SIZE)?;
774        let byte_len = len * T::SIZE;
775        let bytes = self.read_bytes(byte_len)?;
776        Ok(LeSliceView::new(bytes))
777    }
778}
779
780/// A little-endian-decodable fixed-width numeric element of a borrowed sequence.
781///
782/// Implemented for the multi-byte CDR numeric primitives. Single-byte types
783/// (`u8`/`i8`/`bool`) do not need this — they borrow directly as `&[u8]`.
784pub trait LeDecode: Sized + Copy {
785    /// Encoded width in bytes (CDR little-endian).
786    const SIZE: usize;
787    /// Decode one element from exactly [`SIZE`](Self::SIZE) little-endian bytes.
788    fn from_le(bytes: &[u8]) -> Self;
789}
790
791macro_rules! impl_le_decode {
792    ($($t:ty),+ $(,)?) => {$(
793        impl LeDecode for $t {
794            const SIZE: usize = core::mem::size_of::<$t>();
795            #[inline]
796            fn from_le(bytes: &[u8]) -> Self {
797                let mut buf = [0u8; core::mem::size_of::<$t>()];
798                buf.copy_from_slice(bytes);
799                <$t>::from_le_bytes(buf)
800            }
801        }
802    )+};
803}
804impl_le_decode!(u16, i16, u32, i32, u64, i64, f32, f64);
805
806/// A borrowed, alignment-agnostic view over a CDR little-endian numeric
807/// sequence (RFC-0033 `borrowed` mode). Borrows the raw payload bytes
808/// zero-copy; decodes elements lazily on access, so the source buffer need not
809/// be `T`-aligned. Valid only for the borrow lifetime `'a` (the subscription
810/// callback scope).
811#[derive(Clone, Copy)]
812pub struct LeSliceView<'a, T> {
813    bytes: &'a [u8],
814    _marker: core::marker::PhantomData<fn() -> T>,
815}
816
817impl<'a, T: LeDecode> LeSliceView<'a, T> {
818    /// Wrap raw little-endian payload bytes. `bytes.len()` must be a multiple of
819    /// `T::SIZE` (guaranteed by [`CdrReader::read_le_slice`]).
820    #[inline]
821    pub fn new(bytes: &'a [u8]) -> Self {
822        Self {
823            bytes,
824            _marker: core::marker::PhantomData,
825        }
826    }
827
828    /// Number of elements in the view.
829    #[inline]
830    pub fn len(&self) -> usize {
831        self.bytes.len() / T::SIZE
832    }
833
834    /// Whether the view is empty.
835    #[inline]
836    pub fn is_empty(&self) -> bool {
837        self.bytes.is_empty()
838    }
839
840    /// The raw little-endian payload bytes (zero-copy).
841    #[inline]
842    pub fn as_bytes(&self) -> &'a [u8] {
843        self.bytes
844    }
845
846    /// Decode the element at `index`, or `None` if out of bounds.
847    #[inline]
848    pub fn get(&self, index: usize) -> Option<T> {
849        let start = index.checked_mul(T::SIZE)?;
850        let end = start.checked_add(T::SIZE)?;
851        self.bytes.get(start..end).map(T::from_le)
852    }
853
854    /// Iterate over the decoded elements.
855    #[inline]
856    pub fn iter(&self) -> impl Iterator<Item = T> + 'a {
857        let bytes = self.bytes;
858        (0..bytes.len() / T::SIZE).map(move |i| T::from_le(&bytes[i * T::SIZE..(i + 1) * T::SIZE]))
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    // ── phase-303 W2/W3 — XCDR2 (DELIMITED_CDR2 + DHEADER) ──────────────────
867
868    /// A nested-struct serialize (Header-like: `{ Time{i32 sec, u32 nanosec};
869    /// string frame_id }`) wrapped in DHEADERs under XCDR2 lays out the
870    /// encapsulation, the top DHEADER, the nested-Time DHEADER, and the members
871    /// at the canonical offsets — and round-trips through the reader.
872    /// Serialize `std_msgs/Header { builtin_interfaces/Time stamp; string
873    /// frame_id }` the way the generated code does (each struct DHEADER-wrapped),
874    /// with `stamp = {sec:7, nanosec:9}`, `frame_id = "ab"`.
875    fn serialize_header(w: &mut CdrWriter) {
876        let h = w.begin_dheader().unwrap();
877        // nested Time
878        let t = w.begin_dheader().unwrap();
879        w.write_i32(7).unwrap();
880        w.write_u32(9).unwrap();
881        w.end_dheader(t).unwrap();
882        w.write_string("ab").unwrap();
883        w.end_dheader(h).unwrap();
884    }
885
886    /// WIRE ORACLE (phase-303 #0267). Under XCDR1 the DHEADER wrap is a no-op, so
887    /// nano-ros's Header bytes are BYTE-IDENTICAL to what a real ROS 2 Jazzy node
888    /// puts ON THE WIRE by default. Captured live from `ros:jazzy-ros-base`
889    /// (2026-07-26) — a Header pub → a `raw=True` subscriber (the actual
890    /// negotiated RTPS payload, not just `serialize_message`):
891    ///   - `rmw_fastrtps_cpp` (default): exactly these 19 bytes.
892    ///   - `rmw_cyclonedds_cpp`: identical content + one trailing `00`
893    ///     alignment pad (20 bytes) — decodes the same.
894    ///
895    /// Both use encapsulation `00 01` (XCDR1), NO DHEADER: modern Jazzy STILL
896    /// defaults to XCDR1 on the wire. So nano-ros (humble, or a jazzy build
897    /// talking to a default peer) already interoperates byte-for-byte. XCDR2 +
898    /// DHEADER only appears with a non-default negotiated `data_representation`
899    /// (the #0267 domain_bridge trigger) — covered by the XCDR2 path.
900    #[test]
901    fn xcdr1_header_matches_live_jazzy_wire_bytes() {
902        let mut buf = [0u8; 64];
903        let n = {
904            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
905            serialize_header(&mut w);
906            w.position()
907        };
908        // Captured from `ros:jazzy-ros-base` rclpy serialize_message(Header):
909        //   00 01 00 00 | 07 00 00 00 | 09 00 00 00 | 03 00 00 00 | 61 62 00
910        let jazzy: [u8; 19] = [
911            0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x03, 0x00,
912            0x00, 0x00, 0x61, 0x62, 0x00,
913        ];
914        assert_eq!(
915            &buf[..n],
916            &jazzy,
917            "nano-ros XCDR1 Header must equal live Jazzy wire bytes"
918        );
919    }
920
921    /// The XCDR2 (jazzy build) Header: appendable → a DHEADER per struct. The
922    /// encapsulation flips to `00 09`, and Cyclone/a modern peer negotiating
923    /// XCDR2 reads the DHEADERs. Round-trips through the reader.
924    #[test]
925    fn xcdr2_header_has_dheaders_and_roundtrips() {
926        let mut buf = [0u8; 64];
927        let n = {
928            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
929            serialize_header(&mut w);
930            w.position()
931        };
932        assert_eq!(&buf[0..2], &[0x00, 0x09], "XCDR2 DELIMITED encapsulation");
933        // Round-trip via the auto-detecting reader.
934        let mut r = CdrReader::new_with_header(&buf[..n]).unwrap();
935        assert_eq!(r.version(), EncodingVersion::Xcdr2);
936        let h = r.begin_dheader().unwrap();
937        let t = r.begin_dheader().unwrap();
938        assert_eq!(r.read_i32().unwrap(), 7);
939        assert_eq!(r.read_u32().unwrap(), 9);
940        r.end_dheader(t).unwrap();
941        assert_eq!(r.read_string().unwrap(), "ab");
942        r.end_dheader(h).unwrap();
943    }
944
945    #[test]
946    fn xcdr2_nested_dheader_layout_and_roundtrip() {
947        let mut buf = [0u8; 64];
948        {
949            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
950            let top = w.begin_dheader().unwrap();
951            // nested Time { i32 sec; u32 nanosec }
952            let time = w.begin_dheader().unwrap();
953            w.write_i32(7).unwrap();
954            w.write_u32(9).unwrap();
955            w.end_dheader(time).unwrap();
956            // string frame_id
957            w.write_string("ab").unwrap();
958            w.end_dheader(top).unwrap();
959        }
960        // Header (encaps): 0x00 0x09 0x00 0x00.
961        assert_eq!(&buf[0..4], &[0x00, 0x09, 0x00, 0x00]);
962        // pos 4: top DHEADER (u32 LE = size of everything after it).
963        // pos 8: nested Time DHEADER = 8 (two u32).
964        assert_eq!(u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]), 8);
965        // sec @12, nanosec @16.
966        assert_eq!(u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), 7);
967        assert_eq!(u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), 9);
968
969        // Round-trip.
970        let mut r = CdrReader::new_with_header(&buf).unwrap();
971        assert_eq!(r.version(), EncodingVersion::Xcdr2);
972        let top = r.begin_dheader().unwrap();
973        let time = r.begin_dheader().unwrap();
974        assert_eq!(r.read_i32().unwrap(), 7);
975        assert_eq!(r.read_u32().unwrap(), 9);
976        r.end_dheader(time).unwrap();
977        assert_eq!(r.read_string().unwrap(), "ab");
978        r.end_dheader(top).unwrap();
979    }
980
981    /// The DHEADER calls are pure NO-OPs under XCDR1 → a struct wrapped in
982    /// begin/end_dheader emits byte-identical output to one that isn't. This is
983    /// what lets generated code wrap unconditionally with zero Humble impact.
984    #[test]
985    fn xcdr1_dheader_calls_are_byte_identical_noops() {
986        let mut a = [0u8; 32];
987        let mut b = [0u8; 32];
988        let na = {
989            let mut w = CdrWriter::new_with_header(&mut a).unwrap();
990            let d = w.begin_dheader().unwrap();
991            w.write_i32(-2).unwrap();
992            w.write_u32(3).unwrap();
993            w.end_dheader(d).unwrap();
994            w.position()
995        };
996        let nb = {
997            let mut w = CdrWriter::new_with_header(&mut b).unwrap();
998            w.write_i32(-2).unwrap();
999            w.write_u32(3).unwrap();
1000            w.position()
1001        };
1002        assert_eq!(na, nb);
1003        assert_eq!(a[..na], b[..nb]);
1004    }
1005
1006    /// XCDR2 forward-compat: a reader whose type has FEWER members than the
1007    /// writer sent skips the trailing (unknown) bytes via the DHEADER size.
1008    #[test]
1009    fn xcdr2_reader_skips_unknown_trailing_members() {
1010        let mut buf = [0u8; 32];
1011        {
1012            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
1013            let d = w.begin_dheader().unwrap();
1014            w.write_i32(11).unwrap();
1015            // A future writer appended an extra u32 the reader doesn't know.
1016            w.write_u32(22).unwrap();
1017            w.end_dheader(d).unwrap();
1018        }
1019        let mut r = CdrReader::new_with_header(&buf).unwrap();
1020        let d = r.begin_dheader().unwrap();
1021        assert_eq!(r.read_i32().unwrap(), 11);
1022        // The reader read only its known member (i32 @ pos 12); the extra u32 is
1023        // unknown. end_dheader skips it → pos advances to the struct end (16).
1024        assert_eq!(r.position(), 12);
1025        r.end_dheader(d).unwrap();
1026        assert_eq!(
1027            r.position(),
1028            16,
1029            "end_dheader must skip the unknown trailing member"
1030        );
1031    }
1032
1033    #[test]
1034    fn test_write_read_u8() {
1035        let mut buf = [0u8; 16];
1036        let mut writer = CdrWriter::new(&mut buf);
1037        writer.write_u8(0x42).unwrap();
1038        writer.write_u8(0xFF).unwrap();
1039
1040        let mut reader = CdrReader::new(&buf);
1041        assert_eq!(reader.read_u8().unwrap(), 0x42);
1042        assert_eq!(reader.read_u8().unwrap(), 0xFF);
1043    }
1044
1045    #[test]
1046    fn le_slice_view_decodes_unaligned_f32() {
1047        // The whole point of the alignment guard (Phase 229.6): a view can sit
1048        // at an odd byte offset and still decode correctly — no `&[f32]` cast.
1049        let vals = [1.5f32, -2.25, 3.0e10, 0.0];
1050        let mut backing = [0u8; 1 + 4 * 4];
1051        backing[0] = 0xAA; // shift the f32 payload to an odd (1-byte) offset.
1052        for (i, v) in vals.iter().enumerate() {
1053            backing[1 + i * 4..1 + i * 4 + 4].copy_from_slice(&v.to_le_bytes());
1054        }
1055        let view: LeSliceView<f32> = LeSliceView::new(&backing[1..]);
1056        assert_eq!(view.len(), 4);
1057        assert!(!view.is_empty());
1058        for (i, v) in vals.iter().enumerate() {
1059            assert_eq!(view.get(i).unwrap(), *v);
1060        }
1061        assert_eq!(view.get(4), None);
1062        let collected: heapless::Vec<f32, 4> = view.iter().collect();
1063        assert_eq!(&collected[..], &vals[..]);
1064    }
1065
1066    #[test]
1067    fn read_le_slice_roundtrips_through_cdr() {
1068        // Write a `uint16[]` sequence with the CDR writer, read it back as a
1069        // borrowed `LeSliceView` — values + count must match.
1070        let vals = [10u16, 4000, 65535, 1];
1071        let mut buf = [0u8; 64];
1072        let written = {
1073            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
1074            w.write_sequence_len(vals.len()).unwrap();
1075            for v in &vals {
1076                w.write_u16(*v).unwrap();
1077            }
1078            w.position()
1079        };
1080        let mut reader = CdrReader::new_with_header(&buf[..written]).unwrap();
1081        let view = reader.read_le_slice::<u16>().unwrap();
1082        assert_eq!(view.len(), vals.len());
1083        for (i, v) in vals.iter().enumerate() {
1084            assert_eq!(view.get(i).unwrap(), *v);
1085        }
1086    }
1087
1088    #[test]
1089    fn test_write_read_u32_alignment() {
1090        let mut buf = [0u8; 16];
1091        let mut writer = CdrWriter::new(&mut buf);
1092        writer.write_u8(0x01).unwrap(); // Position 1
1093        writer.write_u32(0x12345678).unwrap(); // Should align to position 4
1094
1095        assert_eq!(writer.position(), 8); // 1 byte + 3 padding + 4 bytes
1096
1097        let mut reader = CdrReader::new(&buf);
1098        assert_eq!(reader.read_u8().unwrap(), 0x01);
1099        assert_eq!(reader.read_u32().unwrap(), 0x12345678);
1100    }
1101
1102    #[test]
1103    fn test_write_read_string() {
1104        let mut buf = [0u8; 32];
1105        let mut writer = CdrWriter::new(&mut buf);
1106        writer.write_string("Hello").unwrap();
1107
1108        let mut reader = CdrReader::new(&buf);
1109        assert_eq!(reader.read_string().unwrap(), "Hello");
1110    }
1111
1112    #[test]
1113    fn test_encapsulation_header() {
1114        let mut buf = [0u8; 32];
1115        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1116        writer.write_u32(42).unwrap();
1117
1118        assert_eq!(&buf[0..4], &CDR_LE_HEADER);
1119
1120        let mut reader = CdrReader::new_with_header(&buf).unwrap();
1121        assert_eq!(reader.read_u32().unwrap(), 42);
1122    }
1123
1124    #[test]
1125    fn test_alignment_with_header() {
1126        let mut buf = [0u8; 32];
1127        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1128        // After header (pos=4, origin=4), write u8 then u32
1129        writer.write_u8(0x01).unwrap(); // pos=5
1130        writer.write_u32(0xDEADBEEF).unwrap(); // Should align to pos=8
1131
1132        assert_eq!(writer.position(), 12); // 4 header + 1 byte + 3 padding + 4 bytes
1133
1134        let mut reader = CdrReader::new_with_header(&buf).unwrap();
1135        assert_eq!(reader.read_u8().unwrap(), 0x01);
1136        assert_eq!(reader.read_u32().unwrap(), 0xDEADBEEF);
1137    }
1138}
1139
1140// =============================================================================
1141// Ghost model validation
1142// =============================================================================
1143
1144#[cfg(test)]
1145mod ghost_checks {
1146    use super::*;
1147    use nros_ghost_types::CdrGhost;
1148
1149    /// Structural check: construct CdrGhost from CdrWriter private fields.
1150    /// If a field is renamed or retyped, this fails to compile.
1151    fn ghost_from_writer(w: &CdrWriter) -> CdrGhost {
1152        CdrGhost {
1153            buf_len: w.buf.len(),
1154            pos: w.pos,
1155            origin: w.origin,
1156        }
1157    }
1158
1159    #[test]
1160    fn ghost_new_state() {
1161        let mut buf = [0u8; 64];
1162        let writer = CdrWriter::new(&mut buf);
1163        let ghost = ghost_from_writer(&writer);
1164        assert_eq!(ghost.pos, 0);
1165        assert_eq!(ghost.origin, 0);
1166        assert_eq!(ghost.buf_len, 64);
1167    }
1168
1169    #[test]
1170    fn ghost_header_origin() {
1171        let mut buf = [0u8; 64];
1172        let writer = CdrWriter::new_with_header(&mut buf).unwrap();
1173        let ghost = ghost_from_writer(&writer);
1174        assert_eq!(ghost.pos, 4);
1175        assert_eq!(ghost.origin, 4);
1176    }
1177
1178    #[test]
1179    fn ghost_position_invariant() {
1180        let mut buf = [0u8; 64];
1181        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1182        writer.write_u32(42).unwrap();
1183        let ghost = ghost_from_writer(&writer);
1184        // After header: pos + remaining == buf_len
1185        assert_eq!(ghost.pos + writer.remaining(), ghost.buf_len);
1186    }
1187
1188    #[test]
1189    fn test_read_slice_u8() {
1190        let mut buf = [0u8; 64];
1191        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1192        // Write a uint8[] sequence: [0x10, 0x20, 0x30]
1193        writer.write_u32(3).unwrap(); // length
1194        writer.write_u8(0x10).unwrap();
1195        writer.write_u8(0x20).unwrap();
1196        writer.write_u8(0x30).unwrap();
1197        let len = writer.position();
1198
1199        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1200        let slice = reader.read_slice_u8().unwrap();
1201        assert_eq!(slice, &[0x10, 0x20, 0x30]);
1202    }
1203
1204    #[test]
1205    fn test_read_slice_u8_empty() {
1206        let mut buf = [0u8; 64];
1207        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1208        writer.write_u32(0).unwrap(); // length = 0
1209        let len = writer.position();
1210
1211        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1212        let slice = reader.read_slice_u8().unwrap();
1213        assert!(slice.is_empty());
1214    }
1215}
1216
1217// =============================================================================
1218// Kani bounded model checking proofs
1219// =============================================================================
1220
1221#[cfg(kani)]
1222mod verification {
1223    use super::*;
1224
1225    // ---- Primitive write/read panic-freedom ----
1226
1227    #[kani::proof]
1228    #[kani::unwind(5)]
1229    fn cdr_write_u8_no_panic() {
1230        let mut buf = [0u8; 8];
1231        let mut writer = CdrWriter::new(&mut buf);
1232        let val: u8 = kani::any();
1233        let _ = writer.write_u8(val);
1234    }
1235
1236    #[kani::proof]
1237    #[kani::unwind(5)]
1238    fn cdr_write_bool_no_panic() {
1239        let mut buf = [0u8; 8];
1240        let mut writer = CdrWriter::new(&mut buf);
1241        let val: bool = kani::any();
1242        let _ = writer.write_bool(val);
1243    }
1244
1245    #[kani::proof]
1246    #[kani::unwind(5)]
1247    fn cdr_write_i16_no_panic() {
1248        let mut buf = [0u8; 16];
1249        let mut writer = CdrWriter::new(&mut buf);
1250        let val: i16 = kani::any();
1251        let _ = writer.write_i16(val);
1252    }
1253
1254    #[kani::proof]
1255    #[kani::unwind(5)]
1256    fn cdr_write_i32_no_panic() {
1257        let mut buf = [0u8; 16];
1258        let mut writer = CdrWriter::new(&mut buf);
1259        let val: i32 = kani::any();
1260        let _ = writer.write_i32(val);
1261    }
1262
1263    #[kani::proof]
1264    #[kani::unwind(5)]
1265    fn cdr_write_i64_no_panic() {
1266        let mut buf = [0u8; 16];
1267        let mut writer = CdrWriter::new(&mut buf);
1268        let val: i64 = kani::any();
1269        let _ = writer.write_i64(val);
1270    }
1271
1272    #[kani::proof]
1273    #[kani::unwind(5)]
1274    fn cdr_write_f32_no_panic() {
1275        let mut buf = [0u8; 16];
1276        let mut writer = CdrWriter::new(&mut buf);
1277        let val: f32 = kani::any();
1278        let _ = writer.write_f32(val);
1279    }
1280
1281    #[kani::proof]
1282    #[kani::unwind(5)]
1283    fn cdr_write_f64_no_panic() {
1284        let mut buf = [0u8; 16];
1285        let mut writer = CdrWriter::new(&mut buf);
1286        let val: f64 = kani::any();
1287        let _ = writer.write_f64(val);
1288    }
1289
1290    // ---- Round-trip correctness: write then read produces the same value ----
1291
1292    #[kani::proof]
1293    #[kani::unwind(5)]
1294    fn cdr_roundtrip_u8() {
1295        let mut buf = [0u8; 8];
1296        let val: u8 = kani::any();
1297        let len = {
1298            let mut writer = CdrWriter::new(&mut buf);
1299            writer.write_u8(val).unwrap();
1300            writer.position()
1301        };
1302        let mut reader = CdrReader::new(&buf[..len]);
1303        assert_eq!(reader.read_u8().unwrap(), val);
1304    }
1305
1306    #[kani::proof]
1307    #[kani::unwind(5)]
1308    fn cdr_roundtrip_bool() {
1309        let mut buf = [0u8; 8];
1310        let val: bool = kani::any();
1311        let len = {
1312            let mut writer = CdrWriter::new(&mut buf);
1313            writer.write_bool(val).unwrap();
1314            writer.position()
1315        };
1316        let mut reader = CdrReader::new(&buf[..len]);
1317        assert_eq!(reader.read_bool().unwrap(), val);
1318    }
1319
1320    #[kani::proof]
1321    #[kani::unwind(5)]
1322    fn cdr_roundtrip_i16() {
1323        let mut buf = [0u8; 16];
1324        let val: i16 = kani::any();
1325        let len = {
1326            let mut writer = CdrWriter::new(&mut buf);
1327            writer.write_i16(val).unwrap();
1328            writer.position()
1329        };
1330        let mut reader = CdrReader::new(&buf[..len]);
1331        assert_eq!(reader.read_i16().unwrap(), val);
1332    }
1333
1334    #[kani::proof]
1335    #[kani::unwind(5)]
1336    fn cdr_roundtrip_i32() {
1337        let mut buf = [0u8; 16];
1338        let val: i32 = kani::any();
1339        let len = {
1340            let mut writer = CdrWriter::new(&mut buf);
1341            writer.write_i32(val).unwrap();
1342            writer.position()
1343        };
1344        let mut reader = CdrReader::new(&buf[..len]);
1345        assert_eq!(reader.read_i32().unwrap(), val);
1346    }
1347
1348    #[kani::proof]
1349    #[kani::unwind(5)]
1350    fn cdr_roundtrip_i64() {
1351        let mut buf = [0u8; 16];
1352        let val: i64 = kani::any();
1353        let len = {
1354            let mut writer = CdrWriter::new(&mut buf);
1355            writer.write_i64(val).unwrap();
1356            writer.position()
1357        };
1358        let mut reader = CdrReader::new(&buf[..len]);
1359        assert_eq!(reader.read_i64().unwrap(), val);
1360    }
1361
1362    #[kani::proof]
1363    #[kani::unwind(5)]
1364    fn cdr_roundtrip_f32() {
1365        let mut buf = [0u8; 16];
1366        let val: f32 = kani::any();
1367        let len = {
1368            let mut writer = CdrWriter::new(&mut buf);
1369            writer.write_f32(val).unwrap();
1370            writer.position()
1371        };
1372        let mut reader = CdrReader::new(&buf[..len]);
1373        let result = reader.read_f32().unwrap();
1374        assert_eq!(val.to_bits(), result.to_bits());
1375    }
1376
1377    #[kani::proof]
1378    #[kani::unwind(5)]
1379    fn cdr_roundtrip_f64() {
1380        let mut buf = [0u8; 16];
1381        let val: f64 = kani::any();
1382        let len = {
1383            let mut writer = CdrWriter::new(&mut buf);
1384            writer.write_f64(val).unwrap();
1385            writer.position()
1386        };
1387        let mut reader = CdrReader::new(&buf[..len]);
1388        let result = reader.read_f64().unwrap();
1389        assert_eq!(val.to_bits(), result.to_bits());
1390    }
1391
1392    // ---- CDR header round-trip ----
1393
1394    #[kani::proof]
1395    #[kani::unwind(5)]
1396    fn cdr_roundtrip_with_header_i32() {
1397        let mut buf = [0u8; 16];
1398        let val: i32 = kani::any();
1399        let len = {
1400            let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1401            writer.write_i32(val).unwrap();
1402            writer.position()
1403        };
1404        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1405        assert_eq!(reader.read_i32().unwrap(), val);
1406    }
1407
1408    // ---- Buffer exhaustion returns Err, never panics ----
1409
1410    #[kani::proof]
1411    #[kani::unwind(5)]
1412    fn cdr_write_buffer_exhaustion_u32() {
1413        let mut buf = [0u8; 3]; // Too small for u32
1414        let mut writer = CdrWriter::new(&mut buf);
1415        let val: u32 = kani::any();
1416        let result = writer.write_u32(val);
1417        assert!(result.is_err());
1418    }
1419
1420    #[kani::proof]
1421    #[kani::unwind(5)]
1422    fn cdr_write_header_buffer_too_small() {
1423        let mut buf = [0u8; 3]; // Too small for 4-byte header
1424        let result = CdrWriter::new_with_header(&mut buf);
1425        assert!(result.is_err());
1426    }
1427
1428    // ---- Deserialization of arbitrary bytes: Ok or Err, never panic ----
1429
1430    #[kani::proof]
1431    #[kani::unwind(5)]
1432    fn cdr_deserialize_arbitrary_bytes_i32() {
1433        let mut buf = [0u8; 8];
1434        buf[0] = kani::any();
1435        buf[1] = kani::any();
1436        buf[2] = kani::any();
1437        buf[3] = kani::any();
1438        buf[4] = kani::any();
1439        buf[5] = kani::any();
1440        buf[6] = kani::any();
1441        buf[7] = kani::any();
1442        let result = CdrReader::new_with_header(&buf);
1443        if let Ok(mut reader) = result {
1444            let _ = reader.read_i32(); // Ok or Err, not panic
1445        }
1446    }
1447
1448    #[kani::proof]
1449    #[kani::unwind(5)]
1450    fn cdr_deserialize_empty_buffer() {
1451        let buf = [0u8; 0];
1452        let mut reader = CdrReader::new(&buf);
1453        assert!(reader.read_u8().is_err());
1454        assert!(reader.read_u32().is_err());
1455    }
1456
1457    // ---- Alignment arithmetic correctness ----
1458
1459    #[kani::proof]
1460    fn cdr_alignment_no_overflow() {
1461        let offset: usize = kani::any();
1462        let alignment: usize = kani::any();
1463        kani::assume(alignment > 0 && alignment <= 8);
1464        kani::assume(offset <= 1024); // Realistic buffer size
1465        let padding = (alignment - (offset % alignment)) % alignment;
1466        let aligned = offset + padding;
1467        assert!(aligned % alignment == 0);
1468        assert!(aligned >= offset);
1469        assert!(aligned < offset + alignment);
1470    }
1471
1472    // ---- Position tracking consistency ----
1473
1474    #[kani::proof]
1475    #[kani::unwind(5)]
1476    fn cdr_writer_position_monotonic() {
1477        let mut buf = [0u8; 32];
1478        let mut writer = CdrWriter::new(&mut buf);
1479        let pos0 = writer.position();
1480
1481        let val: u8 = kani::any();
1482        if writer.write_u8(val).is_ok() {
1483            assert!(writer.position() > pos0);
1484        }
1485    }
1486
1487    #[kani::proof]
1488    #[kani::unwind(5)]
1489    fn cdr_writer_remaining_consistent() {
1490        const BUF_LEN: usize = 32;
1491        let mut buf = [0u8; BUF_LEN];
1492        let mut writer = CdrWriter::new(&mut buf);
1493        assert_eq!(writer.position() + writer.remaining(), BUF_LEN);
1494
1495        let val: u32 = kani::any();
1496        let _ = writer.write_u32(val);
1497        assert_eq!(writer.position() + writer.remaining(), BUF_LEN);
1498    }
1499}