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    ///
772    /// The alignment is taken only when there IS a first element to align:
773    /// the writer pads before each primitive it writes and never for an empty
774    /// sequence, so an unconditional `align` here consumed padding that was
775    /// never emitted and desynced any 1-byte field (or the end of the buffer)
776    /// that followed an empty `float64[]`. `len` is wire data, so the byte
777    /// length is a checked product — on a 32-bit target `u32::MAX * 8` wraps
778    /// to a length the bounds check accepts (issues 1148/1149, whose C
779    /// counterpart `rclc_cdr_borrow_le_slice_*` had both defects).
780    pub fn read_le_slice<T: LeDecode>(&mut self) -> Result<LeSliceView<'a, T>, DeserError> {
781        let len = self.read_u32()? as usize;
782        let byte_len = len.checked_mul(T::SIZE).ok_or(DeserError::InvalidData)?;
783        if len > 0 {
784            self.align(T::SIZE)?;
785        }
786        let bytes = self.read_bytes(byte_len)?;
787        Ok(LeSliceView::new(bytes))
788    }
789}
790
791/// A little-endian-decodable fixed-width numeric element of a borrowed sequence.
792///
793/// Implemented for the multi-byte CDR numeric primitives. Single-byte types
794/// (`u8`/`i8`/`bool`) do not need this — they borrow directly as `&[u8]`.
795pub trait LeDecode: Sized + Copy {
796    /// Encoded width in bytes (CDR little-endian).
797    const SIZE: usize;
798    /// Decode one element from exactly [`SIZE`](Self::SIZE) little-endian bytes.
799    fn from_le(bytes: &[u8]) -> Self;
800}
801
802macro_rules! impl_le_decode {
803    ($($t:ty),+ $(,)?) => {$(
804        impl LeDecode for $t {
805            const SIZE: usize = core::mem::size_of::<$t>();
806            #[inline]
807            fn from_le(bytes: &[u8]) -> Self {
808                let mut buf = [0u8; core::mem::size_of::<$t>()];
809                buf.copy_from_slice(bytes);
810                <$t>::from_le_bytes(buf)
811            }
812        }
813    )+};
814}
815impl_le_decode!(u16, i16, u32, i32, u64, i64, f32, f64);
816
817/// A borrowed, alignment-agnostic view over a CDR little-endian numeric
818/// sequence (RFC-0033 `borrowed` mode). Borrows the raw payload bytes
819/// zero-copy; decodes elements lazily on access, so the source buffer need not
820/// be `T`-aligned. Valid only for the borrow lifetime `'a` (the subscription
821/// callback scope).
822#[derive(Clone, Copy)]
823pub struct LeSliceView<'a, T> {
824    bytes: &'a [u8],
825    _marker: core::marker::PhantomData<fn() -> T>,
826}
827
828impl<'a, T: LeDecode> LeSliceView<'a, T> {
829    /// Wrap raw little-endian payload bytes. `bytes.len()` must be a multiple of
830    /// `T::SIZE` (guaranteed by [`CdrReader::read_le_slice`]).
831    #[inline]
832    pub fn new(bytes: &'a [u8]) -> Self {
833        Self {
834            bytes,
835            _marker: core::marker::PhantomData,
836        }
837    }
838
839    /// Number of elements in the view.
840    #[inline]
841    pub fn len(&self) -> usize {
842        self.bytes.len() / T::SIZE
843    }
844
845    /// Whether the view is empty.
846    #[inline]
847    pub fn is_empty(&self) -> bool {
848        self.bytes.is_empty()
849    }
850
851    /// The raw little-endian payload bytes (zero-copy).
852    #[inline]
853    pub fn as_bytes(&self) -> &'a [u8] {
854        self.bytes
855    }
856
857    /// Decode the element at `index`, or `None` if out of bounds.
858    #[inline]
859    pub fn get(&self, index: usize) -> Option<T> {
860        let start = index.checked_mul(T::SIZE)?;
861        let end = start.checked_add(T::SIZE)?;
862        self.bytes.get(start..end).map(T::from_le)
863    }
864
865    /// Iterate over the decoded elements.
866    #[inline]
867    pub fn iter(&self) -> impl Iterator<Item = T> + 'a {
868        let bytes = self.bytes;
869        (0..bytes.len() / T::SIZE).map(move |i| T::from_le(&bytes[i * T::SIZE..(i + 1) * T::SIZE]))
870    }
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    // ── phase-303 W2/W3 — XCDR2 (DELIMITED_CDR2 + DHEADER) ──────────────────
878
879    /// A nested-struct serialize (Header-like: `{ Time{i32 sec, u32 nanosec};
880    /// string frame_id }`) wrapped in DHEADERs under XCDR2 lays out the
881    /// encapsulation, the top DHEADER, the nested-Time DHEADER, and the members
882    /// at the canonical offsets — and round-trips through the reader.
883    /// Serialize `std_msgs/Header { builtin_interfaces/Time stamp; string
884    /// frame_id }` the way the generated code does (each struct DHEADER-wrapped),
885    /// with `stamp = {sec:7, nanosec:9}`, `frame_id = "ab"`.
886    fn serialize_header(w: &mut CdrWriter) {
887        let h = w.begin_dheader().unwrap();
888        // nested Time
889        let t = w.begin_dheader().unwrap();
890        w.write_i32(7).unwrap();
891        w.write_u32(9).unwrap();
892        w.end_dheader(t).unwrap();
893        w.write_string("ab").unwrap();
894        w.end_dheader(h).unwrap();
895    }
896
897    /// WIRE ORACLE (phase-303 #0267). Under XCDR1 the DHEADER wrap is a no-op, so
898    /// nano-ros's Header bytes are BYTE-IDENTICAL to what a real ROS 2 Jazzy node
899    /// puts ON THE WIRE by default. Captured live from `ros:jazzy-ros-base`
900    /// (2026-07-26) — a Header pub → a `raw=True` subscriber (the actual
901    /// negotiated RTPS payload, not just `serialize_message`):
902    ///   - `rmw_fastrtps_cpp` (default): exactly these 19 bytes.
903    ///   - `rmw_cyclonedds_cpp`: identical content + one trailing `00`
904    ///     alignment pad (20 bytes) — decodes the same.
905    ///
906    /// Both use encapsulation `00 01` (XCDR1), NO DHEADER: modern Jazzy STILL
907    /// defaults to XCDR1 on the wire. So nano-ros (humble, or a jazzy build
908    /// talking to a default peer) already interoperates byte-for-byte. XCDR2 +
909    /// DHEADER only appears with a non-default negotiated `data_representation`
910    /// (the #0267 domain_bridge trigger) — covered by the XCDR2 path.
911    #[test]
912    fn xcdr1_header_matches_live_jazzy_wire_bytes() {
913        let mut buf = [0u8; 64];
914        let n = {
915            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
916            serialize_header(&mut w);
917            w.position()
918        };
919        // Captured from `ros:jazzy-ros-base` rclpy serialize_message(Header):
920        //   00 01 00 00 | 07 00 00 00 | 09 00 00 00 | 03 00 00 00 | 61 62 00
921        let jazzy: [u8; 19] = [
922            0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x03, 0x00,
923            0x00, 0x00, 0x61, 0x62, 0x00,
924        ];
925        assert_eq!(
926            &buf[..n],
927            &jazzy,
928            "nano-ros XCDR1 Header must equal live Jazzy wire bytes"
929        );
930    }
931
932    /// The XCDR2 (jazzy build) Header: appendable → a DHEADER per struct. The
933    /// encapsulation flips to `00 09`, and Cyclone/a modern peer negotiating
934    /// XCDR2 reads the DHEADERs. Round-trips through the reader.
935    #[test]
936    fn xcdr2_header_has_dheaders_and_roundtrips() {
937        let mut buf = [0u8; 64];
938        let n = {
939            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
940            serialize_header(&mut w);
941            w.position()
942        };
943        assert_eq!(&buf[0..2], &[0x00, 0x09], "XCDR2 DELIMITED encapsulation");
944        // Round-trip via the auto-detecting reader.
945        let mut r = CdrReader::new_with_header(&buf[..n]).unwrap();
946        assert_eq!(r.version(), EncodingVersion::Xcdr2);
947        let h = r.begin_dheader().unwrap();
948        let t = r.begin_dheader().unwrap();
949        assert_eq!(r.read_i32().unwrap(), 7);
950        assert_eq!(r.read_u32().unwrap(), 9);
951        r.end_dheader(t).unwrap();
952        assert_eq!(r.read_string().unwrap(), "ab");
953        r.end_dheader(h).unwrap();
954    }
955
956    #[test]
957    fn xcdr2_nested_dheader_layout_and_roundtrip() {
958        let mut buf = [0u8; 64];
959        {
960            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
961            let top = w.begin_dheader().unwrap();
962            // nested Time { i32 sec; u32 nanosec }
963            let time = w.begin_dheader().unwrap();
964            w.write_i32(7).unwrap();
965            w.write_u32(9).unwrap();
966            w.end_dheader(time).unwrap();
967            // string frame_id
968            w.write_string("ab").unwrap();
969            w.end_dheader(top).unwrap();
970        }
971        // Header (encaps): 0x00 0x09 0x00 0x00.
972        assert_eq!(&buf[0..4], &[0x00, 0x09, 0x00, 0x00]);
973        // pos 4: top DHEADER (u32 LE = size of everything after it).
974        // pos 8: nested Time DHEADER = 8 (two u32).
975        assert_eq!(u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]), 8);
976        // sec @12, nanosec @16.
977        assert_eq!(u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), 7);
978        assert_eq!(u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), 9);
979
980        // Round-trip.
981        let mut r = CdrReader::new_with_header(&buf).unwrap();
982        assert_eq!(r.version(), EncodingVersion::Xcdr2);
983        let top = r.begin_dheader().unwrap();
984        let time = r.begin_dheader().unwrap();
985        assert_eq!(r.read_i32().unwrap(), 7);
986        assert_eq!(r.read_u32().unwrap(), 9);
987        r.end_dheader(time).unwrap();
988        assert_eq!(r.read_string().unwrap(), "ab");
989        r.end_dheader(top).unwrap();
990    }
991
992    /// The DHEADER calls are pure NO-OPs under XCDR1 → a struct wrapped in
993    /// begin/end_dheader emits byte-identical output to one that isn't. This is
994    /// what lets generated code wrap unconditionally with zero Humble impact.
995    #[test]
996    fn xcdr1_dheader_calls_are_byte_identical_noops() {
997        let mut a = [0u8; 32];
998        let mut b = [0u8; 32];
999        let na = {
1000            let mut w = CdrWriter::new_with_header(&mut a).unwrap();
1001            let d = w.begin_dheader().unwrap();
1002            w.write_i32(-2).unwrap();
1003            w.write_u32(3).unwrap();
1004            w.end_dheader(d).unwrap();
1005            w.position()
1006        };
1007        let nb = {
1008            let mut w = CdrWriter::new_with_header(&mut b).unwrap();
1009            w.write_i32(-2).unwrap();
1010            w.write_u32(3).unwrap();
1011            w.position()
1012        };
1013        assert_eq!(na, nb);
1014        assert_eq!(a[..na], b[..nb]);
1015    }
1016
1017    /// XCDR2 forward-compat: a reader whose type has FEWER members than the
1018    /// writer sent skips the trailing (unknown) bytes via the DHEADER size.
1019    #[test]
1020    fn xcdr2_reader_skips_unknown_trailing_members() {
1021        let mut buf = [0u8; 32];
1022        {
1023            let mut w = CdrWriter::new_with_header_xcdr2(&mut buf).unwrap();
1024            let d = w.begin_dheader().unwrap();
1025            w.write_i32(11).unwrap();
1026            // A future writer appended an extra u32 the reader doesn't know.
1027            w.write_u32(22).unwrap();
1028            w.end_dheader(d).unwrap();
1029        }
1030        let mut r = CdrReader::new_with_header(&buf).unwrap();
1031        let d = r.begin_dheader().unwrap();
1032        assert_eq!(r.read_i32().unwrap(), 11);
1033        // The reader read only its known member (i32 @ pos 12); the extra u32 is
1034        // unknown. end_dheader skips it → pos advances to the struct end (16).
1035        assert_eq!(r.position(), 12);
1036        r.end_dheader(d).unwrap();
1037        assert_eq!(
1038            r.position(),
1039            16,
1040            "end_dheader must skip the unknown trailing member"
1041        );
1042    }
1043
1044    #[test]
1045    fn test_write_read_u8() {
1046        let mut buf = [0u8; 16];
1047        let mut writer = CdrWriter::new(&mut buf);
1048        writer.write_u8(0x42).unwrap();
1049        writer.write_u8(0xFF).unwrap();
1050
1051        let mut reader = CdrReader::new(&buf);
1052        assert_eq!(reader.read_u8().unwrap(), 0x42);
1053        assert_eq!(reader.read_u8().unwrap(), 0xFF);
1054    }
1055
1056    #[test]
1057    fn le_slice_view_decodes_unaligned_f32() {
1058        // The whole point of the alignment guard (Phase 229.6): a view can sit
1059        // at an odd byte offset and still decode correctly — no `&[f32]` cast.
1060        let vals = [1.5f32, -2.25, 3.0e10, 0.0];
1061        let mut backing = [0u8; 1 + 4 * 4];
1062        backing[0] = 0xAA; // shift the f32 payload to an odd (1-byte) offset.
1063        for (i, v) in vals.iter().enumerate() {
1064            backing[1 + i * 4..1 + i * 4 + 4].copy_from_slice(&v.to_le_bytes());
1065        }
1066        let view: LeSliceView<f32> = LeSliceView::new(&backing[1..]);
1067        assert_eq!(view.len(), 4);
1068        assert!(!view.is_empty());
1069        for (i, v) in vals.iter().enumerate() {
1070            assert_eq!(view.get(i).unwrap(), *v);
1071        }
1072        assert_eq!(view.get(4), None);
1073        let collected: heapless::Vec<f32, 4> = view.iter().collect();
1074        assert_eq!(&collected[..], &vals[..]);
1075    }
1076
1077    #[test]
1078    fn read_le_slice_roundtrips_through_cdr() {
1079        // Write a `uint16[]` sequence with the CDR writer, read it back as a
1080        // borrowed `LeSliceView` — values + count must match.
1081        let vals = [10u16, 4000, 65535, 1];
1082        let mut buf = [0u8; 64];
1083        let written = {
1084            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
1085            w.write_sequence_len(vals.len()).unwrap();
1086            for v in &vals {
1087                w.write_u16(*v).unwrap();
1088            }
1089            w.position()
1090        };
1091        let mut reader = CdrReader::new_with_header(&buf[..written]).unwrap();
1092        let view = reader.read_le_slice::<u16>().unwrap();
1093        assert_eq!(view.len(), vals.len());
1094        for (i, v) in vals.iter().enumerate() {
1095            assert_eq!(view.get(i).unwrap(), *v);
1096        }
1097    }
1098
1099    #[test]
1100    fn read_le_slice_consumes_stream_padding_before_8_byte_elements() {
1101        // `uint32 a; uint32 b; float64[] xs; uint32 tail` — the count lands at
1102        // stream offset 8, so the first element sits at 16 behind 4 bytes of
1103        // padding. The view must start AT the element and leave the cursor
1104        // where `tail` is (issue 1148's C symptom: values read as garbage and
1105        // every following field misparsed).
1106        let vals = [1111.0f64, 2222.0];
1107        let mut buf = [0u8; 64];
1108        let written = {
1109            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
1110            w.write_u32(7).unwrap();
1111            w.write_u32(9).unwrap();
1112            w.write_sequence_len(vals.len()).unwrap();
1113            for v in &vals {
1114                w.write_f64(*v).unwrap();
1115            }
1116            w.write_u32(0xCAFE).unwrap();
1117            w.position()
1118        };
1119        let mut r = CdrReader::new_with_header(&buf[..written]).unwrap();
1120        assert_eq!(r.read_u32().unwrap(), 7);
1121        assert_eq!(r.read_u32().unwrap(), 9);
1122        let view = r.read_le_slice::<f64>().unwrap();
1123        assert_eq!(view.len(), 2);
1124        assert_eq!(view.get(0), Some(1111.0));
1125        assert_eq!(view.get(1), Some(2222.0));
1126        assert_eq!(r.read_u32().unwrap(), 0xCAFE);
1127        assert!(r.is_empty());
1128    }
1129
1130    #[test]
1131    fn read_le_slice_of_zero_elements_takes_no_padding() {
1132        // `float64[] xs` (empty) then `uint8 flag`. The writer emits no
1133        // padding for a sequence with no first element, so a reader that
1134        // aligns unconditionally reads `flag` four bytes late.
1135        let mut buf = [0u8; 32];
1136        let written = {
1137            let mut w = CdrWriter::new_with_header(&mut buf).unwrap();
1138            w.write_sequence_len(0).unwrap();
1139            w.write_u8(0xAB).unwrap();
1140            w.position()
1141        };
1142        let mut r = CdrReader::new_with_header(&buf[..written]).unwrap();
1143        let view = r.read_le_slice::<f64>().unwrap();
1144        assert!(view.is_empty());
1145        assert_eq!(r.read_u8().unwrap(), 0xAB);
1146        assert!(r.is_empty());
1147    }
1148
1149    #[test]
1150    fn test_write_read_u32_alignment() {
1151        let mut buf = [0u8; 16];
1152        let mut writer = CdrWriter::new(&mut buf);
1153        writer.write_u8(0x01).unwrap(); // Position 1
1154        writer.write_u32(0x12345678).unwrap(); // Should align to position 4
1155
1156        assert_eq!(writer.position(), 8); // 1 byte + 3 padding + 4 bytes
1157
1158        let mut reader = CdrReader::new(&buf);
1159        assert_eq!(reader.read_u8().unwrap(), 0x01);
1160        assert_eq!(reader.read_u32().unwrap(), 0x12345678);
1161    }
1162
1163    #[test]
1164    fn test_write_read_string() {
1165        let mut buf = [0u8; 32];
1166        let mut writer = CdrWriter::new(&mut buf);
1167        writer.write_string("Hello").unwrap();
1168
1169        let mut reader = CdrReader::new(&buf);
1170        assert_eq!(reader.read_string().unwrap(), "Hello");
1171    }
1172
1173    #[test]
1174    fn test_encapsulation_header() {
1175        let mut buf = [0u8; 32];
1176        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1177        writer.write_u32(42).unwrap();
1178
1179        assert_eq!(&buf[0..4], &CDR_LE_HEADER);
1180
1181        let mut reader = CdrReader::new_with_header(&buf).unwrap();
1182        assert_eq!(reader.read_u32().unwrap(), 42);
1183    }
1184
1185    #[test]
1186    fn test_alignment_with_header() {
1187        let mut buf = [0u8; 32];
1188        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1189        // After header (pos=4, origin=4), write u8 then u32
1190        writer.write_u8(0x01).unwrap(); // pos=5
1191        writer.write_u32(0xDEADBEEF).unwrap(); // Should align to pos=8
1192
1193        assert_eq!(writer.position(), 12); // 4 header + 1 byte + 3 padding + 4 bytes
1194
1195        let mut reader = CdrReader::new_with_header(&buf).unwrap();
1196        assert_eq!(reader.read_u8().unwrap(), 0x01);
1197        assert_eq!(reader.read_u32().unwrap(), 0xDEADBEEF);
1198    }
1199}
1200
1201// =============================================================================
1202// Ghost model validation
1203// =============================================================================
1204
1205#[cfg(test)]
1206mod ghost_checks {
1207    use super::*;
1208    use nros_ghost_types::CdrGhost;
1209
1210    /// Structural check: construct CdrGhost from CdrWriter private fields.
1211    /// If a field is renamed or retyped, this fails to compile.
1212    fn ghost_from_writer(w: &CdrWriter) -> CdrGhost {
1213        CdrGhost {
1214            buf_len: w.buf.len(),
1215            pos: w.pos,
1216            origin: w.origin,
1217        }
1218    }
1219
1220    #[test]
1221    fn ghost_new_state() {
1222        let mut buf = [0u8; 64];
1223        let writer = CdrWriter::new(&mut buf);
1224        let ghost = ghost_from_writer(&writer);
1225        assert_eq!(ghost.pos, 0);
1226        assert_eq!(ghost.origin, 0);
1227        assert_eq!(ghost.buf_len, 64);
1228    }
1229
1230    #[test]
1231    fn ghost_header_origin() {
1232        let mut buf = [0u8; 64];
1233        let writer = CdrWriter::new_with_header(&mut buf).unwrap();
1234        let ghost = ghost_from_writer(&writer);
1235        assert_eq!(ghost.pos, 4);
1236        assert_eq!(ghost.origin, 4);
1237    }
1238
1239    #[test]
1240    fn ghost_position_invariant() {
1241        let mut buf = [0u8; 64];
1242        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1243        writer.write_u32(42).unwrap();
1244        let ghost = ghost_from_writer(&writer);
1245        // After header: pos + remaining == buf_len
1246        assert_eq!(ghost.pos + writer.remaining(), ghost.buf_len);
1247    }
1248
1249    #[test]
1250    fn test_read_slice_u8() {
1251        let mut buf = [0u8; 64];
1252        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1253        // Write a uint8[] sequence: [0x10, 0x20, 0x30]
1254        writer.write_u32(3).unwrap(); // length
1255        writer.write_u8(0x10).unwrap();
1256        writer.write_u8(0x20).unwrap();
1257        writer.write_u8(0x30).unwrap();
1258        let len = writer.position();
1259
1260        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1261        let slice = reader.read_slice_u8().unwrap();
1262        assert_eq!(slice, &[0x10, 0x20, 0x30]);
1263    }
1264
1265    #[test]
1266    fn test_read_slice_u8_empty() {
1267        let mut buf = [0u8; 64];
1268        let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1269        writer.write_u32(0).unwrap(); // length = 0
1270        let len = writer.position();
1271
1272        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1273        let slice = reader.read_slice_u8().unwrap();
1274        assert!(slice.is_empty());
1275    }
1276}
1277
1278// =============================================================================
1279// Kani bounded model checking proofs
1280// =============================================================================
1281
1282#[cfg(kani)]
1283mod verification {
1284    use super::*;
1285
1286    // ---- Primitive write/read panic-freedom ----
1287
1288    #[kani::proof]
1289    #[kani::unwind(5)]
1290    fn cdr_write_u8_no_panic() {
1291        let mut buf = [0u8; 8];
1292        let mut writer = CdrWriter::new(&mut buf);
1293        let val: u8 = kani::any();
1294        let _ = writer.write_u8(val);
1295    }
1296
1297    #[kani::proof]
1298    #[kani::unwind(5)]
1299    fn cdr_write_bool_no_panic() {
1300        let mut buf = [0u8; 8];
1301        let mut writer = CdrWriter::new(&mut buf);
1302        let val: bool = kani::any();
1303        let _ = writer.write_bool(val);
1304    }
1305
1306    #[kani::proof]
1307    #[kani::unwind(5)]
1308    fn cdr_write_i16_no_panic() {
1309        let mut buf = [0u8; 16];
1310        let mut writer = CdrWriter::new(&mut buf);
1311        let val: i16 = kani::any();
1312        let _ = writer.write_i16(val);
1313    }
1314
1315    #[kani::proof]
1316    #[kani::unwind(5)]
1317    fn cdr_write_i32_no_panic() {
1318        let mut buf = [0u8; 16];
1319        let mut writer = CdrWriter::new(&mut buf);
1320        let val: i32 = kani::any();
1321        let _ = writer.write_i32(val);
1322    }
1323
1324    #[kani::proof]
1325    #[kani::unwind(5)]
1326    fn cdr_write_i64_no_panic() {
1327        let mut buf = [0u8; 16];
1328        let mut writer = CdrWriter::new(&mut buf);
1329        let val: i64 = kani::any();
1330        let _ = writer.write_i64(val);
1331    }
1332
1333    #[kani::proof]
1334    #[kani::unwind(5)]
1335    fn cdr_write_f32_no_panic() {
1336        let mut buf = [0u8; 16];
1337        let mut writer = CdrWriter::new(&mut buf);
1338        let val: f32 = kani::any();
1339        let _ = writer.write_f32(val);
1340    }
1341
1342    #[kani::proof]
1343    #[kani::unwind(5)]
1344    fn cdr_write_f64_no_panic() {
1345        let mut buf = [0u8; 16];
1346        let mut writer = CdrWriter::new(&mut buf);
1347        let val: f64 = kani::any();
1348        let _ = writer.write_f64(val);
1349    }
1350
1351    // ---- Round-trip correctness: write then read produces the same value ----
1352
1353    #[kani::proof]
1354    #[kani::unwind(5)]
1355    fn cdr_roundtrip_u8() {
1356        let mut buf = [0u8; 8];
1357        let val: u8 = kani::any();
1358        let len = {
1359            let mut writer = CdrWriter::new(&mut buf);
1360            writer.write_u8(val).unwrap();
1361            writer.position()
1362        };
1363        let mut reader = CdrReader::new(&buf[..len]);
1364        assert_eq!(reader.read_u8().unwrap(), val);
1365    }
1366
1367    #[kani::proof]
1368    #[kani::unwind(5)]
1369    fn cdr_roundtrip_bool() {
1370        let mut buf = [0u8; 8];
1371        let val: bool = kani::any();
1372        let len = {
1373            let mut writer = CdrWriter::new(&mut buf);
1374            writer.write_bool(val).unwrap();
1375            writer.position()
1376        };
1377        let mut reader = CdrReader::new(&buf[..len]);
1378        assert_eq!(reader.read_bool().unwrap(), val);
1379    }
1380
1381    #[kani::proof]
1382    #[kani::unwind(5)]
1383    fn cdr_roundtrip_i16() {
1384        let mut buf = [0u8; 16];
1385        let val: i16 = kani::any();
1386        let len = {
1387            let mut writer = CdrWriter::new(&mut buf);
1388            writer.write_i16(val).unwrap();
1389            writer.position()
1390        };
1391        let mut reader = CdrReader::new(&buf[..len]);
1392        assert_eq!(reader.read_i16().unwrap(), val);
1393    }
1394
1395    #[kani::proof]
1396    #[kani::unwind(5)]
1397    fn cdr_roundtrip_i32() {
1398        let mut buf = [0u8; 16];
1399        let val: i32 = kani::any();
1400        let len = {
1401            let mut writer = CdrWriter::new(&mut buf);
1402            writer.write_i32(val).unwrap();
1403            writer.position()
1404        };
1405        let mut reader = CdrReader::new(&buf[..len]);
1406        assert_eq!(reader.read_i32().unwrap(), val);
1407    }
1408
1409    #[kani::proof]
1410    #[kani::unwind(5)]
1411    fn cdr_roundtrip_i64() {
1412        let mut buf = [0u8; 16];
1413        let val: i64 = kani::any();
1414        let len = {
1415            let mut writer = CdrWriter::new(&mut buf);
1416            writer.write_i64(val).unwrap();
1417            writer.position()
1418        };
1419        let mut reader = CdrReader::new(&buf[..len]);
1420        assert_eq!(reader.read_i64().unwrap(), val);
1421    }
1422
1423    #[kani::proof]
1424    #[kani::unwind(5)]
1425    fn cdr_roundtrip_f32() {
1426        let mut buf = [0u8; 16];
1427        let val: f32 = kani::any();
1428        let len = {
1429            let mut writer = CdrWriter::new(&mut buf);
1430            writer.write_f32(val).unwrap();
1431            writer.position()
1432        };
1433        let mut reader = CdrReader::new(&buf[..len]);
1434        let result = reader.read_f32().unwrap();
1435        assert_eq!(val.to_bits(), result.to_bits());
1436    }
1437
1438    #[kani::proof]
1439    #[kani::unwind(5)]
1440    fn cdr_roundtrip_f64() {
1441        let mut buf = [0u8; 16];
1442        let val: f64 = kani::any();
1443        let len = {
1444            let mut writer = CdrWriter::new(&mut buf);
1445            writer.write_f64(val).unwrap();
1446            writer.position()
1447        };
1448        let mut reader = CdrReader::new(&buf[..len]);
1449        let result = reader.read_f64().unwrap();
1450        assert_eq!(val.to_bits(), result.to_bits());
1451    }
1452
1453    // ---- CDR header round-trip ----
1454
1455    #[kani::proof]
1456    #[kani::unwind(5)]
1457    fn cdr_roundtrip_with_header_i32() {
1458        let mut buf = [0u8; 16];
1459        let val: i32 = kani::any();
1460        let len = {
1461            let mut writer = CdrWriter::new_with_header(&mut buf).unwrap();
1462            writer.write_i32(val).unwrap();
1463            writer.position()
1464        };
1465        let mut reader = CdrReader::new_with_header(&buf[..len]).unwrap();
1466        assert_eq!(reader.read_i32().unwrap(), val);
1467    }
1468
1469    // ---- Buffer exhaustion returns Err, never panics ----
1470
1471    #[kani::proof]
1472    #[kani::unwind(5)]
1473    fn cdr_write_buffer_exhaustion_u32() {
1474        let mut buf = [0u8; 3]; // Too small for u32
1475        let mut writer = CdrWriter::new(&mut buf);
1476        let val: u32 = kani::any();
1477        let result = writer.write_u32(val);
1478        assert!(result.is_err());
1479    }
1480
1481    #[kani::proof]
1482    #[kani::unwind(5)]
1483    fn cdr_write_header_buffer_too_small() {
1484        let mut buf = [0u8; 3]; // Too small for 4-byte header
1485        let result = CdrWriter::new_with_header(&mut buf);
1486        assert!(result.is_err());
1487    }
1488
1489    // ---- Deserialization of arbitrary bytes: Ok or Err, never panic ----
1490
1491    #[kani::proof]
1492    #[kani::unwind(5)]
1493    fn cdr_deserialize_arbitrary_bytes_i32() {
1494        let mut buf = [0u8; 8];
1495        buf[0] = kani::any();
1496        buf[1] = kani::any();
1497        buf[2] = kani::any();
1498        buf[3] = kani::any();
1499        buf[4] = kani::any();
1500        buf[5] = kani::any();
1501        buf[6] = kani::any();
1502        buf[7] = kani::any();
1503        let result = CdrReader::new_with_header(&buf);
1504        if let Ok(mut reader) = result {
1505            let _ = reader.read_i32(); // Ok or Err, not panic
1506        }
1507    }
1508
1509    #[kani::proof]
1510    #[kani::unwind(5)]
1511    fn cdr_deserialize_empty_buffer() {
1512        let buf = [0u8; 0];
1513        let mut reader = CdrReader::new(&buf);
1514        assert!(reader.read_u8().is_err());
1515        assert!(reader.read_u32().is_err());
1516    }
1517
1518    // ---- Alignment arithmetic correctness ----
1519
1520    #[kani::proof]
1521    fn cdr_alignment_no_overflow() {
1522        let offset: usize = kani::any();
1523        let alignment: usize = kani::any();
1524        kani::assume(alignment > 0 && alignment <= 8);
1525        kani::assume(offset <= 1024); // Realistic buffer size
1526        let padding = (alignment - (offset % alignment)) % alignment;
1527        let aligned = offset + padding;
1528        assert!(aligned % alignment == 0);
1529        assert!(aligned >= offset);
1530        assert!(aligned < offset + alignment);
1531    }
1532
1533    // ---- Position tracking consistency ----
1534
1535    #[kani::proof]
1536    #[kani::unwind(5)]
1537    fn cdr_writer_position_monotonic() {
1538        let mut buf = [0u8; 32];
1539        let mut writer = CdrWriter::new(&mut buf);
1540        let pos0 = writer.position();
1541
1542        let val: u8 = kani::any();
1543        if writer.write_u8(val).is_ok() {
1544            assert!(writer.position() > pos0);
1545        }
1546    }
1547
1548    #[kani::proof]
1549    #[kani::unwind(5)]
1550    fn cdr_writer_remaining_consistent() {
1551        const BUF_LEN: usize = 32;
1552        let mut buf = [0u8; BUF_LEN];
1553        let mut writer = CdrWriter::new(&mut buf);
1554        assert_eq!(writer.position() + writer.remaining(), BUF_LEN);
1555
1556        let val: u32 = kani::any();
1557        let _ = writer.write_u32(val);
1558        assert_eq!(writer.position() + writer.remaining(), BUF_LEN);
1559    }
1560}