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 fuer XCDR.
4//!
5//! XCDR-Daten sind alignment-pflichtig: ein `u32`-Feld muss an einer
6//! 4-Byte-Boundary beginnen, ein `u64` an 8-Byte-Boundary etc. Der
7//! Encoder fuegt vor jedem Write die noetigen Padding-Bytes (Wert 0)
8//! ein; der Decoder skipped sie.
9//!
10//! Alignment wird relativ zum **Stream-Anfang** (Offset 0) berechnet,
11//! nicht relativ zur aktuellen Position innerhalb eines verschachtelten
12//! Members. Das entspricht OMG-XTypes §7.4.1: "All elements are aligned
13//! to a multiple of their alignment requirement, relative to the
14//! beginning of the encapsulation".
15//!
16//! Bewusste Architektur-Wahl: nur dynamischer Vec-basierter Writer
17//! (alloc-Feature). Slice-basierter Writer für no_std-without-alloc
18//! ist nicht implementiert — `alloc` ist via `zerodds-foundation`
19//! ohnehin transitive Mandatory-Dep.
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/// Schreib-Buffer mit Alignment-Tracking und konfigurierbarer Endianness.
36///
37/// Phase 0 nutzt `Vec<u8>` als Backing — wachsendes alloc-Feature.
38#[cfg(feature = "alloc")]
39#[derive(Debug, Clone)]
40pub struct BufferWriter {
41    bytes: Vec<u8>,
42    endianness: Endianness,
43}
44
45#[cfg(feature = "alloc")]
46impl BufferWriter {
47    /// Erstellt einen leeren Writer mit der gegebenen Endianness.
48    #[must_use]
49    pub fn new(endianness: Endianness) -> Self {
50        Self {
51            bytes: Vec::new(),
52            endianness,
53        }
54    }
55
56    /// Erstellt einen Writer mit vor-allokierter Kapazitaet.
57    #[must_use]
58    pub fn with_capacity(endianness: Endianness, cap: usize) -> Self {
59        Self {
60            bytes: Vec::with_capacity(cap),
61            endianness,
62        }
63    }
64
65    /// Liefert die aktuelle Endianness.
66    #[must_use]
67    pub fn endianness(&self) -> Endianness {
68        self.endianness
69    }
70
71    /// Aktuelle Position (== bisher geschriebene Byte-Anzahl).
72    #[must_use]
73    pub fn position(&self) -> usize {
74        self.bytes.len()
75    }
76
77    /// Konsumiert den Writer und liefert den geschriebenen Buffer.
78    #[must_use]
79    pub fn into_bytes(self) -> Vec<u8> {
80        self.bytes
81    }
82
83    /// Read-only Sicht auf den bisher geschriebenen Buffer.
84    #[must_use]
85    pub fn as_bytes(&self) -> &[u8] {
86        &self.bytes
87    }
88
89    /// Fuegt Null-Padding ein, bis die aktuelle Position ein Vielfaches
90    /// von `alignment` ist. Alignment muss eine Zweierpotenz sein
91    /// (1, 2, 4, 8); andere Werte sind in XCDR nicht definiert.
92    pub fn align(&mut self, alignment: usize) {
93        debug_assert!(
94            alignment.is_power_of_two(),
95            "alignment must be a power of two"
96        );
97        let pos = self.bytes.len();
98        let pad = padding_for(pos, alignment);
99        for _ in 0..pad {
100            self.bytes.push(0);
101        }
102    }
103
104    /// Schreibt rohe Bytes ohne Alignment.
105    ///
106    /// # Errors
107    /// Liefert nie einen Fehler bei `Vec`-basiertem Backing — die
108    /// Signatur ist symmetrisch zum Slice-basierten Writer (Phase 1).
109    pub fn write_bytes(&mut self, data: &[u8]) -> Result<(), EncodeError> {
110        self.bytes.extend_from_slice(data);
111        Ok(())
112    }
113
114    /// Schreibt ein einzelnes Byte (1-Byte Alignment).
115    ///
116    /// # Errors
117    /// Wie `write_bytes`.
118    pub fn write_u8(&mut self, value: u8) -> Result<(), EncodeError> {
119        self.bytes.push(value);
120        Ok(())
121    }
122
123    /// Aligned + schreibt `u16`.
124    ///
125    /// # Errors
126    /// Wie `write_bytes`.
127    pub fn write_u16(&mut self, value: u16) -> Result<(), EncodeError> {
128        self.align(2);
129        self.write_bytes(&self.endianness.write_u16(value))
130    }
131
132    /// Aligned + schreibt `u32`.
133    ///
134    /// # Errors
135    /// Wie `write_bytes`.
136    pub fn write_u32(&mut self, value: u32) -> Result<(), EncodeError> {
137        self.align(4);
138        self.write_bytes(&self.endianness.write_u32(value))
139    }
140
141    /// Aligned + schreibt `u64`.
142    ///
143    /// # Errors
144    /// Wie `write_bytes`.
145    pub fn write_u64(&mut self, value: u64) -> Result<(), EncodeError> {
146        self.align(8);
147        self.write_bytes(&self.endianness.write_u64(value))
148    }
149
150    /// Schreibt einen CDR-String (§9.3.2.7): 4-byte length (inkl.
151    /// Null-Terminator) + UTF-8 Bytes + 1 Null-Byte. Kein Trailing-
152    /// Padding — das naechste Feld aligned sich selbst.
153    ///
154    /// # Errors
155    /// Wie `write_bytes`.
156    pub fn write_string(&mut self, s: &str) -> Result<(), EncodeError> {
157        let bytes = s.as_bytes();
158        // Laenge = s.len() + 1 fuer null-terminator
159        let len = u32::try_from(bytes.len().saturating_add(1)).map_err(|_| {
160            EncodeError::ValueOutOfRange {
161                message: "CDR string length exceeds u32::MAX",
162            }
163        })?;
164        self.write_u32(len)?;
165        self.write_bytes(bytes)?;
166        self.write_u8(0)
167    }
168}
169
170// ============================================================================
171// BufferReader
172// ============================================================================
173
174/// Lese-Buffer mit Alignment-Tracking.
175#[derive(Debug, Clone)]
176pub struct BufferReader<'a> {
177    bytes: &'a [u8],
178    pos: usize,
179    endianness: Endianness,
180}
181
182impl<'a> BufferReader<'a> {
183    /// Konstruiert einen Reader ueber den gegebenen Slice.
184    #[must_use]
185    pub fn new(bytes: &'a [u8], endianness: Endianness) -> Self {
186        Self {
187            bytes,
188            pos: 0,
189            endianness,
190        }
191    }
192
193    /// Aktuelle Endianness.
194    #[must_use]
195    pub fn endianness(&self) -> Endianness {
196        self.endianness
197    }
198
199    /// Aktuelle Position im Stream.
200    #[must_use]
201    pub fn position(&self) -> usize {
202        self.pos
203    }
204
205    /// Verbleibende Bytes bis zum Ende.
206    #[must_use]
207    pub fn remaining(&self) -> usize {
208        self.bytes.len().saturating_sub(self.pos)
209    }
210
211    /// Skipt Padding bis zur naechsten `alignment`-Boundary.
212    ///
213    /// # Errors
214    /// `UnexpectedEof`, wenn nicht genug Bytes fuer das Padding da sind.
215    pub fn align(&mut self, alignment: usize) -> Result<(), DecodeError> {
216        debug_assert!(
217            alignment.is_power_of_two(),
218            "alignment must be power of two"
219        );
220        let pad = padding_for(self.pos, alignment);
221        if self.remaining() < pad {
222            return Err(DecodeError::UnexpectedEof {
223                needed: pad,
224                offset: self.pos,
225            });
226        }
227        self.pos += pad;
228        Ok(())
229    }
230
231    /// Liest exakt `n` Bytes als Slice (ohne Alignment).
232    ///
233    /// # Errors
234    /// `UnexpectedEof`.
235    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], DecodeError> {
236        if self.remaining() < n {
237            return Err(DecodeError::UnexpectedEof {
238                needed: n,
239                offset: self.pos,
240            });
241        }
242        let slice = &self.bytes[self.pos..self.pos + n];
243        self.pos += n;
244        Ok(slice)
245    }
246
247    /// Liest ein einzelnes Byte.
248    ///
249    /// # Errors
250    /// `UnexpectedEof`.
251    pub fn read_u8(&mut self) -> Result<u8, DecodeError> {
252        let slice = self.read_bytes(1)?;
253        Ok(slice[0])
254    }
255
256    /// Aligned + liest `u16`.
257    ///
258    /// # Errors
259    /// `UnexpectedEof`.
260    pub fn read_u16(&mut self) -> Result<u16, DecodeError> {
261        self.align(2)?;
262        let slice = self.read_bytes(2)?;
263        let mut buf = [0u8; 2];
264        buf.copy_from_slice(slice);
265        Ok(self.endianness.read_u16(buf))
266    }
267
268    /// Aligned + liest `u32`.
269    ///
270    /// # Errors
271    /// `UnexpectedEof`.
272    pub fn read_u32(&mut self) -> Result<u32, DecodeError> {
273        self.align(4)?;
274        let slice = self.read_bytes(4)?;
275        let mut buf = [0u8; 4];
276        buf.copy_from_slice(slice);
277        Ok(self.endianness.read_u32(buf))
278    }
279
280    /// Aligned + liest `u64`.
281    ///
282    /// # Errors
283    /// `UnexpectedEof`.
284    pub fn read_u64(&mut self) -> Result<u64, DecodeError> {
285        self.align(8)?;
286        let slice = self.read_bytes(8)?;
287        let mut buf = [0u8; 8];
288        buf.copy_from_slice(slice);
289        Ok(self.endianness.read_u64(buf))
290    }
291
292    /// Liest einen CDR-String (§9.3.2.7): 4-byte length (inkl. Null-
293    /// Terminator) + UTF-8 Bytes + 1 Null-Byte. Gibt den String **ohne**
294    /// Terminator zurueck.
295    ///
296    /// # Errors
297    /// `UnexpectedEof` bei zu kurzen Daten; `InvalidData` bei nicht-UTF-8
298    /// Bytes, fehlendem Null-Terminator oder `length == 0`.
299    #[cfg(feature = "alloc")]
300    pub fn read_string(&mut self) -> Result<alloc::string::String, DecodeError> {
301        use alloc::string::String;
302        let start = self.pos;
303        let len = self.read_u32()? as usize;
304        if len == 0 {
305            return Err(DecodeError::InvalidString {
306                offset: start,
307                reason: "length must be > 0 (null terminator required)",
308            });
309        }
310        let raw = self.read_bytes(len)?;
311        // last byte must be null terminator
312        if raw[len - 1] != 0 {
313            return Err(DecodeError::InvalidString {
314                offset: start,
315                reason: "missing null terminator",
316            });
317        }
318        String::from_utf8(raw[..len - 1].to_vec())
319            .map_err(|_| DecodeError::InvalidUtf8 { offset: start + 4 })
320    }
321}
322
323// ============================================================================
324// Helpers
325// ============================================================================
326
327/// Berechnet die Anzahl Padding-Bytes, um `pos` an die naechste
328/// Vielfaches-von-`alignment`-Boundary zu schieben.
329#[must_use]
330pub fn padding_for(pos: usize, alignment: usize) -> usize {
331    let mask = alignment - 1;
332    (alignment - (pos & mask)) & mask
333}
334
335#[cfg(test)]
336mod tests {
337    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
338    use super::*;
339
340    #[cfg(feature = "alloc")]
341    extern crate alloc;
342    #[cfg(feature = "alloc")]
343    use alloc::vec;
344
345    #[test]
346    fn padding_for_zero_position_is_zero() {
347        assert_eq!(padding_for(0, 4), 0);
348        assert_eq!(padding_for(0, 8), 0);
349    }
350
351    #[test]
352    fn padding_for_already_aligned_is_zero() {
353        assert_eq!(padding_for(8, 4), 0);
354        assert_eq!(padding_for(16, 8), 0);
355    }
356
357    #[test]
358    fn padding_for_one_byte_to_4_align_is_three() {
359        assert_eq!(padding_for(1, 4), 3);
360    }
361
362    #[test]
363    fn padding_for_three_bytes_to_8_align_is_five() {
364        assert_eq!(padding_for(3, 8), 5);
365    }
366
367    #[cfg(feature = "alloc")]
368    #[test]
369    fn writer_writes_u8_without_padding() {
370        let mut w = BufferWriter::new(Endianness::Little);
371        w.write_u8(0xAB).unwrap();
372        assert_eq!(w.as_bytes(), &[0xAB]);
373        assert_eq!(w.position(), 1);
374    }
375
376    #[cfg(feature = "alloc")]
377    #[test]
378    fn writer_aligns_u32_after_u8() {
379        let mut w = BufferWriter::new(Endianness::Little);
380        w.write_u8(0xAB).unwrap();
381        w.write_u32(0xDEAD_BEEF).unwrap();
382        // 1 byte 0xAB + 3 bytes padding + 4 bytes LE u32
383        assert_eq!(w.as_bytes(), &[0xAB, 0, 0, 0, 0xEF, 0xBE, 0xAD, 0xDE]);
384    }
385
386    #[cfg(feature = "alloc")]
387    #[test]
388    fn writer_aligns_u64_after_u8() {
389        let mut w = BufferWriter::new(Endianness::Big);
390        w.write_u8(0x01).unwrap();
391        w.write_u64(0x0203_0405_0607_0809).unwrap();
392        // 1 byte + 7 bytes padding + 8 bytes BE u64
393        assert_eq!(
394            w.as_bytes(),
395            &[0x01, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9]
396        );
397    }
398
399    #[cfg(feature = "alloc")]
400    #[test]
401    fn writer_with_capacity_preserves_endianness() {
402        let w = BufferWriter::with_capacity(Endianness::Big, 64);
403        assert_eq!(w.endianness(), Endianness::Big);
404        assert_eq!(w.position(), 0);
405    }
406
407    #[cfg(feature = "alloc")]
408    #[test]
409    fn writer_into_bytes_returns_full_buffer() {
410        let mut w = BufferWriter::new(Endianness::Little);
411        w.write_u32(0xCAFE_BABE).unwrap();
412        let bytes = w.into_bytes();
413        assert_eq!(bytes, vec![0xBE, 0xBA, 0xFE, 0xCA]);
414    }
415
416    #[test]
417    fn reader_reads_u8() {
418        let bytes = [0xAB, 0xCD];
419        let mut r = BufferReader::new(&bytes, Endianness::Little);
420        assert_eq!(r.read_u8().unwrap(), 0xAB);
421        assert_eq!(r.position(), 1);
422    }
423
424    #[test]
425    fn reader_aligns_before_u32() {
426        let bytes = [0xAB, 0, 0, 0, 0xEF, 0xBE, 0xAD, 0xDE];
427        let mut r = BufferReader::new(&bytes, Endianness::Little);
428        assert_eq!(r.read_u8().unwrap(), 0xAB);
429        assert_eq!(r.read_u32().unwrap(), 0xDEAD_BEEF);
430        assert_eq!(r.remaining(), 0);
431    }
432
433    #[test]
434    fn reader_aligns_before_u64_be() {
435        let bytes = [0x01, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9];
436        let mut r = BufferReader::new(&bytes, Endianness::Big);
437        assert_eq!(r.read_u8().unwrap(), 0x01);
438        assert_eq!(r.read_u64().unwrap(), 0x0203_0405_0607_0809);
439    }
440
441    #[test]
442    fn reader_unexpected_eof_on_short_read() {
443        let bytes = [0u8; 2];
444        let mut r = BufferReader::new(&bytes, Endianness::Little);
445        let res = r.read_u32();
446        // u32 verlangt 4 Byte ab Position 0; nur 2 verfuegbar.
447        match res {
448            Err(DecodeError::UnexpectedEof {
449                needed: 4,
450                offset: 0,
451            }) => {}
452            other => panic!("expected UnexpectedEof, got {other:?}"),
453        }
454    }
455
456    #[test]
457    fn reader_eof_on_align_overflow() {
458        let bytes = [0u8; 1];
459        let mut r = BufferReader::new(&bytes, Endianness::Little);
460        let _ = r.read_u8().unwrap();
461        let res = r.align(8);
462        assert!(matches!(res, Err(DecodeError::UnexpectedEof { .. })));
463    }
464
465    #[cfg(feature = "alloc")]
466    #[test]
467    fn writer_reader_roundtrip_mixed_primitives() {
468        let mut w = BufferWriter::new(Endianness::Little);
469        w.write_u8(1).unwrap();
470        w.write_u16(0x1234).unwrap();
471        w.write_u32(0x5678_9ABC).unwrap();
472        w.write_u64(0x0102_0304_0506_0708).unwrap();
473        let bytes = w.into_bytes();
474        let mut r = BufferReader::new(&bytes, Endianness::Little);
475        assert_eq!(r.read_u8().unwrap(), 1);
476        assert_eq!(r.read_u16().unwrap(), 0x1234);
477        assert_eq!(r.read_u32().unwrap(), 0x5678_9ABC);
478        assert_eq!(r.read_u64().unwrap(), 0x0102_0304_0506_0708);
479        assert_eq!(r.remaining(), 0);
480    }
481
482    #[cfg(feature = "alloc")]
483    #[test]
484    fn write_read_string_roundtrip_ascii() {
485        let mut w = BufferWriter::new(Endianness::Little);
486        w.write_string("ChatterTopic").unwrap();
487        let bytes = w.into_bytes();
488        // length=13 (12 chars + null), 12 bytes ascii, 1 null
489        assert_eq!(&bytes[0..4], &[13, 0, 0, 0]);
490        assert_eq!(&bytes[4..16], b"ChatterTopic");
491        assert_eq!(bytes[16], 0);
492        let mut r = BufferReader::new(&bytes, Endianness::Little);
493        assert_eq!(r.read_string().unwrap(), "ChatterTopic");
494    }
495
496    #[cfg(feature = "alloc")]
497    #[test]
498    fn write_read_string_empty() {
499        // "": length=1 (just null), 0 chars, 1 null
500        let mut w = BufferWriter::new(Endianness::Little);
501        w.write_string("").unwrap();
502        let bytes = w.into_bytes();
503        assert_eq!(&bytes[..], &[1, 0, 0, 0, 0]);
504        let mut r = BufferReader::new(&bytes, Endianness::Little);
505        assert_eq!(r.read_string().unwrap(), "");
506    }
507
508    #[cfg(feature = "alloc")]
509    #[test]
510    fn write_read_string_utf8_multibyte() {
511        let mut w = BufferWriter::new(Endianness::Little);
512        w.write_string("Zähler").unwrap(); // ä = 2 bytes UTF-8
513        let bytes = w.into_bytes();
514        let mut r = BufferReader::new(&bytes, Endianness::Little);
515        assert_eq!(r.read_string().unwrap(), "Zähler");
516    }
517
518    #[cfg(feature = "alloc")]
519    #[test]
520    fn read_string_rejects_length_zero() {
521        let bytes = [0u8, 0, 0, 0];
522        let mut r = BufferReader::new(&bytes, Endianness::Little);
523        assert!(matches!(
524            r.read_string(),
525            Err(DecodeError::InvalidString { .. })
526        ));
527    }
528
529    #[cfg(feature = "alloc")]
530    #[test]
531    fn read_string_rejects_missing_null_terminator() {
532        // length=4, aber nur ASCII ohne null-terminator
533        let bytes = [4u8, 0, 0, 0, b'A', b'B', b'C', b'D'];
534        let mut r = BufferReader::new(&bytes, Endianness::Little);
535        assert!(matches!(
536            r.read_string(),
537            Err(DecodeError::InvalidString { .. })
538        ));
539    }
540
541    #[cfg(feature = "alloc")]
542    #[test]
543    fn read_string_rejects_invalid_utf8() {
544        // length=3, bytes = 0xFF, 0xFE, null → invalid UTF-8
545        let bytes = [3u8, 0, 0, 0, 0xFF, 0xFE, 0];
546        let mut r = BufferReader::new(&bytes, Endianness::Little);
547        assert!(matches!(
548            r.read_string(),
549            Err(DecodeError::InvalidUtf8 { .. })
550        ));
551    }
552
553    // ---- Mutation-Killer fuer BufferReader ----
554
555    /// Faengt Mutation `endianness -> Default::default()`. Reader muss
556    /// die Endianness aus dem Konstruktor zurueckgeben, nicht Default.
557    /// Default::default() ist Little — wir testen mit Big.
558    #[test]
559    fn reader_endianness_getter_returns_construction_value() {
560        let r_be = BufferReader::new(&[0u8; 4], Endianness::Big);
561        assert_eq!(r_be.endianness(), Endianness::Big);
562        let r_le = BufferReader::new(&[0u8; 4], Endianness::Little);
563        assert_eq!(r_le.endianness(), Endianness::Little);
564    }
565
566    /// Faengt Mutation `< -> <=` in `align`. align() darf NICHT erroren
567    /// wenn `remaining()` exakt `pad` Bytes hat.
568    #[test]
569    fn reader_align_succeeds_when_remaining_equals_pad() {
570        // pos=1, align=4 → pad=3, remaining muss >= 3 sein.
571        // Genau 4 Bytes Puffer: read_u8 ueber 1 Byte, dann align(4)
572        // braucht 3 pad-Bytes. Buffer hat nach read_u8 noch 3 Bytes
573        // → remaining == pad (3). Original `<`: 3<3 false, kein Error.
574        // Mutation `<=`: 3<=3 true, Error.
575        let bytes = [0xAA, 0, 0, 0];
576        let mut r = BufferReader::new(&bytes, Endianness::Little);
577        r.read_u8().unwrap();
578        assert!(r.align(4).is_ok());
579        assert_eq!(r.position(), 4);
580    }
581
582    /// Faengt Mutation `+= -> *=` auf `self.pos += pad` in align().
583    /// align() muss pos VORWAERTS bewegen.
584    #[test]
585    fn reader_align_advances_position_strictly() {
586        let bytes = [0xAA, 0, 0, 0, 1, 2, 3, 4];
587        let mut r = BufferReader::new(&bytes, Endianness::Little);
588        r.read_u8().unwrap();
589        assert_eq!(r.position(), 1);
590        r.align(4).unwrap();
591        // pos *= pad = 1*3 = 3 mit Mutation; pos = 1+3 = 4 ohne Mutation.
592        assert_eq!(r.position(), 4);
593    }
594
595    /// Faengt Mutation `+= -> *=` auf `self.pos += n` in read_bytes().
596    /// read_bytes() muss pos um genau n vorwaerts bewegen.
597    #[test]
598    fn reader_read_bytes_advances_position_strictly() {
599        let bytes = [1, 2, 3, 4, 5, 6, 7, 8];
600        let mut r = BufferReader::new(&bytes, Endianness::Little);
601        let _ = r.read_bytes(3).unwrap();
602        // pos *= n = 0*3 = 0 mit Mutation; pos = 0+3 = 3 ohne Mutation.
603        assert_eq!(r.position(), 3);
604        let _ = r.read_bytes(2).unwrap();
605        // pos *= 2 = 3*2 = 6 mit Mutation; pos = 3+2 = 5 ohne Mutation.
606        assert_eq!(r.position(), 5);
607    }
608
609    /// Faengt Mutation `read_u16 -> Ok(0)`. read_u16 muss tatsaechlich
610    /// die Bytes lesen, nicht pauschal 0 zurueckgeben.
611    #[test]
612    fn reader_read_u16_returns_actual_bytes_not_zero() {
613        let bytes = [0x12, 0x34];
614        let mut r = BufferReader::new(&bytes, Endianness::Little);
615        assert_eq!(r.read_u16().unwrap(), 0x3412);
616        let mut r_be = BufferReader::new(&bytes, Endianness::Big);
617        assert_eq!(r_be.read_u16().unwrap(), 0x1234);
618    }
619
620    /// Faengt Mutation `+ -> *` auf `start + 4` in der read_string-
621    /// Fehler-Offset-Berechnung.
622    ///
623    /// Spec: offset zeigt auf den Beginn der utf-8-Daten = start + 4
624    /// (nach dem 4-Byte-Length-Prefix). `start` wird VOR dem
625    /// `read_u32`/Alignment gesetzt; bei pos=1 zu Beginn ist start=1
626    /// und offset=5. Mutation `*` wuerde 1*4=4 liefern.
627    #[test]
628    fn reader_read_string_invalid_utf8_offset_is_start_plus_four() {
629        // Vorgaenger-u8 verschiebt start auf 1 (nicht 0), damit `+` vs
630        // `*` differenzieren: 1+4=5 vs 1*4=4.
631        let mut bytes = vec![0xAB]; // pos=0
632        // pad to 4: 3 zero bytes
633        bytes.extend_from_slice(&[0, 0, 0]);
634        // length=3 LE (inkl. Null-Terminator-Slot)
635        bytes.extend_from_slice(&3u32.to_le_bytes());
636        // 3 Bytes, letztes ist NUL — die ersten zwei sind invalid utf-8.
637        bytes.extend_from_slice(&[0xFF, 0xFE, 0]);
638
639        let mut r = BufferReader::new(&bytes, Endianness::Little);
640        r.read_u8().unwrap();
641        let err = r.read_string().unwrap_err();
642        // `start = self.pos` BEVOR read_u32. Nach read_u8 ist pos=1.
643        // Also start=1, start+4=5. Mutation `*`: 1*4=4. Differenz reicht.
644        match err {
645            DecodeError::InvalidUtf8 { offset } => assert_eq!(offset, 5),
646            other => panic!("expected InvalidUtf8, got {other:?}"),
647        }
648    }
649
650    /// Zweite Variante mit start=2: 2+4=6 vs 2*4=8 — beidseitige
651    /// Differenzierung (oben start=1: +1, unten start=2: +4).
652    #[test]
653    fn reader_read_string_invalid_utf8_offset_with_start_two() {
654        let mut bytes = vec![0xAB, 0xCD]; // pos=0..2
655        // pad to 4: 2 zero bytes
656        bytes.extend_from_slice(&[0, 0]);
657        bytes.extend_from_slice(&3u32.to_le_bytes());
658        bytes.extend_from_slice(&[0xFF, 0xFE, 0]);
659
660        let mut r = BufferReader::new(&bytes, Endianness::Little);
661        r.read_u8().unwrap();
662        r.read_u8().unwrap();
663        let err = r.read_string().unwrap_err();
664        match err {
665            DecodeError::InvalidUtf8 { offset } => assert_eq!(offset, 6),
666            other => panic!("expected InvalidUtf8, got {other:?}"),
667        }
668    }
669}