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