Skip to main content

zerodds_cdr/
buffer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Alignment-tracking buffer reader/writer for XCDR.
4//!
5//! XCDR data is alignment-sensitive: a `u32` field must start at a
6//! 4-byte boundary, a `u64` at an 8-byte boundary, etc. The encoder
7//! inserts the necessary padding bytes (value 0) before each write;
8//! the decoder skips them.
9//!
10//! Alignment is computed relative to the **stream start** (offset 0),
11//! not relative to the current position inside a nested member. This
12//! matches OMG XTypes §7.4.1: "All elements are aligned to a multiple
13//! of their alignment requirement, relative to the beginning of the
14//! encapsulation".
15//!
16//! Deliberate architectural choice: only the dynamic Vec-based writer
17//! (alloc feature). A slice-based writer for no_std-without-alloc is
18//! not implemented — `alloc` is a transitive mandatory dependency via
19//! `zerodds-foundation` anyway.
20
21#[cfg(feature = "alloc")]
22extern crate alloc;
23#[cfg(feature = "alloc")]
24use alloc::vec::Vec;
25
26use crate::endianness::Endianness;
27use crate::error::DecodeError;
28#[cfg(feature = "alloc")]
29use crate::error::EncodeError;
30
31// ============================================================================
32// BufferWriter
33// ============================================================================
34
35/// Write buffer with alignment tracking and configurable endianness.
36///
37/// Phase 0 uses `Vec<u8>` as backing — a growing alloc feature.
38#[cfg(feature = "alloc")]
39#[derive(Debug, Clone)]
40pub struct BufferWriter {
41    bytes: Vec<u8>,
42    endianness: Endianness,
43    /// Maximum alignment that [`Self::align`] caps at. XCDR1
44    /// (PLAIN_CDR) = 8 (default), XCDR2 (PLAIN_CDR2) = 4.
45    /// OMG XTypes 1.3 §7.4.1.1.1: XCDR2 limits alignment to
46    /// `min(sizeof, 4)` — 64-bit primitives (double/i64/u64) are aligned
47    /// to **4**, NOT 8. Wrong alignment breaks byte-wire interop with
48    /// Cyclone/RTI/FastDDS (whose XCDR2 caps correctly).
49    max_alignment: usize,
50    /// Virtual alignment origin: [`Self::align`] aligns to
51    /// `(position + align_origin)` instead of just `position`. Default 0.
52    ///
53    /// Needed for GIOP 1.2 (CORBA §15.4.2/§15.4.4): the request/reply body
54    /// is encoded into a sub-stream that is assembled **separately** from
55    /// the 12-byte GIOP header — but the CDR alignment origin is the
56    /// message start (byte 0 of the header). `align_origin = 12` restores
57    /// continuous alignment, otherwise the unconditionally 8-aligned GIOP
58    /// 1.2 body lands at absolute offset ≡4 mod 8 → interop break with
59    /// omniORB/TAO. Affects only 8-byte alignments; ≤4-byte stays identical
60    /// (12 is divisible by 4), hence no effect on XCDR2/RTPS (origin 0).
61    align_origin: usize,
62}
63
64/// Default max alignment for XCDR1 (PLAIN_CDR): 64-bit to 8.
65const XCDR1_MAX_ALIGNMENT: usize = 8;
66/// Max alignment for XCDR2 (PLAIN_CDR2): 64-bit to 4 (§7.4.1.1.1).
67pub(crate) const XCDR2_MAX_ALIGNMENT: usize = 4;
68
69#[cfg(feature = "alloc")]
70impl BufferWriter {
71    /// Creates an empty writer with the given endianness
72    /// (XCDR1 alignment, cap 8 — default + backward-compat).
73    #[must_use]
74    pub fn new(endianness: Endianness) -> Self {
75        Self {
76            bytes: Vec::new(),
77            endianness,
78            max_alignment: XCDR1_MAX_ALIGNMENT,
79            align_origin: 0,
80        }
81    }
82
83    /// Creates a writer with pre-allocated capacity (XCDR1 cap 8).
84    #[must_use]
85    pub fn with_capacity(endianness: Endianness, cap: usize) -> Self {
86        Self {
87            bytes: Vec::with_capacity(cap),
88            endianness,
89            max_alignment: XCDR1_MAX_ALIGNMENT,
90            align_origin: 0,
91        }
92    }
93
94    /// Builder: sets the virtual [`Self::align`] origin (default 0).
95    /// For GIOP 1.2 = `12` (header size), see the `align_origin` field doc.
96    #[must_use]
97    pub fn with_align_origin(mut self, origin: usize) -> Self {
98        self.align_origin = origin;
99        self
100    }
101
102    /// Builder: sets the alignment cap (XCDR1=8, XCDR2=4). Inherited by
103    /// nested member writers (see `struct_enc`).
104    #[must_use]
105    pub fn with_max_alignment(mut self, max_alignment: usize) -> Self {
106        debug_assert!(
107            max_alignment.is_power_of_two(),
108            "max_alignment must be a power of two"
109        );
110        self.max_alignment = max_alignment;
111        self
112    }
113
114    /// Builder: switches to XCDR2 alignment (cap 4, §7.4.1.1.1).
115    #[must_use]
116    pub fn xcdr2(self) -> Self {
117        self.with_max_alignment(XCDR2_MAX_ALIGNMENT)
118    }
119
120    /// The current alignment cap (8 = XCDR1, 4 = XCDR2).
121    #[must_use]
122    pub fn max_alignment(&self) -> usize {
123        self.max_alignment
124    }
125
126    /// Returns the current endianness.
127    #[must_use]
128    pub fn endianness(&self) -> Endianness {
129        self.endianness
130    }
131
132    /// Current position (== number of bytes written so far).
133    #[must_use]
134    pub fn position(&self) -> usize {
135        self.bytes.len()
136    }
137
138    /// Consumes the writer and returns the written buffer.
139    #[must_use]
140    pub fn into_bytes(self) -> Vec<u8> {
141        self.bytes
142    }
143
144    /// Read-only view of the buffer written so far.
145    #[must_use]
146    pub fn as_bytes(&self) -> &[u8] {
147        &self.bytes
148    }
149
150    /// Inserts null padding until the current position is a multiple
151    /// of `alignment`. Alignment must be a power of two
152    /// (1, 2, 4, 8); other values are undefined in XCDR.
153    pub fn align(&mut self, alignment: usize) {
154        debug_assert!(
155            alignment.is_power_of_two(),
156            "alignment must be a power of two"
157        );
158        // XCDR2 (§7.4.1.1.1) caps the alignment at `max_alignment` (4):
159        // a u64/double aligns to 4, not 8. XCDR1 has cap 8
160        // (= no-op for all XCDR types).
161        let effective = alignment.min(self.max_alignment);
162        let pos = self.bytes.len() + self.align_origin;
163        let pad = padding_for(pos, effective);
164        for _ in 0..pad {
165            self.bytes.push(0);
166        }
167    }
168
169    /// Writes raw bytes without alignment.
170    ///
171    /// # Errors
172    /// Never returns an error with `Vec`-based backing — the signature
173    /// is symmetric to the slice-based writer (Phase 1).
174    pub fn write_bytes(&mut self, data: &[u8]) -> Result<(), EncodeError> {
175        self.bytes.extend_from_slice(data);
176        Ok(())
177    }
178
179    /// Writes a single byte (1-byte alignment).
180    ///
181    /// # Errors
182    /// As `write_bytes`.
183    pub fn write_u8(&mut self, value: u8) -> Result<(), EncodeError> {
184        self.bytes.push(value);
185        Ok(())
186    }
187
188    /// Aligns + writes `u16`.
189    ///
190    /// # Errors
191    /// As `write_bytes`.
192    pub fn write_u16(&mut self, value: u16) -> Result<(), EncodeError> {
193        self.align(2);
194        self.write_bytes(&self.endianness.write_u16(value))
195    }
196
197    /// Aligns + writes `u32`.
198    ///
199    /// # Errors
200    /// As `write_bytes`.
201    pub fn write_u32(&mut self, value: u32) -> Result<(), EncodeError> {
202        self.align(4);
203        self.write_bytes(&self.endianness.write_u32(value))
204    }
205
206    /// Aligns + writes `u64`.
207    ///
208    /// # Errors
209    /// As `write_bytes`.
210    pub fn write_u64(&mut self, value: u64) -> Result<(), EncodeError> {
211        self.align(8);
212        self.write_bytes(&self.endianness.write_u64(value))
213    }
214
215    /// Writes a CDR string (§9.3.2.7): 4-byte length (incl.
216    /// null terminator) + UTF-8 bytes + 1 null byte. No trailing
217    /// padding — the next field aligns itself.
218    ///
219    /// # Errors
220    /// As `write_bytes`.
221    pub fn write_string(&mut self, s: &str) -> Result<(), EncodeError> {
222        let bytes = s.as_bytes();
223        // Length = s.len() + 1 for the null terminator
224        let len = u32::try_from(bytes.len().saturating_add(1)).map_err(|_| {
225            EncodeError::ValueOutOfRange {
226                message: "CDR string length exceeds u32::MAX",
227            }
228        })?;
229        self.write_u32(len)?;
230        self.write_bytes(bytes)?;
231        self.write_u8(0)
232    }
233}
234
235// ============================================================================
236// BufferReader
237// ============================================================================
238
239/// Read buffer with alignment tracking.
240#[derive(Debug, Clone)]
241pub struct BufferReader<'a> {
242    bytes: &'a [u8],
243    pos: usize,
244    endianness: Endianness,
245    /// Alignment cap analogous to [`BufferWriter`] — MUST match the
246    /// encode side, otherwise the reader skips wrong padding. XCDR1=8,
247    /// XCDR2=4.
248    max_alignment: usize,
249    /// Virtual alignment origin, mirroring [`BufferWriter`]:
250    /// [`Self::align`] skips padding until `(pos + align_origin)` is
251    /// aligned. GIOP 1.2 body = 12, otherwise 0. See
252    /// `BufferWriter::align_origin`.
253    align_origin: usize,
254}
255
256impl<'a> BufferReader<'a> {
257    /// Constructs a reader over the given slice (XCDR1 cap 8).
258    #[must_use]
259    pub fn new(bytes: &'a [u8], endianness: Endianness) -> Self {
260        Self {
261            bytes,
262            pos: 0,
263            endianness,
264            max_alignment: XCDR1_MAX_ALIGNMENT,
265            align_origin: 0,
266        }
267    }
268
269    /// Builder: sets the virtual [`Self::align`] origin (default 0).
270    /// For GIOP 1.2 = `12` (header size), MUST match the encode side.
271    #[must_use]
272    pub fn with_align_origin(mut self, origin: usize) -> Self {
273        self.align_origin = origin;
274        self
275    }
276
277    /// Builder: sets the alignment cap (XCDR1=8, XCDR2=4). Inherited by
278    /// nested member readers.
279    #[must_use]
280    pub fn with_max_alignment(mut self, max_alignment: usize) -> Self {
281        debug_assert!(
282            max_alignment.is_power_of_two(),
283            "max_alignment must be a power of two"
284        );
285        self.max_alignment = max_alignment;
286        self
287    }
288
289    /// Builder: switches to XCDR2 alignment (cap 4, §7.4.1.1.1).
290    #[must_use]
291    pub fn xcdr2(self) -> Self {
292        self.with_max_alignment(XCDR2_MAX_ALIGNMENT)
293    }
294
295    /// The current alignment cap (8 = XCDR1, 4 = XCDR2).
296    #[must_use]
297    pub fn max_alignment(&self) -> usize {
298        self.max_alignment
299    }
300
301    /// Current endianness.
302    #[must_use]
303    pub fn endianness(&self) -> Endianness {
304        self.endianness
305    }
306
307    /// Current position in the stream.
308    #[must_use]
309    pub fn position(&self) -> usize {
310        self.pos
311    }
312
313    /// Remaining bytes until the end.
314    #[must_use]
315    pub fn remaining(&self) -> usize {
316        self.bytes.len().saturating_sub(self.pos)
317    }
318
319    /// Skips padding until the next `alignment` boundary.
320    ///
321    /// # Errors
322    /// `UnexpectedEof` when not enough bytes remain for the padding.
323    pub fn align(&mut self, alignment: usize) -> Result<(), DecodeError> {
324        debug_assert!(
325            alignment.is_power_of_two(),
326            "alignment must be power of two"
327        );
328        // XCDR2 cap (§7.4.1.1.1), mirroring the BufferWriter.
329        let alignment = alignment.min(self.max_alignment);
330        let pad = padding_for(self.pos + self.align_origin, alignment);
331        if self.remaining() < pad {
332            return Err(DecodeError::UnexpectedEof {
333                needed: pad,
334                offset: self.pos,
335            });
336        }
337        self.pos += pad;
338        Ok(())
339    }
340
341    /// Reads exactly `n` bytes as a slice (without alignment).
342    ///
343    /// # Errors
344    /// `UnexpectedEof`.
345    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], DecodeError> {
346        if self.remaining() < n {
347            return Err(DecodeError::UnexpectedEof {
348                needed: n,
349                offset: self.pos,
350            });
351        }
352        let slice = &self.bytes[self.pos..self.pos + n];
353        self.pos += n;
354        Ok(slice)
355    }
356
357    /// Reads a single byte.
358    ///
359    /// # Errors
360    /// `UnexpectedEof`.
361    pub fn read_u8(&mut self) -> Result<u8, DecodeError> {
362        let slice = self.read_bytes(1)?;
363        Ok(slice[0])
364    }
365
366    /// Aligns + reads `u16`.
367    ///
368    /// # Errors
369    /// `UnexpectedEof`.
370    pub fn read_u16(&mut self) -> Result<u16, DecodeError> {
371        self.align(2)?;
372        let slice = self.read_bytes(2)?;
373        let mut buf = [0u8; 2];
374        buf.copy_from_slice(slice);
375        Ok(self.endianness.read_u16(buf))
376    }
377
378    /// Aligns + reads `u32`.
379    ///
380    /// # Errors
381    /// `UnexpectedEof`.
382    pub fn read_u32(&mut self) -> Result<u32, DecodeError> {
383        self.align(4)?;
384        let slice = self.read_bytes(4)?;
385        let mut buf = [0u8; 4];
386        buf.copy_from_slice(slice);
387        Ok(self.endianness.read_u32(buf))
388    }
389
390    /// Aligns + reads `u64`.
391    ///
392    /// # Errors
393    /// `UnexpectedEof`.
394    pub fn read_u64(&mut self) -> Result<u64, DecodeError> {
395        self.align(8)?;
396        let slice = self.read_bytes(8)?;
397        let mut buf = [0u8; 8];
398        buf.copy_from_slice(slice);
399        Ok(self.endianness.read_u64(buf))
400    }
401
402    /// Reads a CDR string (§9.3.2.7): 4-byte length (incl. null
403    /// terminator) + UTF-8 bytes + 1 null byte. Returns the string
404    /// **without** the terminator.
405    ///
406    /// # Errors
407    /// `UnexpectedEof` for too-short data; `InvalidData` for non-UTF-8
408    /// bytes, a missing null terminator, or `length == 0`.
409    #[cfg(feature = "alloc")]
410    pub fn read_string(&mut self) -> Result<alloc::string::String, DecodeError> {
411        use alloc::string::String;
412        let start = self.pos;
413        let len = self.read_u32()? as usize;
414        if len == 0 {
415            return Err(DecodeError::InvalidString {
416                offset: start,
417                reason: "length must be > 0 (null terminator required)",
418            });
419        }
420        let raw = self.read_bytes(len)?;
421        // last byte must be null terminator
422        if raw[len - 1] != 0 {
423            return Err(DecodeError::InvalidString {
424                offset: start,
425                reason: "missing null terminator",
426            });
427        }
428        String::from_utf8(raw[..len - 1].to_vec())
429            .map_err(|_| DecodeError::InvalidUtf8 { offset: start + 4 })
430    }
431}
432
433// ============================================================================
434// Helpers
435// ============================================================================
436
437/// Computes the number of padding bytes to push `pos` to the next
438/// multiple-of-`alignment` boundary.
439#[must_use]
440pub fn padding_for(pos: usize, alignment: usize) -> usize {
441    let mask = alignment - 1;
442    (alignment - (pos & mask)) & mask
443}
444
445#[cfg(test)]
446mod tests {
447    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
448    use super::*;
449
450    #[cfg(feature = "alloc")]
451    extern crate alloc;
452    #[cfg(feature = "alloc")]
453    use alloc::vec;
454
455    #[test]
456    fn padding_for_zero_position_is_zero() {
457        assert_eq!(padding_for(0, 4), 0);
458        assert_eq!(padding_for(0, 8), 0);
459    }
460
461    #[cfg(feature = "alloc")]
462    #[test]
463    fn xcdr2_caps_u64_alignment_at_4_not_8() {
464        // OMG XTypes 1.3 §7.4.1.1.1: XCDR2 caps alignment at 4 —
465        // a u64 after a u8 aligns to offset 4 (3 pad), NOT 8.
466        // Cyclone/RTI/FastDDS do it this way; without the cap the
467        // byte-wire comparison breaks (capture V-3/V-8).
468        let mut w = BufferWriter::new(Endianness::Little).xcdr2();
469        w.write_u8(1).unwrap();
470        w.write_u64(0x4242_4242_4242_4242).unwrap();
471        assert_eq!(w.position(), 4 + 8, "u64 at offset 4, not 8");
472        assert_eq!(&w.as_bytes()[1..4], &[0, 0, 0], "exactly 3 pad bytes");
473
474        // Reader mirrored: u8 then u64 @ offset 4.
475        let bytes = w.into_bytes();
476        let mut r = BufferReader::new(&bytes, Endianness::Little).xcdr2();
477        assert_eq!(r.read_u8().unwrap(), 1);
478        assert_eq!(r.read_u64().unwrap(), 0x4242_4242_4242_4242);
479        assert_eq!(r.position(), 12);
480    }
481
482    #[cfg(feature = "alloc")]
483    #[test]
484    fn xcdr1_default_keeps_u64_alignment_at_8() {
485        // Default (XCDR1, PLAIN_CDR): u64 still 8-aligned —
486        // backward-compat, no regression for the XCDR1 wire.
487        let mut w = BufferWriter::new(Endianness::Little);
488        w.write_u8(1).unwrap();
489        w.write_u64(7).unwrap();
490        assert_eq!(w.position(), 8 + 8, "XCDR1: u64 at offset 8");
491    }
492
493    #[test]
494    fn padding_for_already_aligned_is_zero() {
495        assert_eq!(padding_for(8, 4), 0);
496        assert_eq!(padding_for(16, 8), 0);
497    }
498
499    #[test]
500    fn padding_for_one_byte_to_4_align_is_three() {
501        assert_eq!(padding_for(1, 4), 3);
502    }
503
504    #[test]
505    fn padding_for_three_bytes_to_8_align_is_five() {
506        assert_eq!(padding_for(3, 8), 5);
507    }
508
509    #[cfg(feature = "alloc")]
510    #[test]
511    fn writer_writes_u8_without_padding() {
512        let mut w = BufferWriter::new(Endianness::Little);
513        w.write_u8(0xAB).unwrap();
514        assert_eq!(w.as_bytes(), &[0xAB]);
515        assert_eq!(w.position(), 1);
516    }
517
518    #[cfg(feature = "alloc")]
519    #[test]
520    fn writer_aligns_u32_after_u8() {
521        let mut w = BufferWriter::new(Endianness::Little);
522        w.write_u8(0xAB).unwrap();
523        w.write_u32(0xDEAD_BEEF).unwrap();
524        // 1 byte 0xAB + 3 bytes padding + 4 bytes LE u32
525        assert_eq!(w.as_bytes(), &[0xAB, 0, 0, 0, 0xEF, 0xBE, 0xAD, 0xDE]);
526    }
527
528    #[cfg(feature = "alloc")]
529    #[test]
530    fn writer_aligns_u64_after_u8() {
531        let mut w = BufferWriter::new(Endianness::Big);
532        w.write_u8(0x01).unwrap();
533        w.write_u64(0x0203_0405_0607_0809).unwrap();
534        // 1 byte + 7 bytes padding + 8 bytes BE u64
535        assert_eq!(
536            w.as_bytes(),
537            &[0x01, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9]
538        );
539    }
540
541    #[cfg(feature = "alloc")]
542    #[test]
543    fn writer_with_capacity_preserves_endianness() {
544        let w = BufferWriter::with_capacity(Endianness::Big, 64);
545        assert_eq!(w.endianness(), Endianness::Big);
546        assert_eq!(w.position(), 0);
547    }
548
549    #[cfg(feature = "alloc")]
550    #[test]
551    fn writer_into_bytes_returns_full_buffer() {
552        let mut w = BufferWriter::new(Endianness::Little);
553        w.write_u32(0xCAFE_BABE).unwrap();
554        let bytes = w.into_bytes();
555        assert_eq!(bytes, vec![0xBE, 0xBA, 0xFE, 0xCA]);
556    }
557
558    #[test]
559    fn reader_reads_u8() {
560        let bytes = [0xAB, 0xCD];
561        let mut r = BufferReader::new(&bytes, Endianness::Little);
562        assert_eq!(r.read_u8().unwrap(), 0xAB);
563        assert_eq!(r.position(), 1);
564    }
565
566    #[test]
567    fn reader_aligns_before_u32() {
568        let bytes = [0xAB, 0, 0, 0, 0xEF, 0xBE, 0xAD, 0xDE];
569        let mut r = BufferReader::new(&bytes, Endianness::Little);
570        assert_eq!(r.read_u8().unwrap(), 0xAB);
571        assert_eq!(r.read_u32().unwrap(), 0xDEAD_BEEF);
572        assert_eq!(r.remaining(), 0);
573    }
574
575    #[test]
576    fn reader_aligns_before_u64_be() {
577        let bytes = [0x01, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9];
578        let mut r = BufferReader::new(&bytes, Endianness::Big);
579        assert_eq!(r.read_u8().unwrap(), 0x01);
580        assert_eq!(r.read_u64().unwrap(), 0x0203_0405_0607_0809);
581    }
582
583    #[test]
584    fn reader_unexpected_eof_on_short_read() {
585        let bytes = [0u8; 2];
586        let mut r = BufferReader::new(&bytes, Endianness::Little);
587        let res = r.read_u32();
588        // u32 requires 4 bytes from position 0; only 2 available.
589        match res {
590            Err(DecodeError::UnexpectedEof {
591                needed: 4,
592                offset: 0,
593            }) => {}
594            other => panic!("expected UnexpectedEof, got {other:?}"),
595        }
596    }
597
598    #[test]
599    fn reader_eof_on_align_overflow() {
600        let bytes = [0u8; 1];
601        let mut r = BufferReader::new(&bytes, Endianness::Little);
602        let _ = r.read_u8().unwrap();
603        let res = r.align(8);
604        assert!(matches!(res, Err(DecodeError::UnexpectedEof { .. })));
605    }
606
607    #[cfg(feature = "alloc")]
608    #[test]
609    fn writer_reader_roundtrip_mixed_primitives() {
610        let mut w = BufferWriter::new(Endianness::Little);
611        w.write_u8(1).unwrap();
612        w.write_u16(0x1234).unwrap();
613        w.write_u32(0x5678_9ABC).unwrap();
614        w.write_u64(0x0102_0304_0506_0708).unwrap();
615        let bytes = w.into_bytes();
616        let mut r = BufferReader::new(&bytes, Endianness::Little);
617        assert_eq!(r.read_u8().unwrap(), 1);
618        assert_eq!(r.read_u16().unwrap(), 0x1234);
619        assert_eq!(r.read_u32().unwrap(), 0x5678_9ABC);
620        assert_eq!(r.read_u64().unwrap(), 0x0102_0304_0506_0708);
621        assert_eq!(r.remaining(), 0);
622    }
623
624    #[cfg(feature = "alloc")]
625    #[test]
626    fn write_read_string_roundtrip_ascii() {
627        let mut w = BufferWriter::new(Endianness::Little);
628        w.write_string("ChatterTopic").unwrap();
629        let bytes = w.into_bytes();
630        // length=13 (12 chars + null), 12 bytes ascii, 1 null
631        assert_eq!(&bytes[0..4], &[13, 0, 0, 0]);
632        assert_eq!(&bytes[4..16], b"ChatterTopic");
633        assert_eq!(bytes[16], 0);
634        let mut r = BufferReader::new(&bytes, Endianness::Little);
635        assert_eq!(r.read_string().unwrap(), "ChatterTopic");
636    }
637
638    #[cfg(feature = "alloc")]
639    #[test]
640    fn write_read_string_empty() {
641        // "": length=1 (just null), 0 chars, 1 null
642        let mut w = BufferWriter::new(Endianness::Little);
643        w.write_string("").unwrap();
644        let bytes = w.into_bytes();
645        assert_eq!(&bytes[..], &[1, 0, 0, 0, 0]);
646        let mut r = BufferReader::new(&bytes, Endianness::Little);
647        assert_eq!(r.read_string().unwrap(), "");
648    }
649
650    #[cfg(feature = "alloc")]
651    #[test]
652    fn write_read_string_utf8_multibyte() {
653        let mut w = BufferWriter::new(Endianness::Little);
654        w.write_string("Zähler").unwrap(); // ä = 2 bytes UTF-8
655        let bytes = w.into_bytes();
656        let mut r = BufferReader::new(&bytes, Endianness::Little);
657        assert_eq!(r.read_string().unwrap(), "Zähler");
658    }
659
660    #[cfg(feature = "alloc")]
661    #[test]
662    fn read_string_rejects_length_zero() {
663        let bytes = [0u8, 0, 0, 0];
664        let mut r = BufferReader::new(&bytes, Endianness::Little);
665        assert!(matches!(
666            r.read_string(),
667            Err(DecodeError::InvalidString { .. })
668        ));
669    }
670
671    #[cfg(feature = "alloc")]
672    #[test]
673    fn read_string_rejects_missing_null_terminator() {
674        // length=4, but only ASCII without null terminator
675        let bytes = [4u8, 0, 0, 0, b'A', b'B', b'C', b'D'];
676        let mut r = BufferReader::new(&bytes, Endianness::Little);
677        assert!(matches!(
678            r.read_string(),
679            Err(DecodeError::InvalidString { .. })
680        ));
681    }
682
683    #[cfg(feature = "alloc")]
684    #[test]
685    fn read_string_rejects_invalid_utf8() {
686        // length=3, bytes = 0xFF, 0xFE, null -> invalid UTF-8
687        let bytes = [3u8, 0, 0, 0, 0xFF, 0xFE, 0];
688        let mut r = BufferReader::new(&bytes, Endianness::Little);
689        assert!(matches!(
690            r.read_string(),
691            Err(DecodeError::InvalidUtf8 { .. })
692        ));
693    }
694
695    // ---- Mutation killers for BufferReader ----
696
697    /// Catches mutation `endianness -> Default::default()`. The reader
698    /// must return the endianness from the constructor, not the default.
699    /// Default::default() is Little — we test with Big.
700    #[test]
701    fn reader_endianness_getter_returns_construction_value() {
702        let r_be = BufferReader::new(&[0u8; 4], Endianness::Big);
703        assert_eq!(r_be.endianness(), Endianness::Big);
704        let r_le = BufferReader::new(&[0u8; 4], Endianness::Little);
705        assert_eq!(r_le.endianness(), Endianness::Little);
706    }
707
708    /// Catches mutation `< -> <=` in `align`. align() must NOT error
709    /// when `remaining()` has exactly `pad` bytes.
710    #[test]
711    fn reader_align_succeeds_when_remaining_equals_pad() {
712        // pos=1, align=4 -> pad=3, remaining must be >= 3.
713        // Exactly 4-byte buffer: read_u8 over 1 byte, then align(4)
714        // needs 3 pad bytes. After read_u8 the buffer still has 3 bytes
715        // -> remaining == pad (3). Original `<`: 3<3 false, no error.
716        // Mutation `<=`: 3<=3 true, error.
717        let bytes = [0xAA, 0, 0, 0];
718        let mut r = BufferReader::new(&bytes, Endianness::Little);
719        r.read_u8().unwrap();
720        assert!(r.align(4).is_ok());
721        assert_eq!(r.position(), 4);
722    }
723
724    /// Catches mutation `+= -> *=` on `self.pos += pad` in align().
725    /// align() must advance pos FORWARD.
726    #[test]
727    fn reader_align_advances_position_strictly() {
728        let bytes = [0xAA, 0, 0, 0, 1, 2, 3, 4];
729        let mut r = BufferReader::new(&bytes, Endianness::Little);
730        r.read_u8().unwrap();
731        assert_eq!(r.position(), 1);
732        r.align(4).unwrap();
733        // pos *= pad = 1*3 = 3 with the mutation; pos = 1+3 = 4 without it.
734        assert_eq!(r.position(), 4);
735    }
736
737    /// Catches mutation `+= -> *=` on `self.pos += n` in read_bytes().
738    /// read_bytes() must advance pos forward by exactly n.
739    #[test]
740    fn reader_read_bytes_advances_position_strictly() {
741        let bytes = [1, 2, 3, 4, 5, 6, 7, 8];
742        let mut r = BufferReader::new(&bytes, Endianness::Little);
743        let _ = r.read_bytes(3).unwrap();
744        // pos *= n = 0*3 = 0 with the mutation; pos = 0+3 = 3 without it.
745        assert_eq!(r.position(), 3);
746        let _ = r.read_bytes(2).unwrap();
747        // pos *= 2 = 3*2 = 6 with the mutation; pos = 3+2 = 5 without it.
748        assert_eq!(r.position(), 5);
749    }
750
751    /// Catches mutation `read_u16 -> Ok(0)`. read_u16 must actually
752    /// read the bytes, not just return 0.
753    #[test]
754    fn reader_read_u16_returns_actual_bytes_not_zero() {
755        let bytes = [0x12, 0x34];
756        let mut r = BufferReader::new(&bytes, Endianness::Little);
757        assert_eq!(r.read_u16().unwrap(), 0x3412);
758        let mut r_be = BufferReader::new(&bytes, Endianness::Big);
759        assert_eq!(r_be.read_u16().unwrap(), 0x1234);
760    }
761
762    /// Catches mutation `+ -> *` on `start + 4` in the read_string
763    /// error-offset computation.
764    ///
765    /// Spec: offset points to the start of the UTF-8 data = start + 4
766    /// (after the 4-byte length prefix). `start` is set BEFORE the
767    /// `read_u32`/alignment; with pos=1 at the start, start=1 and
768    /// offset=5. Mutation `*` would yield 1*4=4.
769    #[test]
770    fn reader_read_string_invalid_utf8_offset_is_start_plus_four() {
771        // A preceding u8 shifts start to 1 (not 0) so that `+` vs
772        // `*` differentiate: 1+4=5 vs 1*4=4.
773        let mut bytes = vec![0xAB]; // pos=0
774        // pad to 4: 3 zero bytes
775        bytes.extend_from_slice(&[0, 0, 0]);
776        // length=3 LE (incl. null-terminator slot)
777        bytes.extend_from_slice(&3u32.to_le_bytes());
778        // 3 bytes, last is NUL — the first two are invalid UTF-8.
779        bytes.extend_from_slice(&[0xFF, 0xFE, 0]);
780
781        let mut r = BufferReader::new(&bytes, Endianness::Little);
782        r.read_u8().unwrap();
783        let err = r.read_string().unwrap_err();
784        // `start = self.pos` BEFORE read_u32. After read_u8, pos=1.
785        // So start=1, start+4=5. Mutation `*`: 1*4=4. The difference suffices.
786        match err {
787            DecodeError::InvalidUtf8 { offset } => assert_eq!(offset, 5),
788            other => panic!("expected InvalidUtf8, got {other:?}"),
789        }
790    }
791
792    /// Second variant with start=2: 2+4=6 vs 2*4=8 — two-sided
793    /// differentiation (above start=1: +1, here start=2: +4).
794    #[test]
795    fn reader_read_string_invalid_utf8_offset_with_start_two() {
796        let mut bytes = vec![0xAB, 0xCD]; // pos=0..2
797        // pad to 4: 2 zero bytes
798        bytes.extend_from_slice(&[0, 0]);
799        bytes.extend_from_slice(&3u32.to_le_bytes());
800        bytes.extend_from_slice(&[0xFF, 0xFE, 0]);
801
802        let mut r = BufferReader::new(&bytes, Endianness::Little);
803        r.read_u8().unwrap();
804        r.read_u8().unwrap();
805        let err = r.read_string().unwrap_err();
806        match err {
807            DecodeError::InvalidUtf8 { offset } => assert_eq!(offset, 6),
808            other => panic!("expected InvalidUtf8, got {other:?}"),
809        }
810    }
811}