Skip to main content

zerodds_cdr/
struct_enc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Struct encoding with extensibility (W3, XCDR2 §7.4.3, §7.4.5).
4//!
5//! XCDR2 has three struct layouts:
6//!
7//! - **`@final`**: tight-packed, no header. Members in declared order.
8//!   Reader and writer must share the member list exactly — no
9//!   forward/backward compatibility.
10//! - **`@appendable`**: 4-byte **DHEADER** (uint32 = byte length of the
11//!   body after the header) + tight-packed members. Forward-compatible:
12//!   the reader can skip bytes after the known members.
13//! - **`@mutable`**: one **EMHEADER** + value per member. EMHEADER =
14//!   `uint32` with member ID + length code. Backward/forward-compatible
15//!   through member-ID-based matching.
16//!
17//! Helpers for all 3 modes with a classic 2-pass encoder (inner buffer
18//! for the body, then header + body into the outer one). Length codes:
19//! LC0..LC7 fully implemented (default constructor `LC4` variable-length
20//! with a separate `uint32` NEXTINT; LC0..LC3 compact 1/2/4/8-byte
21//! without NEXTINT; LC5..LC7 sequence/array-specific).
22//!
23//! Alignment: the body content of a DHEADER/EMHEADER frame starts at
24//! offset 0 relative to the body start (XCDR2 §7.4.3.4.5).
25
26extern crate alloc;
27use alloc::vec::Vec;
28
29use crate::buffer::{BufferReader, BufferWriter};
30use crate::error::{DecodeError, EncodeError};
31
32// ============================================================================
33// @appendable
34// ============================================================================
35
36/// Encodes an `@appendable` struct. The body is written into an inner
37/// buffer, then length + body into the outer writer.
38///
39/// # Errors
40/// Encoder error from the body, or `ValueOutOfRange` if the body
41/// exceeds `u32::MAX` bytes.
42pub fn encode_appendable<F>(writer: &mut BufferWriter, body: F) -> Result<(), EncodeError>
43where
44    F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
45{
46    // The inner body inherits the parent's alignment cap (XCDR2=4) —
47    // otherwise a 64-bit member inside the DHEADER body would re-align to 8.
48    let mut inner =
49        BufferWriter::new(writer.endianness()).with_max_alignment(writer.max_alignment());
50    body(&mut inner)?;
51    let bytes = inner.into_bytes();
52    let len = u32::try_from(bytes.len()).map_err(|_| EncodeError::ValueOutOfRange {
53        message: "appendable struct body exceeds u32::MAX",
54    })?;
55    writer.write_u32(len)?;
56    writer.write_bytes(&bytes)?;
57    Ok(())
58}
59
60/// Decodes an `@appendable` struct. Reads the DHEADER length, builds a
61/// sub-reader over the body, and hands it to `body`. The sub-reader lets
62/// the body consume fewer bytes than announced — unused bytes are
63/// skipped.
64///
65/// # Errors
66/// Decoder error from the body, or `LengthExceeded`/`UnexpectedEof` if
67/// the length does not fit in the stream.
68pub fn decode_appendable<T, F>(reader: &mut BufferReader<'_>, body: F) -> Result<T, DecodeError>
69where
70    F: FnOnce(&mut BufferReader<'_>) -> Result<T, DecodeError>,
71{
72    let len = reader.read_u32()? as usize;
73    if len > reader.remaining() {
74        return Err(DecodeError::LengthExceeded {
75            announced: len,
76            remaining: reader.remaining(),
77            offset: reader.position(),
78        });
79    }
80    let body_bytes = reader.read_bytes(len)?;
81    // The inner body inherits the alignment cap (XCDR2=4) — symmetric to
82    // the encode side, otherwise the reader skips wrong 64-bit padding.
83    let mut sub = BufferReader::new(body_bytes, reader.endianness())
84        .with_max_alignment(reader.max_alignment());
85    body(&mut sub)
86}
87
88// ============================================================================
89// @mutable
90// ============================================================================
91
92/// Length-code variant (XTypes 1.3 §7.4.3.4.2). WP 1.A: all 8 LCs.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94#[repr(u8)]
95pub enum LengthCode {
96    /// 1-byte body, no NEXTINT.
97    Lc0 = 0,
98    /// 2-byte body, no NEXTINT.
99    Lc1 = 1,
100    /// 4-byte body, no NEXTINT.
101    Lc2 = 2,
102    /// 8-byte body, no NEXTINT.
103    Lc3 = 3,
104    /// Variable-length body, NEXTINT (uint32) = body length in bytes.
105    Lc4 = 4,
106    /// Variable-length aggregate, NEXTINT = body length INCLUDING DHEADER.
107    Lc5 = 5,
108    /// Array of 4-byte primitives, NEXTINT = element count, body = `4 + 4*N`.
109    Lc6 = 6,
110    /// Array of 8-byte primitives, NEXTINT = element count, body = `4 + 8*N`.
111    Lc7 = 7,
112}
113
114impl LengthCode {
115    /// Body length in bytes for this LC, given the NEXTINT value.
116    #[must_use]
117    pub fn body_len(self, nextint: u32) -> u64 {
118        match self {
119            Self::Lc0 => 1,
120            Self::Lc1 => 2,
121            Self::Lc2 => 4,
122            Self::Lc3 => 8,
123            Self::Lc4 | Self::Lc5 => u64::from(nextint),
124            Self::Lc6 => 4 * u64::from(nextint) + 4,
125            Self::Lc7 => 8 * u64::from(nextint) + 4,
126        }
127    }
128
129    /// `true` if the LC carries a NEXTINT field after the EMHEADER.
130    #[must_use]
131    pub const fn has_nextint(self) -> bool {
132        matches!(self, Self::Lc4 | Self::Lc5 | Self::Lc6 | Self::Lc7)
133    }
134
135    /// Decode from the 3-bit wire field of an EMHEADER.
136    #[must_use]
137    pub const fn from_wire(value: u8) -> Option<Self> {
138        match value {
139            0 => Some(Self::Lc0),
140            1 => Some(Self::Lc1),
141            2 => Some(Self::Lc2),
142            3 => Some(Self::Lc3),
143            4 => Some(Self::Lc4),
144            5 => Some(Self::Lc5),
145            6 => Some(Self::Lc6),
146            7 => Some(Self::Lc7),
147            _ => None,
148        }
149    }
150}
151
152/// Encodes a `@mutable` member with **LC4** (the default universal code).
153///
154/// # Errors
155/// Body error or member ID > 0x0FFF_FFFF.
156pub fn encode_mutable_member<F>(
157    writer: &mut BufferWriter,
158    member_id: u32,
159    must_understand: bool,
160    body: F,
161) -> Result<(), EncodeError>
162where
163    F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
164{
165    encode_mutable_member_lc(writer, member_id, must_understand, LengthCode::Lc4, body)
166}
167
168/// Encodes a `@mutable` member with an explicit length code.
169///
170/// # Errors
171/// `ValueOutOfRange` on member-ID overflow or body-length mismatch for
172/// the chosen LC.
173pub fn encode_mutable_member_lc<F>(
174    writer: &mut BufferWriter,
175    member_id: u32,
176    must_understand: bool,
177    lc: LengthCode,
178    body: F,
179) -> Result<(), EncodeError>
180where
181    F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
182{
183    if member_id > 0x0FFF_FFFF {
184        return Err(EncodeError::ValueOutOfRange {
185            message: "EMHEADER member_id exceeds 28-bit field",
186        });
187    }
188    // The inner body inherits the parent's alignment cap (XCDR2=4) —
189    // otherwise a 64-bit member inside the DHEADER body would re-align to 8.
190    let mut inner =
191        BufferWriter::new(writer.endianness()).with_max_alignment(writer.max_alignment());
192    body(&mut inner)?;
193    let body_bytes = inner.into_bytes();
194    let body_len = body_bytes.len();
195
196    let nextint: Option<u32> = match lc {
197        LengthCode::Lc0 => {
198            if body_len != 1 {
199                return Err(EncodeError::ValueOutOfRange {
200                    message: "LC0 requires exactly 1 byte body",
201                });
202            }
203            None
204        }
205        LengthCode::Lc1 => {
206            if body_len != 2 {
207                return Err(EncodeError::ValueOutOfRange {
208                    message: "LC1 requires exactly 2 bytes body",
209                });
210            }
211            None
212        }
213        LengthCode::Lc2 => {
214            if body_len != 4 {
215                return Err(EncodeError::ValueOutOfRange {
216                    message: "LC2 requires exactly 4 bytes body",
217                });
218            }
219            None
220        }
221        LengthCode::Lc3 => {
222            if body_len != 8 {
223                return Err(EncodeError::ValueOutOfRange {
224                    message: "LC3 requires exactly 8 bytes body",
225                });
226            }
227            None
228        }
229        LengthCode::Lc4 | LengthCode::Lc5 => {
230            let n = u32::try_from(body_len).map_err(|_| EncodeError::ValueOutOfRange {
231                message: "LC4/LC5 body exceeds u32::MAX",
232            })?;
233            Some(n)
234        }
235        LengthCode::Lc6 => {
236            // `body_len % 4 != 0` is equivalent to `(body_len - 4) % 4 != 0`
237            // for body_len >= 4 (after the first check). Eliminates a
238            // mathematically-equivalent `-4`/`+4` mutation.
239            if body_len < 4 || body_len % 4 != 0 {
240                return Err(EncodeError::ValueOutOfRange {
241                    message: "LC6 body must be DHEADER + 4n bytes",
242                });
243            }
244            let n =
245                u32::try_from((body_len - 4) / 4).map_err(|_| EncodeError::ValueOutOfRange {
246                    message: "LC6 element count exceeds u32::MAX",
247                })?;
248            Some(n)
249        }
250        LengthCode::Lc7 => {
251            // body_len < 4: reject. body_len in {4,12,20,...}: pass.
252            // body_len in {5,6,7}: body_len % 8 ≠ 0 → reject. But the
253            // boundary check must be careful: body_len=8 has body_len%8=0,
254            // yet (8-4)/8=0.5 is not a valid n. We need
255            // `(body_len - 4) % 8 == 0`, which is equivalent to
256            // `body_len % 8 == 4`.
257            if body_len < 4 || body_len % 8 != 4 {
258                return Err(EncodeError::ValueOutOfRange {
259                    message: "LC7 body must be DHEADER + 8n bytes",
260                });
261            }
262            let n =
263                u32::try_from((body_len - 4) / 8).map_err(|_| EncodeError::ValueOutOfRange {
264                    message: "LC7 element count exceeds u32::MAX",
265                })?;
266            Some(n)
267        }
268    };
269
270    let m_bit = u32::from(must_understand) << 31;
271    let lc_bits = (lc as u32) << 28;
272    // Arithmetic form instead of OR: the bit positions don't overlap
273    // (m_bit=bit 31, lc_bits=bits 28-30, member_id<=bits 0-27).
274    // Mathematically identical to `m_bit | lc_bits | member_id`, but
275    // more mutation-detection-friendly: `+` vs `^`/`-`/`*` are not
276    // equivalent to each other.
277    let emheader = m_bit + lc_bits + member_id;
278    writer.write_u32(emheader)?;
279    if let Some(ni) = nextint {
280        writer.write_u32(ni)?;
281    }
282    writer.write_bytes(&body_bytes)?;
283    Ok(())
284}
285
286/// Encoder for a `@mutable` struct that validates non-optional member
287/// completeness (XTypes 1.3 §7.4.1.2.3).
288///
289/// Before each member encode, `member_id` is recorded as "emitted"; at
290/// `finish`, every member ID listed in `required_ids` must have been
291/// emitted, otherwise `EncodeError::MissingNonOptionalMember` is
292/// returned.
293///
294/// Spec background: an `EXTENSIBLE` (final/appendable/mutable) encode
295/// MUST contain all non-optional members. This validator closes the
296/// encoder gap for @mutable, because with MUTABLE the EMHEADER order is
297/// not fixed and encoder bugs would otherwise pass silently.
298pub struct MutableStructEncoder<'a> {
299    writer: &'a mut BufferWriter,
300    required_ids: Vec<u32>,
301    emitted_ids: Vec<u32>,
302}
303
304impl<'a> MutableStructEncoder<'a> {
305    /// New encoder. `required_ids` is the list of member IDs that, per
306    /// spec, MUST all be emitted (= all non-optional members of the
307    /// struct).
308    pub fn new(writer: &'a mut BufferWriter, required_ids: Vec<u32>) -> Self {
309        Self {
310            writer,
311            required_ids,
312            emitted_ids: Vec::new(),
313        }
314    }
315
316    /// Encode a member. Behaves like `encode_mutable_member`, plus
317    /// tracking of the emitted ID.
318    ///
319    /// # Errors
320    /// Same as `encode_mutable_member`.
321    pub fn encode_member<F>(
322        &mut self,
323        member_id: u32,
324        must_understand: bool,
325        body: F,
326    ) -> Result<(), EncodeError>
327    where
328        F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
329    {
330        encode_mutable_member(self.writer, member_id, must_understand, body)?;
331        self.emitted_ids.push(member_id);
332        Ok(())
333    }
334
335    /// Member with an explicit length code.
336    ///
337    /// # Errors
338    /// Same as `encode_mutable_member_lc`.
339    pub fn encode_member_lc<F>(
340        &mut self,
341        member_id: u32,
342        must_understand: bool,
343        lc: LengthCode,
344        body: F,
345    ) -> Result<(), EncodeError>
346    where
347        F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
348    {
349        encode_mutable_member_lc(self.writer, member_id, must_understand, lc, body)?;
350        self.emitted_ids.push(member_id);
351        Ok(())
352    }
353
354    /// Finishes the mutable sequence and checks that every member ID
355    /// listed in `required_ids` was emitted.
356    ///
357    /// # Errors
358    /// `MissingNonOptionalMember { member_id }` with the first missing
359    /// ID (deterministic in the order of the `required_ids` list).
360    pub fn finish(self) -> Result<(), EncodeError> {
361        for required in &self.required_ids {
362            if !self.emitted_ids.contains(required) {
363                return Err(EncodeError::MissingNonOptionalMember {
364                    member_id: *required,
365                });
366            }
367        }
368        Ok(())
369    }
370}
371
372/// Parsed EMHEADER + body slice of a `@mutable` member.
373#[derive(Debug, Clone)]
374pub struct MutableMember<'a> {
375    /// 28-bit member ID.
376    pub member_id: u32,
377    /// `must_understand` flag.
378    pub must_understand: bool,
379    /// Length code.
380    pub length_code: LengthCode,
381    /// Body as an unconsumed slice.
382    pub body: &'a [u8],
383}
384
385/// Reads a `@mutable` member entry (EMHEADER + NEXTINT + body).
386///
387/// # Errors
388/// `UnexpectedEof` / `LengthExceeded` on a truncated/oversize body.
389pub fn read_mutable_member<'a>(
390    reader: &mut BufferReader<'a>,
391) -> Result<Option<MutableMember<'a>>, DecodeError> {
392    if reader.remaining() == 0 {
393        return Ok(None);
394    }
395    let emheader = reader.read_u32()?;
396    let must_understand = (emheader >> 31) & 1 == 1;
397    let lc_raw = ((emheader >> 28) & 0b0111) as u8;
398    let member_id = emheader & 0x0FFF_FFFF;
399    let length_code = LengthCode::from_wire(lc_raw).ok_or_else(|| DecodeError::LengthExceeded {
400        announced: usize::from(lc_raw),
401        remaining: 0,
402        offset: reader.position(),
403    })?;
404
405    let nextint = if length_code.has_nextint() {
406        reader.read_u32()?
407    } else {
408        0
409    };
410
411    let body_len_u64 = length_code.body_len(nextint);
412    let body_len = usize::try_from(body_len_u64).map_err(|_| DecodeError::LengthExceeded {
413        announced: usize::MAX,
414        remaining: reader.remaining(),
415        offset: reader.position(),
416    })?;
417    if body_len > reader.remaining() {
418        return Err(DecodeError::LengthExceeded {
419            announced: body_len,
420            remaining: reader.remaining(),
421            offset: reader.position(),
422        });
423    }
424    let body = reader.read_bytes(body_len)?;
425    Ok(Some(MutableMember {
426        member_id,
427        must_understand,
428        length_code,
429        body,
430    }))
431}
432
433/// Collect all members of a `@mutable` struct into a list. Lets the
434/// caller look up members by ID instead of reading sequentially.
435///
436/// # Errors
437/// Same as [`read_mutable_member`].
438pub fn read_all_mutable_members<'a>(
439    reader: &mut BufferReader<'a>,
440) -> Result<Vec<MutableMember<'a>>, DecodeError> {
441    let mut out = Vec::new();
442    while let Some(m) = read_mutable_member(reader)? {
443        out.push(m);
444    }
445    Ok(out)
446}
447
448// ============================================================================
449// @final (no-op wrapper)
450// ============================================================================
451
452/// `@final` struct: tight-packed, no header. This function is a pure
453/// convenience wrapper so the 3 extensibility modes have uniform call
454/// sites.
455///
456/// # Errors
457/// Body error.
458pub fn encode_final<F>(writer: &mut BufferWriter, body: F) -> Result<(), EncodeError>
459where
460    F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
461{
462    body(writer)
463}
464
465/// Decoder counterpart: just call the body.
466///
467/// # Errors
468/// Body error.
469pub fn decode_final<T, F>(reader: &mut BufferReader<'_>, body: F) -> Result<T, DecodeError>
470where
471    F: FnOnce(&mut BufferReader<'_>) -> Result<T, DecodeError>,
472{
473    body(reader)
474}
475
476#[cfg(test)]
477mod tests {
478    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
479    use super::*;
480    use crate::Endianness;
481    use crate::encode::{CdrDecode, CdrEncode};
482    use alloc::vec;
483
484    // ---- @final ----
485
486    #[test]
487    fn final_struct_two_u32_members() {
488        let mut w = BufferWriter::new(Endianness::Little);
489        encode_final(&mut w, |w| {
490            42u32.encode(w)?;
491            100u32.encode(w)?;
492            Ok(())
493        })
494        .unwrap();
495        let bytes = w.into_bytes();
496        // Tight-packed: 2 * 4 byte u32 = 8 byte total.
497        assert_eq!(bytes.len(), 8);
498
499        let mut r = BufferReader::new(&bytes, Endianness::Little);
500        let (a, b) = decode_final(&mut r, |r| {
501            Ok::<_, DecodeError>((u32::decode(r)?, u32::decode(r)?))
502        })
503        .unwrap();
504        assert_eq!((a, b), (42, 100));
505    }
506
507    // ---- @appendable ----
508
509    #[test]
510    fn appendable_struct_writes_dheader() {
511        let mut w = BufferWriter::new(Endianness::Little);
512        encode_appendable(&mut w, |w| {
513            42u32.encode(w)?;
514            7u8.encode(w)?;
515            Ok(())
516        })
517        .unwrap();
518        let bytes = w.into_bytes();
519        // DHEADER (4 byte u32 = body length) + body.
520        // Body: u32 (4) + u8 (1) = 5 byte.
521        assert_eq!(&bytes[0..4], &[5, 0, 0, 0]); // DHEADER LE
522        assert_eq!(&bytes[4..8], &[42, 0, 0, 0]); // u32 = 42
523        assert_eq!(bytes[8], 7);
524        assert_eq!(bytes.len(), 9);
525    }
526
527    #[test]
528    fn appendable_struct_roundtrip() {
529        let mut w = BufferWriter::new(Endianness::Little);
530        encode_appendable(&mut w, |w| {
531            42u32.encode(w)?;
532            7u8.encode(w)?;
533            Ok(())
534        })
535        .unwrap();
536        let bytes = w.into_bytes();
537
538        let mut r = BufferReader::new(&bytes, Endianness::Little);
539        let (a, b) = decode_appendable(&mut r, |r| {
540            Ok::<_, DecodeError>((u32::decode(r)?, u8::decode(r)?))
541        })
542        .unwrap();
543        assert_eq!((a, b), (42, 7));
544    }
545
546    #[test]
547    fn appendable_decoder_skips_extra_trailing_bytes() {
548        // Encode a struct with 2 members, but the decoder reads only the
549        // first — the sub-reader trick discards the remaining bytes
550        // without error.
551        let mut w = BufferWriter::new(Endianness::Little);
552        encode_appendable(&mut w, |w| {
553            42u32.encode(w)?;
554            99u8.encode(w)?;
555            Ok(())
556        })
557        .unwrap();
558        let bytes = w.into_bytes();
559
560        let mut r = BufferReader::new(&bytes, Endianness::Little);
561        let only_first = decode_appendable(&mut r, u32::decode).unwrap();
562        assert_eq!(only_first, 42);
563        // The outer reader consumed everything (DHEADER + full body).
564        assert_eq!(r.remaining(), 0);
565    }
566
567    #[test]
568    fn appendable_decoder_rejects_announced_overrun() {
569        let bytes = [0xFFu8, 0xFF, 0xFF, 0xFF, 1, 2, 3];
570        let mut r = BufferReader::new(&bytes, Endianness::Little);
571        let res = decode_appendable(&mut r, u32::decode);
572        assert!(matches!(res, Err(DecodeError::LengthExceeded { .. })));
573    }
574
575    // ---- @mutable ----
576
577    // ---- MutableStructEncoder (XTypes 1.3 §7.4.1.2.3) ----
578
579    #[test]
580    fn mutable_struct_encoder_succeeds_when_all_required_emitted() {
581        let mut w = BufferWriter::new(Endianness::Little);
582        let mut enc = MutableStructEncoder::new(&mut w, vec![1, 2, 3]);
583        enc.encode_member(1, false, |w| 42u32.encode(w)).unwrap();
584        enc.encode_member(2, false, |w| 7u8.encode(w)).unwrap();
585        enc.encode_member(3, false, |w| 99u16.encode(w)).unwrap();
586        enc.finish().unwrap();
587    }
588
589    #[test]
590    fn mutable_encode_omitting_non_optional_member_errors() {
591        let mut w = BufferWriter::new(Endianness::Little);
592        let mut enc = MutableStructEncoder::new(&mut w, vec![1, 2, 3]);
593        enc.encode_member(1, false, |w| 42u32.encode(w)).unwrap();
594        // Member 2 is not emitted — a spec violation.
595        enc.encode_member(3, false, |w| 99u16.encode(w)).unwrap();
596        let err = enc.finish().unwrap_err();
597        assert_eq!(err, EncodeError::MissingNonOptionalMember { member_id: 2 });
598    }
599
600    #[test]
601    fn mutable_encode_first_missing_id_is_reported() {
602        let mut w = BufferWriter::new(Endianness::Little);
603        let mut enc = MutableStructEncoder::new(&mut w, vec![10, 20, 30]);
604        enc.encode_member(20, false, |w| 5u32.encode(w)).unwrap();
605        // 10 and 30 are missing — the encoder reports 10 first.
606        let err = enc.finish().unwrap_err();
607        assert_eq!(err, EncodeError::MissingNonOptionalMember { member_id: 10 });
608    }
609
610    #[test]
611    fn mutable_encode_optional_only_with_no_required_succeeds() {
612        // If all members are optional, required_ids is empty and the
613        // encoder may emit zero members.
614        let mut w = BufferWriter::new(Endianness::Little);
615        let enc = MutableStructEncoder::new(&mut w, vec![]);
616        enc.finish().unwrap();
617    }
618
619    #[test]
620    fn mutable_encode_extra_optional_emitted_does_not_break_finish() {
621        // required = [1]; emitted = [1, 99]; OK — 99 is optional.
622        let mut w = BufferWriter::new(Endianness::Little);
623        let mut enc = MutableStructEncoder::new(&mut w, vec![1]);
624        enc.encode_member(1, false, |w| 42u32.encode(w)).unwrap();
625        enc.encode_member(99, false, |w| 0u8.encode(w)).unwrap();
626        enc.finish().unwrap();
627    }
628
629    #[test]
630    fn mutable_encode_with_lc_variant_tracks_id() {
631        let mut w = BufferWriter::new(Endianness::Little);
632        let mut enc = MutableStructEncoder::new(&mut w, vec![5]);
633        enc.encode_member_lc(5, false, LengthCode::Lc0, |w| 0x42u8.encode(w))
634            .unwrap();
635        enc.finish().unwrap();
636    }
637
638    #[test]
639    fn mutable_member_emheader_layout() {
640        let mut w = BufferWriter::new(Endianness::Little);
641        encode_mutable_member(&mut w, 0x1234, false, |w| 42u32.encode(w)).unwrap();
642        let bytes = w.into_bytes();
643        // EMHEADER LE: m_bit=0, lc=4 (bits 30-28 = 100), member_id=0x1234
644        // → 0x4000_1234
645        assert_eq!(&bytes[0..4], &[0x34, 0x12, 0x00, 0x40]);
646        // NEXTINT = body length = 4
647        assert_eq!(&bytes[4..8], &[4, 0, 0, 0]);
648        // body = u32 LE 42
649        assert_eq!(&bytes[8..12], &[42, 0, 0, 0]);
650    }
651
652    #[test]
653    fn mutable_member_must_understand_sets_high_bit() {
654        let mut w = BufferWriter::new(Endianness::Little);
655        encode_mutable_member(&mut w, 1, true, |w| 0u8.encode(w)).unwrap();
656        let bytes = w.into_bytes();
657        // EMHEADER LE: m_bit=1, lc=4, id=1 → 0xC000_0001
658        assert_eq!(&bytes[0..4], &[0x01, 0x00, 0x00, 0xC0]);
659    }
660
661    #[test]
662    fn mutable_member_rejects_oversized_id() {
663        let mut w = BufferWriter::new(Endianness::Little);
664        let res = encode_mutable_member(&mut w, 0xFFFF_FFFF, false, |w| 0u8.encode(w));
665        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
666    }
667
668    #[test]
669    fn mutable_struct_roundtrip_two_members() {
670        let mut w = BufferWriter::new(Endianness::Little);
671        encode_mutable_member(&mut w, 1, false, |w| 42u32.encode(w)).unwrap();
672        encode_mutable_member(&mut w, 2, true, |w| 7u8.encode(w)).unwrap();
673        let bytes = w.into_bytes();
674
675        let mut r = BufferReader::new(&bytes, Endianness::Little);
676        let members = read_all_mutable_members(&mut r).unwrap();
677        assert_eq!(members.len(), 2);
678        assert_eq!(members[0].member_id, 1);
679        assert!(!members[0].must_understand);
680        assert_eq!(members[1].member_id, 2);
681        assert!(members[1].must_understand);
682
683        let mut sub = BufferReader::new(members[0].body, Endianness::Little);
684        assert_eq!(u32::decode(&mut sub).unwrap(), 42);
685        let mut sub = BufferReader::new(members[1].body, Endianness::Little);
686        assert_eq!(u8::decode(&mut sub).unwrap(), 7);
687    }
688
689    #[test]
690    fn mutable_member_reads_none_on_eof() {
691        let bytes: [u8; 0] = [];
692        let mut r = BufferReader::new(&bytes, Endianness::Little);
693        let res = read_mutable_member(&mut r).unwrap();
694        assert!(res.is_none());
695    }
696
697    // ---- WP 1.A: LC0..7 Encoder/Decoder ----
698
699    #[test]
700    fn lc0_encode_decode_one_byte_body() {
701        let mut w = BufferWriter::new(Endianness::Little);
702        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc0, |w| 0x42u8.encode(w)).unwrap();
703        let bytes = w.into_bytes();
704        assert_eq!(&bytes[0..4], &[0x01, 0x00, 0x00, 0x00]);
705        assert_eq!(bytes[4], 0x42);
706        assert_eq!(bytes.len(), 5);
707        let mut r = BufferReader::new(&bytes, Endianness::Little);
708        let m = read_mutable_member(&mut r).unwrap().unwrap();
709        assert_eq!(m.length_code, LengthCode::Lc0);
710        assert_eq!(m.body, &[0x42]);
711    }
712
713    #[test]
714    fn lc1_encode_decode_two_byte_body() {
715        let mut w = BufferWriter::new(Endianness::Little);
716        encode_mutable_member_lc(&mut w, 7, false, LengthCode::Lc1, |w| 0x1234u16.encode(w))
717            .unwrap();
718        let bytes = w.into_bytes();
719        assert_eq!(bytes.len(), 4 + 2);
720        let mut r = BufferReader::new(&bytes, Endianness::Little);
721        let m = read_mutable_member(&mut r).unwrap().unwrap();
722        assert_eq!(m.length_code, LengthCode::Lc1);
723        assert_eq!(m.body, &[0x34, 0x12]);
724    }
725
726    #[test]
727    fn lc2_encode_decode_four_byte_body() {
728        let mut w = BufferWriter::new(Endianness::Little);
729        encode_mutable_member_lc(&mut w, 9, true, LengthCode::Lc2, |w| 42u32.encode(w)).unwrap();
730        let bytes = w.into_bytes();
731        // m=1, lc=2, id=9 → 0xA000_0009 LE
732        assert_eq!(&bytes[0..4], &[0x09, 0x00, 0x00, 0xA0]);
733        assert_eq!(bytes.len(), 4 + 4);
734        let mut r = BufferReader::new(&bytes, Endianness::Little);
735        let m = read_mutable_member(&mut r).unwrap().unwrap();
736        assert_eq!(m.length_code, LengthCode::Lc2);
737        assert!(m.must_understand);
738    }
739
740    #[test]
741    fn lc3_encode_decode_eight_byte_body() {
742        let mut w = BufferWriter::new(Endianness::Little);
743        encode_mutable_member_lc(&mut w, 11, false, LengthCode::Lc3, |w| {
744            0xDEADBEEF_CAFEBABEu64.encode(w)
745        })
746        .unwrap();
747        let bytes = w.into_bytes();
748        assert_eq!(bytes.len(), 4 + 8);
749        let mut r = BufferReader::new(&bytes, Endianness::Little);
750        let m = read_mutable_member(&mut r).unwrap().unwrap();
751        assert_eq!(m.length_code, LengthCode::Lc3);
752        assert_eq!(m.body.len(), 8);
753    }
754
755    #[test]
756    fn lc4_default_path_unchanged() {
757        let mut w = BufferWriter::new(Endianness::Little);
758        encode_mutable_member(&mut w, 1, false, |w| 42u32.encode(w)).unwrap();
759        let bytes = w.into_bytes();
760        assert_eq!(bytes.len(), 4 + 4 + 4);
761        let mut r = BufferReader::new(&bytes, Endianness::Little);
762        let m = read_mutable_member(&mut r).unwrap().unwrap();
763        assert_eq!(m.length_code, LengthCode::Lc4);
764    }
765
766    #[test]
767    fn lc5_aggregate_with_dheader() {
768        let mut w = BufferWriter::new(Endianness::Little);
769        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc5, |w| {
770            8u32.encode(w)?;
771            42u32.encode(w)?;
772            7u32.encode(w)?;
773            Ok(())
774        })
775        .unwrap();
776        let bytes = w.into_bytes();
777        assert_eq!(bytes.len(), 20);
778        assert_eq!(&bytes[4..8], &[12, 0, 0, 0]);
779        let mut r = BufferReader::new(&bytes, Endianness::Little);
780        let m = read_mutable_member(&mut r).unwrap().unwrap();
781        assert_eq!(m.length_code, LengthCode::Lc5);
782        assert_eq!(m.body.len(), 12);
783    }
784
785    #[test]
786    fn lc6_array_of_4byte_primitives() {
787        let mut w = BufferWriter::new(Endianness::Little);
788        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |w| {
789            12u32.encode(w)?;
790            10u32.encode(w)?;
791            20u32.encode(w)?;
792            30u32.encode(w)?;
793            Ok(())
794        })
795        .unwrap();
796        let bytes = w.into_bytes();
797        assert_eq!(&bytes[4..8], &[3, 0, 0, 0]);
798        assert_eq!(bytes.len(), 4 + 4 + 16);
799        let mut r = BufferReader::new(&bytes, Endianness::Little);
800        let m = read_mutable_member(&mut r).unwrap().unwrap();
801        assert_eq!(m.length_code, LengthCode::Lc6);
802        assert_eq!(m.body.len(), 16);
803    }
804
805    #[test]
806    fn lc6_lc7_roundtrip_against_cyclone_sample() {
807        // Spec §7.4.3.4.2: LC=6 for 4-byte-element arrays; LC=7 for
808        // 8-byte-element arrays. Both encoders produce the same wire
809        // layout that Cyclone DDS and FastDDS can decode. We verify three
810        // places byte-exactly:
811        //   - LC=6 EMHEADER (bits 30-28 = 110)
812        //   - NEXTINT = element count (4 bytes)
813        //   - DHEADER + payload
814        let mut w = BufferWriter::new(Endianness::Little);
815        // LC=6 body layout: DHEADER (4) + 4n element bytes.
816        // 100 u32 elements = 400 bytes → body_len = 404.
817        encode_mutable_member_lc(&mut w, 0xABCD, false, LengthCode::Lc6, |w| {
818            // DHEADER: gives the number of element bytes (400).
819            400u32.encode(w)?;
820            for i in 0..100u32 {
821                i.encode(w)?;
822            }
823            Ok(())
824        })
825        .unwrap();
826        let bytes = w.into_bytes();
827
828        // EMHEADER: must_understand=0, lc=6, member_id=0xABCD.
829        // → 0x6000_ABCD LE = [0xCD, 0xAB, 0x00, 0x60].
830        assert_eq!(&bytes[0..4], &[0xCD, 0xAB, 0x00, 0x60]);
831        // NEXTINT = element count = 100 LE.
832        assert_eq!(&bytes[4..8], &[100, 0, 0, 0]);
833        // Payload: DHEADER 4 + 100 * 4 = 404 bytes starting at offset 8.
834        assert_eq!(bytes.len(), 8 + 404);
835
836        // Decoder accepts.
837        let mut r = BufferReader::new(&bytes, Endianness::Little);
838        let m = read_mutable_member(&mut r).unwrap().unwrap();
839        assert_eq!(m.length_code, LengthCode::Lc6);
840        assert_eq!(m.member_id, 0xABCD);
841        assert_eq!(m.body.len(), 404);
842    }
843
844    #[test]
845    fn lc6_with_many_elements_decodes_correctly() {
846        // 70_000 elements — the encoder writes LC=6 with a large NEXTINT.
847        // Verifies that the decoder reads the >= u16 NEXTINT correctly
848        // (no silent truncation).
849        let mut w = BufferWriter::new(Endianness::Little);
850        encode_mutable_member_lc(&mut w, 5, false, LengthCode::Lc6, |w| {
851            // DHEADER = element byte count (70_000 * 4 = 280_000).
852            280_000u32.encode(w)?;
853            for i in 0..70_000u32 {
854                i.encode(w)?;
855            }
856            Ok(())
857        })
858        .unwrap();
859        let bytes = w.into_bytes();
860        let nextint = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
861        assert_eq!(nextint, 70_000);
862        let mut r = BufferReader::new(&bytes, Endianness::Little);
863        let m = read_mutable_member(&mut r).unwrap().unwrap();
864        // body including DHEADER = 4 + 280_000.
865        assert_eq!(m.body.len(), 4 + 70_000 * 4);
866    }
867
868    #[test]
869    fn lc7_array_of_8byte_primitives() {
870        let mut w = BufferWriter::new(Endianness::Little);
871        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc7, |w| {
872            16u32.encode(w)?;
873            w.write_bytes(&100u64.to_le_bytes())?;
874            w.write_bytes(&200u64.to_le_bytes())?;
875            Ok(())
876        })
877        .unwrap();
878        let bytes = w.into_bytes();
879        assert_eq!(&bytes[4..8], &[2, 0, 0, 0]);
880        assert_eq!(bytes.len(), 4 + 4 + 20);
881        let mut r = BufferReader::new(&bytes, Endianness::Little);
882        let m = read_mutable_member(&mut r).unwrap().unwrap();
883        assert_eq!(m.length_code, LengthCode::Lc7);
884        assert_eq!(m.body.len(), 20);
885    }
886
887    #[test]
888    fn lc0_rejects_wrong_body_size() {
889        let mut w = BufferWriter::new(Endianness::Little);
890        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc0, |w| 42u32.encode(w));
891        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
892    }
893
894    #[test]
895    fn lc6_rejects_misaligned_body() {
896        let mut w = BufferWriter::new(Endianness::Little);
897        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |w| {
898            0u32.encode(w)?;
899            0u8.encode(w)?;
900            0u8.encode(w)?;
901            0u8.encode(w)?;
902            Ok(())
903        });
904        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
905    }
906
907    #[test]
908    fn length_code_body_len_calculation() {
909        assert_eq!(LengthCode::Lc0.body_len(0), 1);
910        assert_eq!(LengthCode::Lc1.body_len(0), 2);
911        assert_eq!(LengthCode::Lc2.body_len(0), 4);
912        assert_eq!(LengthCode::Lc3.body_len(0), 8);
913        assert_eq!(LengthCode::Lc4.body_len(100), 100);
914        assert_eq!(LengthCode::Lc5.body_len(20), 20);
915        assert_eq!(LengthCode::Lc6.body_len(3), 16);
916        assert_eq!(LengthCode::Lc7.body_len(2), 20);
917    }
918
919    #[test]
920    fn length_code_has_nextint_flag() {
921        assert!(!LengthCode::Lc0.has_nextint());
922        assert!(!LengthCode::Lc3.has_nextint());
923        assert!(LengthCode::Lc4.has_nextint());
924        assert!(LengthCode::Lc7.has_nextint());
925    }
926
927    #[test]
928    fn length_code_from_wire_roundtrip() {
929        for v in 0..=7u8 {
930            let lc = LengthCode::from_wire(v).expect("valid");
931            assert_eq!(lc as u8, v);
932        }
933        assert!(LengthCode::from_wire(8).is_none());
934    }
935
936    // ---- Mixed nesting ----
937
938    #[test]
939    fn appendable_in_mutable_member() {
940        let mut w = BufferWriter::new(Endianness::Little);
941        encode_mutable_member(&mut w, 5, false, |w| {
942            encode_appendable(w, |w| {
943                42u32.encode(w)?;
944                100u32.encode(w)?;
945                Ok(())
946            })
947        })
948        .unwrap();
949        let bytes = w.into_bytes();
950        let mut r = BufferReader::new(&bytes, Endianness::Little);
951        let m = read_mutable_member(&mut r).unwrap().unwrap();
952        assert_eq!(m.member_id, 5);
953        let mut sub = BufferReader::new(m.body, Endianness::Little);
954        let (a, b) = decode_appendable(&mut sub, |r| {
955            Ok::<_, DecodeError>((u32::decode(r)?, u32::decode(r)?))
956        })
957        .unwrap();
958        assert_eq!((a, b), (42, 100));
959    }
960
961    // ---- Mutation killers for encode_mutable_member_lc ----
962
963    /// Catches the `>` -> `>=` mutation on the member_id boundary.
964    /// member_id == 0x0FFFFFFF (= 28-bit MAX) must PASS.
965    #[test]
966    fn mutable_member_id_at_28bit_max_accepted() {
967        let mut w = BufferWriter::new(Endianness::Little);
968        let res = encode_mutable_member_lc(&mut w, 0x0FFF_FFFF, false, LengthCode::Lc2, |inner| {
969            u32::encode(&0u32, inner)
970        });
971        assert!(
972            res.is_ok(),
973            "member_id=0x0FFFFFFF must succeed, got {res:?}"
974        );
975    }
976
977    /// A member_id above 28 bits must be REJECTED.
978    /// Catches the `>` -> `==` mutation on the same line (=> only an exact
979    /// match would error, all higher values would pass).
980    #[test]
981    fn mutable_member_id_29bit_rejected() {
982        let mut w = BufferWriter::new(Endianness::Little);
983        let res = encode_mutable_member_lc(&mut w, 0x1000_0000, false, LengthCode::Lc2, |inner| {
984            u32::encode(&0u32, inner)
985        });
986        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
987    }
988
989    /// Catches the `<` -> `==` mutation on body_len < 4 in Lc6.
990    /// body_len < 4 must error — regardless of the value.
991    #[test]
992    fn lc6_body_len_less_than_4_rejected() {
993        for short_len in [0usize, 1, 2, 3] {
994            let mut w = BufferWriter::new(Endianness::Little);
995            let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |inner| {
996                inner.write_bytes(&vec![0u8; short_len])
997            });
998            assert!(
999                matches!(res, Err(EncodeError::ValueOutOfRange { .. })),
1000                "Lc6 with body_len={short_len} must error, got {res:?}"
1001            );
1002        }
1003    }
1004
1005    /// Catches the `<` -> `<=` mutation: body_len == 4 (DHEADER alone, n=0)
1006    /// must PASS for Lc6 (4-4=0, 0%4=0).
1007    #[test]
1008    fn lc6_body_len_exactly_4_accepted() {
1009        let mut w = BufferWriter::new(Endianness::Little);
1010        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |inner| {
1011            inner.write_bytes(&[0u8; 4])
1012        });
1013        assert!(res.is_ok(), "Lc6 body_len=4 must succeed, got {res:?}");
1014    }
1015
1016    /// Catches the `-` -> `+` mutation on `(body_len - 4) / 4` for Lc6.
1017    /// nextint must be EXACTLY (body_len - 4) / 4, not (body_len + 4) / 4.
1018    /// body_len=12 → original n=2, mutated n=4.
1019    #[test]
1020    fn lc6_nextint_value_is_minus_4_div_4() {
1021        let mut w = BufferWriter::new(Endianness::Little);
1022        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |inner| {
1023            inner.write_bytes(&[0u8; 12])
1024        })
1025        .unwrap();
1026        let bytes = w.into_bytes();
1027        // Layout: 4-byte EMHEADER + 4-byte nextint + 12-byte body = 20.
1028        assert_eq!(bytes.len(), 20);
1029        // nextint = bytes[4..8] LE
1030        let mut ni = [0u8; 4];
1031        ni.copy_from_slice(&bytes[4..8]);
1032        let nextint = u32::from_le_bytes(ni);
1033        assert_eq!(nextint, 2, "nextint must be (12-4)/4=2, not (12+4)/4=4");
1034    }
1035
1036    /// Lc7 variant: same mutations as Lc6 but with `% 8` and `/ 8`.
1037    /// body_len < 4 must error.
1038    #[test]
1039    fn lc7_body_len_less_than_4_rejected() {
1040        for short_len in [0usize, 1, 2, 3] {
1041            let mut w = BufferWriter::new(Endianness::Little);
1042            let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc7, |inner| {
1043                inner.write_bytes(&vec![0u8; short_len])
1044            });
1045            assert!(
1046                matches!(res, Err(EncodeError::ValueOutOfRange { .. })),
1047                "Lc7 with body_len={short_len} must error"
1048            );
1049        }
1050    }
1051
1052    /// Lc7 body_len==4 (DHEADER + 0 elements) must pass.
1053    /// Catches the `<` -> `<=` mutation.
1054    #[test]
1055    fn lc7_body_len_exactly_4_accepted() {
1056        let mut w = BufferWriter::new(Endianness::Little);
1057        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc7, |inner| {
1058            inner.write_bytes(&[0u8; 4])
1059        });
1060        assert!(res.is_ok());
1061    }
1062
1063    /// Lc7 nextint = (body_len - 4) / 8. body_len=20 → n=2 original,
1064    /// n=3 with the `+` mutation.
1065    #[test]
1066    fn lc7_nextint_value_is_minus_4_div_8() {
1067        let mut w = BufferWriter::new(Endianness::Little);
1068        encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc7, |inner| {
1069            inner.write_bytes(&[0u8; 20])
1070        })
1071        .unwrap();
1072        let bytes = w.into_bytes();
1073        // 4 EMHEADER + 4 nextint + 20 body = 28
1074        assert_eq!(bytes.len(), 28);
1075        let mut ni = [0u8; 4];
1076        ni.copy_from_slice(&bytes[4..8]);
1077        let nextint = u32::from_le_bytes(ni);
1078        assert_eq!(nextint, 2, "nextint must be (20-4)/8=2, not (20+4)/8=3");
1079    }
1080
1081    /// Lc6 body_len=8 must pass ((8-4)%4=0 ok, so pass — no boundary
1082    /// fail). Here we test Lc6 body_len=6: (6-4)%4=2 ≠ 0 → error.
1083    /// Catches `||` -> `&&` (the line was not directly missed, but this
1084    /// test is for completeness).
1085    #[test]
1086    fn lc6_misaligned_body_len_rejected() {
1087        let mut w = BufferWriter::new(Endianness::Little);
1088        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc6, |inner| {
1089            inner.write_bytes(&[0u8; 6])
1090        });
1091        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
1092    }
1093
1094    /// Lc7 misaligned body. Catches `||` -> `&&`.
1095    /// body_len=12: (12-4)%8 = 8%8 = 0 ok. Need body_len=10: (10-4)%8 = 6.
1096    #[test]
1097    fn lc7_misaligned_body_len_rejected() {
1098        let mut w = BufferWriter::new(Endianness::Little);
1099        let res = encode_mutable_member_lc(&mut w, 1, false, LengthCode::Lc7, |inner| {
1100            inner.write_bytes(&[0u8; 10])
1101        });
1102        assert!(matches!(res, Err(EncodeError::ValueOutOfRange { .. })));
1103    }
1104
1105    /// EMHEADER must_understand bit + LC bits are set correctly.
1106    /// Catches `|` -> `^`/`-`/`*` mutations on the EMHEADER construction
1107    /// (after the refactor to `+`).
1108    #[test]
1109    fn emheader_combines_must_understand_lc_and_member_id() {
1110        let mut w = BufferWriter::new(Endianness::Little);
1111        encode_mutable_member_lc(&mut w, 0x123_4567, true, LengthCode::Lc6, |inner| {
1112            inner.write_bytes(&[0u8; 4])
1113        })
1114        .unwrap();
1115        let bytes = w.into_bytes();
1116        let mut h = [0u8; 4];
1117        h.copy_from_slice(&bytes[..4]);
1118        let emheader = u32::from_le_bytes(h);
1119        // m_bit (Bit 31) = 0x8000_0000
1120        // lc_bits (Lc6 = 6 << 28) = 0x6000_0000
1121        // member_id = 0x0123_4567
1122        // Sum = 0x8000_0000 + 0x6000_0000 + 0x0123_4567 = 0xE123_4567
1123        assert_eq!(emheader, 0xE123_4567);
1124    }
1125}