Skip to main content

zerodds_types/dynamic/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Reflective XCDR codec: CDR bytes ⟷ [`DynamicData`], driven by a runtime
4//! [`DynamicType`].
5//!
6//! The DDS CDR codec is mature + interop-hardened (the 4-vendor ×
7//! 3-extensibility matrix) but **monomorphic** — `DdsType::decode` per generated
8//! Rust type. This module is the missing **reflective** path: it walks a runtime
9//! `DynamicType` and reuses the SAME hardened wire framing from
10//! `zerodds_cdr::struct_enc` (`decode_appendable` / `read_all_mutable_members` +
11//! `encode_appendable` / `encode_mutable_member`) and the `BufferReader` /
12//! `BufferWriter` primitives. No DHEADER/EMHEADER/alignment logic is
13//! reimplemented — only the type-driven member walk lives here.
14//!
15//! Supported (both directions, byte-exact round-trip tested):
16//! - primitives, enum, `string`, `wstring` (via the hardened `zerodds_cdr::WString`);
17//! - struct FINAL / APPENDABLE / MUTABLE; nested struct;
18//! - union FINAL / APPENDABLE / MUTABLE (PL_CDR2: discriminator = EMHEADER member
19//!   id 0, selected branch = its own EMHEADER member), with the discriminator
20//!   encoded at its REAL kind (bool/char/int8/16/32/64/enum — not int32);
21//! - sequences + arrays of scalar AND composite (struct/union) elements, incl.
22//!   the XCDR2 collection DHEADER for non-primitive elements. Composite element
23//!   types are carried via [`crate::dynamic::collection`] (a resolved element
24//!   `DynamicType`); scalar elements are rebuilt from the shallow descriptor.
25//! - alias (typedef) members whose target is a scalar (composite typedefs are
26//!   resolved to their underlying type at IDL-lowering time).
27//!
28//! Residual (honest `NotSupported`, never silent-wrong):
29//! - `long double` / `Float128` — blocked on stable Rust `f128`;
30//! - `bitmask` / `bitset` are lowered to their underlying wire integer at
31//!   IDL-lowering time (so they decode correctly as integers); the codec keeps no
32//!   dedicated kind for them.
33
34use alloc::boxed::Box;
35use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38use zerodds_cdr::buffer::{BufferReader, BufferWriter};
39use zerodds_cdr::endianness::Endianness;
40use zerodds_cdr::error::{DecodeError, EncodeError};
41use zerodds_cdr::struct_enc::{
42    decode_appendable, encode_appendable, encode_mutable_member, read_all_mutable_members,
43};
44use zerodds_cdr::xcdr1::{encode_pl_cdr1_member, read_all_pl_cdr1_members, write_pl_cdr1_sentinel};
45use zerodds_cdr::{CdrDecode, CdrEncode, WString};
46
47use crate::dynamic::data::{DynamicData, DynamicValue};
48use crate::dynamic::descriptor::{ExtensibilityKind, TypeKind};
49use crate::dynamic::type_::DynamicType;
50
51/// Reflective-codec error.
52#[derive(Debug)]
53pub enum CodecError {
54    /// A wire-level read/write failure from the underlying CDR layer.
55    Wire(String),
56    /// A `DynamicData` API failure (type mismatch, unknown member …).
57    Dynamic(String),
58    /// A construct the reflective path does not yet support.
59    NotSupported(String),
60}
61
62impl core::fmt::Display for CodecError {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        match self {
65            Self::Wire(m) => write!(f, "wire: {m}"),
66            Self::Dynamic(m) => write!(f, "dynamic: {m}"),
67            Self::NotSupported(m) => write!(f, "not supported: {m}"),
68        }
69    }
70}
71
72type R<T> = Result<T, CodecError>;
73
74const fn endian(big: bool) -> Endianness {
75    if big {
76        Endianness::Big
77    } else {
78        Endianness::Little
79    }
80}
81
82/// Sentinel errors used to bail out of the cdr closure helpers; the real
83/// [`CodecError`] is captured out-of-band and re-surfaced by the caller.
84fn dec_sentinel() -> DecodeError {
85    DecodeError::UnexpectedEof {
86        needed: 0,
87        offset: 0,
88    }
89}
90fn enc_sentinel() -> EncodeError {
91    EncodeError::ValueOutOfRange {
92        message: "dynamic codec inner error",
93    }
94}
95
96/// `true` for fixed-width scalar kinds (incl. enum) — collections of these get
97/// NO XCDR2 collection DHEADER (mirrors `cdr::composite::needs_collection_dheader`).
98const fn is_primitive_kind(kind: TypeKind) -> bool {
99    matches!(
100        kind,
101        TypeKind::Boolean
102            | TypeKind::Byte
103            | TypeKind::Int8
104            | TypeKind::UInt8
105            | TypeKind::Int16
106            | TypeKind::UInt16
107            | TypeKind::Int32
108            | TypeKind::UInt32
109            | TypeKind::Int64
110            | TypeKind::UInt64
111            | TypeKind::Float32
112            | TypeKind::Float64
113            | TypeKind::Char8
114            | TypeKind::Char16
115            | TypeKind::Enumeration
116    )
117}
118
119/// Build a `DynamicType` for a scalar (primitive or string) element/target
120/// described by `desc`. Returns `None` for composite kinds (the documented
121/// collection-of-composite / alias-to-composite residual).
122fn scalar_dynamic_type(desc: &crate::dynamic::descriptor::TypeDescriptor) -> Option<DynamicType> {
123    use crate::dynamic::builder::DynamicTypeBuilderFactory as F;
124    match desc.kind {
125        TypeKind::String8 => Some(F::create_string_type(
126            desc.bound.first().copied().unwrap_or(0),
127        )),
128        TypeKind::String16 => Some(F::create_wstring_type(
129            desc.bound.first().copied().unwrap_or(0),
130        )),
131        k if is_primitive_kind(k) => Some(DynamicType::new_primitive(k)),
132        _ => None,
133    }
134}
135
136/// The reserved EMHEADER member id of a union's discriminator in PL_CDR(2)
137/// framing (DDS-XTypes 1.3 §7.2.2.4.4.4.6: `DISCRIMINATOR_ID`).
138const DISCRIMINATOR_MEMBER_ID: u32 = 0;
139
140/// Read an integral union discriminator of `kind`, returned as `i64` (signed
141/// kinds are sign-extended). Mirrors the typed codec's per-kind discriminator
142/// encoding — NOT hard-coded to int32.
143fn read_discriminator(r: &mut BufferReader<'_>, kind: TypeKind) -> R<i64> {
144    let w = |e: DecodeError| CodecError::Wire(e.to_string());
145    Ok(match kind {
146        TypeKind::Boolean | TypeKind::Byte | TypeKind::UInt8 | TypeKind::Char8 => {
147            i64::from(r.read_u8().map_err(w)?)
148        }
149        TypeKind::Int8 => i64::from(r.read_u8().map_err(w)? as i8),
150        TypeKind::UInt16 | TypeKind::Char16 => i64::from(r.read_u16().map_err(w)?),
151        TypeKind::Int16 => i64::from(r.read_u16().map_err(w)? as i16),
152        TypeKind::UInt32 | TypeKind::Enumeration => i64::from(r.read_u32().map_err(w)?),
153        TypeKind::Int32 => i64::from(r.read_u32().map_err(w)? as i32),
154        TypeKind::UInt64 => r.read_u64().map_err(w)? as i64,
155        TypeKind::Int64 => r.read_u64().map_err(w)? as i64,
156        other => {
157            return Err(CodecError::NotSupported(alloc::format!(
158                "union discriminator kind {other:?}"
159            )));
160        }
161    })
162}
163
164/// Write an integral union discriminator `val` as `kind`.
165fn write_discriminator(w: &mut BufferWriter, kind: TypeKind, val: i64) -> R<()> {
166    let e = |x: EncodeError| CodecError::Wire(x.to_string());
167    match kind {
168        TypeKind::Boolean | TypeKind::Byte | TypeKind::UInt8 | TypeKind::Char8 | TypeKind::Int8 => {
169            w.write_u8(val as u8).map_err(e)
170        }
171        TypeKind::UInt16 | TypeKind::Char16 | TypeKind::Int16 => w.write_u16(val as u16).map_err(e),
172        TypeKind::UInt32 | TypeKind::Enumeration | TypeKind::Int32 => {
173            w.write_u32(val as u32).map_err(e)
174        }
175        TypeKind::UInt64 | TypeKind::Int64 => w.write_u64(val as u64).map_err(e),
176        other => Err(CodecError::NotSupported(alloc::format!(
177            "union discriminator kind {other:?}"
178        ))),
179    }
180}
181
182/// The discriminator kind of a union type (from its descriptor), defaulting to
183/// `Int32` when absent.
184fn union_discriminator_kind(ty: &DynamicType) -> TypeKind {
185    ty.descriptor()
186        .discriminator_type
187        .as_ref()
188        .map_or(TypeKind::Int32, |d| d.kind)
189}
190
191// ----------------------------------------------------------------------------
192// Decode
193// ----------------------------------------------------------------------------
194
195/// Decode `bytes` (a CDR payload WITHOUT the encapsulation header) into a
196/// [`DynamicData`] of `ty`. `xcdr2` selects XCDR2 vs XCDR1 framing; `big_endian`
197/// the byte order — both come from the sample's encapsulation id on the wire.
198///
199/// # Errors
200/// [`CodecError`] on a wire failure, a `DynamicData` failure, or an unsupported
201/// construct.
202pub fn decode_dynamic(
203    ty: &DynamicType,
204    bytes: &[u8],
205    xcdr2: bool,
206    big_endian: bool,
207) -> R<DynamicData> {
208    let mut r = BufferReader::new(bytes, endian(big_endian));
209    if xcdr2 {
210        r = r.xcdr2();
211    }
212    decode_aggregate(ty, &mut r, xcdr2)
213}
214
215fn decode_aggregate(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
216    match ty.kind() {
217        TypeKind::Structure => decode_struct(ty, r, xcdr2),
218        TypeKind::Union => decode_union(ty, r, xcdr2),
219        other => Err(CodecError::NotSupported(alloc::format!(
220            "top-level type kind {other:?} is not an aggregate"
221        ))),
222    }
223}
224
225fn decode_struct(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
226    match ty.descriptor().extensibility_kind {
227        ExtensibilityKind::Final => {
228            // FINAL never carries a DHEADER — read members in declared order.
229            let mut data = DynamicData::new(ty.clone());
230            read_members_in_order(ty, r, xcdr2, &mut data)?;
231            Ok(data)
232        }
233        ExtensibilityKind::Appendable => {
234            // XCDR1: appendable is plain (no DHEADER), exactly like final.
235            if !xcdr2 {
236                let mut data = DynamicData::new(ty.clone());
237                read_members_in_order(ty, r, xcdr2, &mut data)?;
238                return Ok(data);
239            }
240            let mut inner: Option<CodecError> = None;
241            let res = decode_appendable(r, |sub| {
242                let mut data = DynamicData::new(ty.clone());
243                match read_members_in_order(ty, sub, xcdr2, &mut data) {
244                    Ok(()) => Ok(data),
245                    Err(e) => {
246                        inner = Some(e);
247                        Err(dec_sentinel())
248                    }
249                }
250            });
251            match res {
252                Ok(d) => Ok(d),
253                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
254            }
255        }
256        ExtensibilityKind::Mutable => {
257            if xcdr2 {
258                decode_struct_mutable(ty, r, xcdr2)
259            } else {
260                decode_struct_pl_cdr1(ty, r)
261            }
262        }
263    }
264}
265
266fn read_members_in_order(
267    ty: &DynamicType,
268    r: &mut BufferReader<'_>,
269    xcdr2: bool,
270    data: &mut DynamicData,
271) -> R<()> {
272    for i in 0..ty.member_count() {
273        let m = ty
274            .member_by_index(i)
275            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
276        let mt = m.dynamic_type().clone();
277        let id = m.id();
278        let val = read_value(&mt, r, xcdr2)?;
279        data.set_value_raw(id, val);
280    }
281    Ok(())
282}
283
284fn decode_struct_mutable(
285    ty: &DynamicType,
286    r: &mut BufferReader<'_>,
287    xcdr2: bool,
288) -> R<DynamicData> {
289    // PL_CDR2: a DHEADER frames the EMHEADER parameter list (XTypes 1.3
290    // §7.4.3.4.2). `decode_appendable` strips that DHEADER and bounds the
291    // sub-reader so a nested mutable member stops at its own frame — exactly
292    // what the typed codec does (`decode_appendable` around
293    // `read_all_mutable_members`).
294    let mut err: Option<CodecError> = None;
295    let res = decode_appendable(r, |sub| {
296        let members = read_all_mutable_members(sub)?;
297        let mut data = DynamicData::new(ty.clone());
298        for mm in members {
299            let Some(m) = ty.member_by_id(mm.member_id) else {
300                continue; // unknown member id → skip (forward-compat)
301            };
302            let mt = m.dynamic_type().clone();
303            let mut bsub = BufferReader::new(mm.body, sub.endianness());
304            if xcdr2 {
305                bsub = bsub.xcdr2();
306            }
307            match read_value(&mt, &mut bsub, xcdr2) {
308                Ok(val) => data.set_value_raw(m.id(), val),
309                Err(e) => {
310                    err = Some(e);
311                    return Err(dec_sentinel());
312                }
313            }
314        }
315        Ok(data)
316    });
317    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
318}
319
320/// XCDR1 (`PL_CDR`) decode of a `@mutable` struct: the 16-bit-PID parameter list
321/// terminated by `PID_LIST_END`. XCDR1 alignment (max-align 8) is the default of
322/// the per-member sub-reader.
323fn decode_struct_pl_cdr1(ty: &DynamicType, r: &mut BufferReader<'_>) -> R<DynamicData> {
324    let members = read_all_pl_cdr1_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
325    let mut data = DynamicData::new(ty.clone());
326    for mm in members {
327        let Some(m) = ty.member_by_id(mm.member_id) else {
328            continue; // unknown member id → skip
329        };
330        let mt = m.dynamic_type().clone();
331        let mut bsub = BufferReader::new(&mm.body, r.endianness());
332        let val = read_value(&mt, &mut bsub, false)?;
333        data.set_value_raw(m.id(), val);
334    }
335    Ok(data)
336}
337
338/// XCDR1 (`PL_CDR`) decode of a `@mutable` union: discriminator = PID 0, the
339/// single other PID = the selected branch (matched by position, vendor-agnostic).
340fn decode_union_pl_cdr1(ty: &DynamicType, r: &mut BufferReader<'_>) -> R<DynamicData> {
341    let members = read_all_pl_cdr1_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
342    let mut data = DynamicData::new(ty.clone());
343    let disc_idx = members
344        .iter()
345        .position(|m| m.member_id == DISCRIMINATOR_MEMBER_ID)
346        .ok_or_else(|| {
347            CodecError::Wire("mutable union (PL_CDR1): no discriminator member id 0".to_string())
348        })?;
349    let mut dsub = BufferReader::new(&members[disc_idx].body, r.endianness());
350    let disc = read_discriminator(&mut dsub, union_discriminator_kind(ty))?;
351
352    let mut chosen = None;
353    let mut default = None;
354    for m in ty.members() {
355        let d = m.descriptor();
356        if d.is_default_label {
357            default = Some(m);
358        }
359        if d.label.contains(&disc) {
360            chosen = Some(m);
361            break;
362        }
363    }
364    if let Some(m) = chosen.or(default) {
365        if let Some(bw) = members
366            .iter()
367            .enumerate()
368            .find(|(i, _)| *i != disc_idx)
369            .map(|(_, w)| w)
370        {
371            let mt = m.dynamic_type().clone();
372            let mut bsub = BufferReader::new(&bw.body, r.endianness());
373            let val = read_value(&mt, &mut bsub, false)?;
374            data.set_value_raw(m.id(), val);
375        }
376    }
377    Ok(data)
378}
379
380fn decode_union(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
381    match ty.descriptor().extensibility_kind {
382        ExtensibilityKind::Final => decode_union_body(ty, r, xcdr2),
383        ExtensibilityKind::Appendable => {
384            // XCDR1: appendable union = plain disc+branch (no DHEADER).
385            if !xcdr2 {
386                return decode_union_body(ty, r, xcdr2);
387            }
388            let mut inner: Option<CodecError> = None;
389            let res = decode_appendable(r, |sub| match decode_union_body(ty, sub, xcdr2) {
390                Ok(d) => Ok(d),
391                Err(e) => {
392                    inner = Some(e);
393                    Err(dec_sentinel())
394                }
395            });
396            match res {
397                Ok(d) => Ok(d),
398                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
399            }
400        }
401        ExtensibilityKind::Mutable => {
402            if xcdr2 {
403                decode_union_mutable(ty, r, xcdr2)
404            } else {
405                decode_union_pl_cdr1(ty, r)
406            }
407        }
408    }
409}
410
411/// XTypes 1.3 PL_CDR(2) union (§7.4.3.4 / §7.2.2.4.4.4.6): a parameter list of
412/// EMHEADER-framed members, exactly like a mutable struct. The discriminator is
413/// the member with the reserved id [`DISCRIMINATOR_MEMBER_ID`] (0); the selected
414/// branch is the member whose id matches the chosen case member. Fully
415/// data-driven — branch ids are read from the wire and matched against the
416/// `DynamicType` (both derived from the same TypeObject), so no id convention is
417/// assumed beyond the spec-fixed discriminator id 0.
418fn decode_union_mutable(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
419    // PL_CDR2 frames the parameter list with a DHEADER (like a mutable struct) —
420    // strip + bound it via `decode_appendable` so a nested union member stops at
421    // its own frame.
422    let mut err: Option<CodecError> = None;
423    let res = decode_appendable(r, |sub| match decode_union_mutable_body(ty, sub, xcdr2) {
424        Ok(d) => Ok(d),
425        Err(e) => {
426            err = Some(e);
427            Err(dec_sentinel())
428        }
429    });
430    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
431}
432
433/// The PL_CDR2 union member walk, on a reader already scoped to the DHEADER body.
434fn decode_union_mutable_body(
435    ty: &DynamicType,
436    r: &mut BufferReader<'_>,
437    xcdr2: bool,
438) -> R<DynamicData> {
439    let members = read_all_mutable_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
440    let mut data = DynamicData::new(ty.clone());
441
442    // The discriminator is the member with the reserved id 0. Track its wire slot
443    // so a branch that happens to also carry id 0 (an out-of-band IDL may number a
444    // case 0; vendors reserve 0) is matched from the REMAINING slots.
445    let disc_idx = members
446        .iter()
447        .position(|m| m.member_id == DISCRIMINATOR_MEMBER_ID)
448        .ok_or_else(|| {
449            CodecError::Wire(
450                "mutable union: no discriminator member (id 0) on the wire".to_string(),
451            )
452        })?;
453    let mut dsub = BufferReader::new(members[disc_idx].body, r.endianness());
454    if xcdr2 {
455        dsub = dsub.xcdr2();
456    }
457    let disc = read_discriminator(&mut dsub, union_discriminator_kind(ty))?;
458
459    let mut chosen = None;
460    let mut default = None;
461    for m in ty.members() {
462        let d = m.descriptor();
463        if d.is_default_label {
464            default = Some(m);
465        }
466        if d.label.contains(&disc) {
467            chosen = Some(m);
468            break;
469        }
470    }
471    if let Some(m) = chosen.or(default) {
472        // A union serializes the discriminator + at most ONE selected branch, so
473        // the single non-discriminator wire member IS that branch — regardless of
474        // the member id the writer assigned it. (Vendors number union cases from 1
475        // after the id-0 discriminator, e.g. FastDDS emits case id 2 for the 2nd
476        // case; a DynamicType built from an out-of-band IDL numbers them from 0.
477        // Matching by position, not id, makes the decode vendor-agnostic.) A
478        // valueless branch is simply absent.
479        if let Some(bw) = members
480            .iter()
481            .enumerate()
482            .find(|(i, _)| *i != disc_idx)
483            .map(|(_, w)| w)
484        {
485            let mt = m.dynamic_type().clone();
486            let mut bsub = BufferReader::new(bw.body, r.endianness());
487            if xcdr2 {
488                bsub = bsub.xcdr2();
489            }
490            let val = read_value(&mt, &mut bsub, xcdr2)?;
491            data.set_value_raw(m.id(), val);
492        }
493    }
494    Ok(data)
495}
496
497fn decode_union_body(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
498    let disc = read_discriminator(r, union_discriminator_kind(ty))?;
499    let mut data = DynamicData::new(ty.clone());
500    let mut chosen = None;
501    let mut default = None;
502    for m in ty.members() {
503        let d = m.descriptor();
504        if d.is_default_label {
505            default = Some(m);
506        }
507        if d.label.contains(&disc) {
508            chosen = Some(m);
509            break;
510        }
511    }
512    if let Some(m) = chosen.or(default) {
513        let mt = m.dynamic_type().clone();
514        let val = read_value(&mt, r, xcdr2)?;
515        data.set_value_raw(m.id(), val);
516    }
517    Ok(data)
518}
519
520/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
521fn read_value(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicValue> {
522    let w = |e: DecodeError| CodecError::Wire(e.to_string());
523    Ok(match ty.kind() {
524        TypeKind::Boolean => DynamicValue::Bool(r.read_u8().map_err(w)? != 0),
525        TypeKind::Byte => DynamicValue::Byte(r.read_u8().map_err(w)?),
526        TypeKind::UInt8 => DynamicValue::UInt8(r.read_u8().map_err(w)?),
527        TypeKind::Int8 => DynamicValue::Int8(r.read_u8().map_err(w)? as i8),
528        TypeKind::Int16 => DynamicValue::Int16(r.read_u16().map_err(w)? as i16),
529        TypeKind::UInt16 => DynamicValue::UInt16(r.read_u16().map_err(w)?),
530        TypeKind::Int32 => DynamicValue::Int32(r.read_u32().map_err(w)? as i32),
531        TypeKind::UInt32 => DynamicValue::UInt32(r.read_u32().map_err(w)?),
532        TypeKind::Enumeration => DynamicValue::Int32(r.read_u32().map_err(w)? as i32),
533        TypeKind::Int64 => DynamicValue::Int64(r.read_u64().map_err(w)? as i64),
534        TypeKind::UInt64 => DynamicValue::UInt64(r.read_u64().map_err(w)?),
535        TypeKind::Float32 => DynamicValue::Float32(f32::from_bits(r.read_u32().map_err(w)?)),
536        TypeKind::Float64 => DynamicValue::Float64(f64::from_bits(r.read_u64().map_err(w)?)),
537        TypeKind::Char8 => DynamicValue::Char8(r.read_u8().map_err(w)?),
538        TypeKind::Char16 => DynamicValue::Char16(r.read_u16().map_err(w)?),
539        TypeKind::String8 => DynamicValue::String(r.read_string().map_err(w)?),
540        TypeKind::String16 => {
541            // wstring: reuse the hardened WString codec (reads BOM + no-BOM).
542            let ws = WString::decode(r).map_err(w)?;
543            DynamicValue::WString(ws.0.encode_utf16().collect())
544        }
545        TypeKind::Structure | TypeKind::Union => {
546            DynamicValue::Complex(Box::new(decode_aggregate(ty, r, xcdr2)?))
547        }
548        TypeKind::Sequence => read_collection(ty, r, xcdr2, None)?,
549        TypeKind::Array => {
550            let n: usize = ty.descriptor().bound.iter().copied().product::<u32>() as usize;
551            read_collection(ty, r, xcdr2, Some(n))?
552        }
553        TypeKind::Alias => {
554            // The type model does not auto-deref typedefs; follow base_type to a
555            // scalar target. Composite alias targets are the documented residual.
556            let target = ty
557                .descriptor()
558                .base_type
559                .as_ref()
560                .and_then(|b| scalar_dynamic_type(b))
561                .ok_or_else(|| CodecError::NotSupported("alias-to-composite target".to_string()))?;
562            read_value(&target, r, xcdr2)?
563        }
564        other => {
565            return Err(CodecError::NotSupported(alloc::format!(
566                "member kind {other:?}"
567            )));
568        }
569    })
570}
571
572/// Decode a sequence (`fixed_len == None`) or array (`Some(n)`) element-by-element.
573///
574/// Scalar elements (primitive/enum/string/wstring) are stored as a scalar
575/// `DynamicData` with the value under member id 0; COMPOSITE elements
576/// (struct/union) are stored as the decoded aggregate `DynamicData` directly —
577/// both matching what `data_to_json` / the SQLite sink expect. The resolved
578/// element type comes from [`crate::dynamic::collection::resolved_element`] (set
579/// by the IDL lowering for composite elements), falling back to rebuilding a
580/// scalar element from the shallow descriptor.
581/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
582fn read_collection(
583    ty: &DynamicType,
584    r: &mut BufferReader<'_>,
585    xcdr2: bool,
586    fixed_len: Option<usize>,
587) -> R<DynamicValue> {
588    let desc = ty.descriptor();
589    let elem_desc = desc
590        .element_type
591        .as_ref()
592        .ok_or_else(|| CodecError::Dynamic("collection has no element_type".to_string()))?;
593    let elem_kind = elem_desc.kind;
594    let elem_ty = element_type(ty, elem_desc)?;
595    let w = |e: DecodeError| CodecError::Wire(e.to_string());
596    if xcdr2 && !is_primitive_kind(elem_kind) {
597        let _dheader = r.read_u32().map_err(w)?; // covers count+elements / elements
598    }
599    let count = match fixed_len {
600        Some(n) => n,
601        None => r.read_u32().map_err(w)? as usize,
602    };
603    let mut out = Vec::with_capacity(count.min(4096));
604    for _ in 0..count {
605        let v = read_value(&elem_ty, r, xcdr2)?;
606        let elem_data = match v {
607            // Composite element: read_value already built the aggregate.
608            DynamicValue::Complex(d) => *d,
609            // Scalar element: wrap the value under member id 0.
610            scalar => {
611                let mut d = DynamicData::new(elem_ty.clone());
612                d.set_value_raw(0, scalar);
613                d
614            }
615        };
616        out.push(elem_data);
617    }
618    Ok(DynamicValue::Sequence(out))
619}
620
621/// Resolve the element `DynamicType` of a collection: the fully-resolved element
622/// (composite-capable) if attached, else a scalar element rebuilt from the
623/// shallow descriptor.
624fn element_type(
625    ty: &DynamicType,
626    elem_desc: &crate::dynamic::descriptor::TypeDescriptor,
627) -> R<DynamicType> {
628    crate::dynamic::collection::resolved_element(ty)
629        .cloned()
630        .or_else(|| scalar_dynamic_type(elem_desc))
631        .ok_or_else(|| {
632            CodecError::NotSupported(alloc::format!(
633                "collection of {:?} (no resolved element type)",
634                elem_desc.kind
635            ))
636        })
637}
638
639/// Encode a sequence/array, mirroring [`read_collection`] (count for sequences;
640/// XCDR2 collection DHEADER for non-primitive elements; composite elements
641/// written as aggregates, scalars from member id 0).
642/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
643fn write_collection(
644    items: &[DynamicData],
645    ty: &DynamicType,
646    w: &mut BufferWriter,
647    xcdr2: bool,
648) -> R<()> {
649    let desc = ty.descriptor();
650    let elem_desc = desc
651        .element_type
652        .as_ref()
653        .ok_or_else(|| CodecError::Dynamic("collection has no element_type".to_string()))?;
654    let elem_kind = elem_desc.kind;
655    let elem_ty = element_type(ty, elem_desc)?;
656    let composite = matches!(elem_ty.kind(), TypeKind::Structure | TypeKind::Union);
657    let is_array = ty.kind() == TypeKind::Array;
658    let e = |x: EncodeError| CodecError::Wire(x.to_string());
659
660    let write_body = |w: &mut BufferWriter| -> R<()> {
661        if !is_array {
662            w.write_u32(items.len() as u32).map_err(e)?;
663        }
664        for d in items {
665            if composite {
666                write_aggregate(d, w, xcdr2)?;
667            } else {
668                let v = d
669                    .get_value(0)
670                    .ok_or_else(|| CodecError::Dynamic("scalar element unset".to_string()))?;
671                write_value(&v.clone(), &elem_ty, w, xcdr2)?;
672            }
673        }
674        Ok(())
675    };
676
677    if xcdr2 && !is_primitive_kind(elem_kind) {
678        // DHEADER = byte length of the body; encode to a sub-writer, prepend.
679        let mut sub = BufferWriter::new(w.endianness());
680        if xcdr2 {
681            sub = sub.xcdr2();
682        }
683        write_body(&mut sub)?;
684        let body = sub.into_bytes();
685        w.write_u32(body.len() as u32).map_err(e)?;
686        w.write_bytes(&body).map_err(e)?;
687        Ok(())
688    } else {
689        write_body(w)
690    }
691}
692
693// ----------------------------------------------------------------------------
694// Encode (mirror)
695// ----------------------------------------------------------------------------
696
697/// Encode `data` to a CDR payload (WITHOUT the encapsulation header), the mirror
698/// of [`decode_dynamic`]: `encode_dynamic(decode_dynamic(ty, b, x, e)?, x, e)? == b`.
699///
700/// # Errors
701/// [`CodecError`].
702pub fn encode_dynamic(data: &DynamicData, xcdr2: bool, big_endian: bool) -> R<Vec<u8>> {
703    let mut w = BufferWriter::new(endian(big_endian));
704    if xcdr2 {
705        w = w.xcdr2();
706    }
707    write_aggregate(data, &mut w, xcdr2)?;
708    Ok(w.into_bytes())
709}
710
711fn write_aggregate(data: &DynamicData, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
712    let ty = data.dynamic_type().clone();
713    match ty.kind() {
714        TypeKind::Structure => write_struct(data, &ty, w, xcdr2),
715        TypeKind::Union => write_union(data, &ty, w, xcdr2),
716        other => Err(CodecError::NotSupported(alloc::format!(
717            "top-level type kind {other:?} is not an aggregate"
718        ))),
719    }
720}
721
722fn write_struct(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
723    match ty.descriptor().extensibility_kind {
724        ExtensibilityKind::Final => write_members_in_order(data, ty, w, xcdr2),
725        ExtensibilityKind::Appendable => {
726            // XCDR1 (classic CDR): an @appendable struct is serialized exactly
727            // like @final — plain, no DHEADER (the DHEADER frame is XCDR2-only).
728            if !xcdr2 {
729                return write_members_in_order(data, ty, w, xcdr2);
730            }
731            let mut inner: Option<CodecError> = None;
732            let res = encode_appendable(w, |sub| {
733                match write_members_in_order(data, ty, sub, xcdr2) {
734                    Ok(()) => Ok(()),
735                    Err(e) => {
736                        inner = Some(e);
737                        Err(enc_sentinel())
738                    }
739                }
740            });
741            match res {
742                Ok(()) => Ok(()),
743                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
744            }
745        }
746        ExtensibilityKind::Mutable => {
747            // XCDR1: a @mutable struct is PL_CDR1 — a 16-bit-PID/length parameter
748            // list terminated by PID_LIST_END (not the XCDR2 EMHEADER/DHEADER).
749            if !xcdr2 {
750                return write_struct_pl_cdr1(data, ty, w);
751            }
752            // PL_CDR2: the EMHEADER parameter list is framed by a DHEADER (so a
753            // nested mutable member is self-delimiting). Mirror the typed codec,
754            // which wraps `encode_mutable_member`s in `encode_appendable`.
755            let mut outer: Option<CodecError> = None;
756            let res = encode_appendable(w, |fw| {
757                for i in 0..ty.member_count() {
758                    let m = match ty.member_by_index(i).ok_or_else(|| {
759                        CodecError::Dynamic(alloc::format!("missing member index {i}"))
760                    }) {
761                        Ok(m) => m,
762                        Err(e) => {
763                            outer = Some(e);
764                            return Err(enc_sentinel());
765                        }
766                    };
767                    let id = m.id();
768                    let Some(val) = data.get_value(id).cloned() else {
769                        continue;
770                    };
771                    let mt = m.dynamic_type().clone();
772                    let mu = m.descriptor().is_must_understand;
773                    let res = encode_mutable_member(fw, id, mu, |sub| {
774                        match write_value(&val, &mt, sub, xcdr2) {
775                            Ok(()) => Ok(()),
776                            Err(e) => {
777                                outer = Some(e);
778                                Err(enc_sentinel())
779                            }
780                        }
781                    });
782                    if let Err(e) = res {
783                        if outer.is_none() {
784                            outer = Some(CodecError::Wire(e.to_string()));
785                        }
786                        return Err(enc_sentinel());
787                    }
788                }
789                Ok(())
790            });
791            res.map_err(|e| outer.unwrap_or_else(|| CodecError::Wire(e.to_string())))
792        }
793    }
794}
795
796fn write_members_in_order(
797    data: &DynamicData,
798    ty: &DynamicType,
799    w: &mut BufferWriter,
800    xcdr2: bool,
801) -> R<()> {
802    for i in 0..ty.member_count() {
803        let m = ty
804            .member_by_index(i)
805            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
806        let id = m.id();
807        let val = data
808            .get_value(id)
809            .ok_or_else(|| CodecError::Dynamic(alloc::format!("member {id} unset on encode")))?
810            .clone();
811        let mt = m.dynamic_type().clone();
812        write_value(&val, &mt, w, xcdr2)?;
813    }
814    Ok(())
815}
816
817/// XCDR1 (`PL_CDR`) encode of a `@mutable` struct: each set member as a
818/// 16-bit-PID/length parameter, terminated by `PID_LIST_END` (XTypes 1.3
819/// §7.4.1.2). Mirrors the typed codec (`encode_pl_cdr1_member` + sentinel).
820fn write_struct_pl_cdr1(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter) -> R<()> {
821    for i in 0..ty.member_count() {
822        let m = ty
823            .member_by_index(i)
824            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
825        let id = m.id();
826        let Some(val) = data.get_value(id).cloned() else {
827            continue;
828        };
829        let mt = m.dynamic_type().clone();
830        let mut err: Option<CodecError> = None;
831        let res = encode_pl_cdr1_member(w, id, |sub| match write_value(&val, &mt, sub, false) {
832            Ok(()) => Ok(()),
833            Err(e) => {
834                err = Some(e);
835                Err(enc_sentinel())
836            }
837        });
838        if let Err(e) = res {
839            return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
840        }
841    }
842    write_pl_cdr1_sentinel(w).map_err(|e| CodecError::Wire(e.to_string()))
843}
844
845/// XCDR1 (`PL_CDR`) encode of a `@mutable` union: discriminator as PID 0, then
846/// the selected branch as its own PID, terminated by `PID_LIST_END`.
847fn write_union_pl_cdr1(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter) -> R<()> {
848    let disc_kind = union_discriminator_kind(ty);
849    let m = ty
850        .members()
851        .find(|m| data.get_value(m.id()).is_some())
852        .ok_or_else(|| CodecError::Dynamic("union has no branch set".to_string()))?;
853    let disc = m.descriptor().label.first().copied().unwrap_or(0);
854    let val = data
855        .get_value(m.id())
856        .cloned()
857        .ok_or_else(|| CodecError::Dynamic("union branch value vanished".to_string()))?;
858    let mt = m.dynamic_type().clone();
859
860    let mut err: Option<CodecError> = None;
861    let r1 = encode_pl_cdr1_member(w, DISCRIMINATOR_MEMBER_ID, |sub| match write_discriminator(
862        sub, disc_kind, disc,
863    ) {
864        Ok(()) => Ok(()),
865        Err(e) => {
866            err = Some(e);
867            Err(enc_sentinel())
868        }
869    });
870    if let Err(e) = r1 {
871        return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
872    }
873    let r2 = encode_pl_cdr1_member(w, m.id(), |sub| match write_value(&val, &mt, sub, false) {
874        Ok(()) => Ok(()),
875        Err(e) => {
876            err = Some(e);
877            Err(enc_sentinel())
878        }
879    });
880    if let Err(e) = r2 {
881        return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
882    }
883    write_pl_cdr1_sentinel(w).map_err(|e| CodecError::Wire(e.to_string()))
884}
885
886fn write_union(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
887    match ty.descriptor().extensibility_kind {
888        ExtensibilityKind::Final => write_union_body(data, ty, w, xcdr2),
889        ExtensibilityKind::Appendable => {
890            // XCDR1: appendable union = plain disc+branch (no DHEADER), like final.
891            if !xcdr2 {
892                return write_union_body(data, ty, w, xcdr2);
893            }
894            let mut inner: Option<CodecError> = None;
895            let res = encode_appendable(w, |sub| match write_union_body(data, ty, sub, xcdr2) {
896                Ok(()) => Ok(()),
897                Err(e) => {
898                    inner = Some(e);
899                    Err(enc_sentinel())
900                }
901            });
902            match res {
903                Ok(()) => Ok(()),
904                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
905            }
906        }
907        ExtensibilityKind::Mutable => {
908            if xcdr2 {
909                write_union_mutable(data, ty, w, xcdr2)
910            } else {
911                write_union_pl_cdr1(data, ty, w)
912            }
913        }
914    }
915}
916
917fn write_union_body(
918    data: &DynamicData,
919    ty: &DynamicType,
920    w: &mut BufferWriter,
921    xcdr2: bool,
922) -> R<()> {
923    let disc_kind = union_discriminator_kind(ty);
924    for m in ty.members() {
925        let id = m.id();
926        if let Some(val) = data.get_value(id).cloned() {
927            let disc = m.descriptor().label.first().copied().unwrap_or(0);
928            write_discriminator(w, disc_kind, disc)?;
929            let mt = m.dynamic_type().clone();
930            return write_value(&val, &mt, w, xcdr2);
931        }
932    }
933    Err(CodecError::Dynamic("union has no branch set".to_string()))
934}
935
936/// PL_CDR(2) union encode (inverse of [`decode_union_mutable`]): the
937/// discriminator as EMHEADER member [`DISCRIMINATOR_MEMBER_ID`] (always
938/// must-understand), then the selected branch as its own EMHEADER member. The
939/// parameter list is bounded by the enclosing frame, exactly like a mutable
940/// struct (`encode_mutable_member` writes no outer DHEADER itself).
941fn write_union_mutable(
942    data: &DynamicData,
943    ty: &DynamicType,
944    w: &mut BufferWriter,
945    xcdr2: bool,
946) -> R<()> {
947    let disc_kind = union_discriminator_kind(ty);
948    let m = ty
949        .members()
950        .find(|m| data.get_value(m.id()).is_some())
951        .ok_or_else(|| CodecError::Dynamic("union has no branch set".to_string()))?;
952    let disc = m.descriptor().label.first().copied().unwrap_or(0);
953    let val = data
954        .get_value(m.id())
955        .cloned()
956        .ok_or_else(|| CodecError::Dynamic("union branch value vanished".to_string()))?;
957    let mt = m.dynamic_type().clone();
958
959    let mu = m.descriptor().is_must_understand;
960    // PL_CDR2: DHEADER-framed parameter list — (1) discriminator as reserved
961    // EMHEADER member 0 (must-understand), (2) the selected branch as its own
962    // EMHEADER member. The DHEADER comes from `encode_appendable`.
963    let mut err: Option<CodecError> = None;
964    let res =
965        encode_appendable(w, |fw| {
966            let r1 = encode_mutable_member(fw, DISCRIMINATOR_MEMBER_ID, true, |sub| {
967                match write_discriminator(sub, disc_kind, disc) {
968                    Ok(()) => Ok(()),
969                    Err(e) => {
970                        err = Some(e);
971                        Err(enc_sentinel())
972                    }
973                }
974            });
975            if let Err(e) = r1 {
976                if err.is_none() {
977                    err = Some(CodecError::Wire(e.to_string()));
978                }
979                return Err(enc_sentinel());
980            }
981            let r2 = encode_mutable_member(fw, m.id(), mu, |sub| {
982                match write_value(&val, &mt, sub, xcdr2) {
983                    Ok(()) => Ok(()),
984                    Err(e) => {
985                        err = Some(e);
986                        Err(enc_sentinel())
987                    }
988                }
989            });
990            if let Err(e) = r2 {
991                if err.is_none() {
992                    err = Some(CodecError::Wire(e.to_string()));
993                }
994                return Err(enc_sentinel());
995            }
996            Ok(())
997        });
998    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
999}
1000
1001/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
1002fn write_value(v: &DynamicValue, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
1003    let e = |x: EncodeError| CodecError::Wire(x.to_string());
1004    match v {
1005        DynamicValue::Bool(b) => w.write_u8(u8::from(*b)).map_err(e),
1006        DynamicValue::Byte(x) | DynamicValue::UInt8(x) | DynamicValue::Char8(x) => {
1007            w.write_u8(*x).map_err(e)
1008        }
1009        DynamicValue::Int8(x) => w.write_u8(*x as u8).map_err(e),
1010        DynamicValue::Int16(x) => w.write_u16(*x as u16).map_err(e),
1011        DynamicValue::UInt16(x) | DynamicValue::Char16(x) => w.write_u16(*x).map_err(e),
1012        DynamicValue::Int32(x) => w.write_u32(*x as u32).map_err(e),
1013        DynamicValue::UInt32(x) => w.write_u32(*x).map_err(e),
1014        DynamicValue::Int64(x) => w.write_u64(*x as u64).map_err(e),
1015        DynamicValue::UInt64(x) => w.write_u64(*x).map_err(e),
1016        DynamicValue::Float32(x) => w.write_u32(x.to_bits()).map_err(e),
1017        DynamicValue::Float64(x) => w.write_u64(x.to_bits()).map_err(e),
1018        DynamicValue::String(s) => w.write_string(s).map_err(e),
1019        DynamicValue::WString(units) => {
1020            // wstring: reuse the hardened WString codec (global BOM policy —
1021            // matches what zerodds typed writers emit).
1022            WString(String::from_utf16_lossy(units))
1023                .encode(w)
1024                .map_err(e)
1025        }
1026        DynamicValue::Complex(d) => write_aggregate(d, w, xcdr2),
1027        DynamicValue::Sequence(items) => write_collection(items, ty, w, xcdr2),
1028        DynamicValue::None => Ok(()),
1029    }
1030}
1031
1032#[cfg(test)]
1033#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1034mod tests {
1035    use super::*;
1036    use crate::dynamic::builder::DynamicTypeBuilderFactory;
1037    use crate::dynamic::descriptor::{MemberDescriptor, TypeDescriptor};
1038
1039    /// Build a struct type `{ a: int32, b: uint64, s: string, f: float64 }` with
1040    /// the given extensibility.
1041    fn sample_type(ext: ExtensibilityKind) -> DynamicType {
1042        let mut desc = TypeDescriptor::structure("Sample");
1043        desc.extensibility_kind = ext;
1044        let mut b = DynamicTypeBuilderFactory::create_type(desc).expect("builder");
1045        b.add_struct_member("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"))
1046            .unwrap();
1047        b.add_struct_member(
1048            "b",
1049            1,
1050            TypeDescriptor::primitive(TypeKind::UInt64, "uint64"),
1051        )
1052        .unwrap();
1053        let string_desc = DynamicTypeBuilderFactory::create_string_type(255)
1054            .descriptor()
1055            .clone();
1056        b.add_struct_member("s", 2, string_desc).unwrap();
1057        b.add_struct_member(
1058            "f",
1059            3,
1060            TypeDescriptor::primitive(TypeKind::Float64, "float64"),
1061        )
1062        .unwrap();
1063        b.build().expect("build")
1064    }
1065
1066    fn populate(ty: &DynamicType) -> DynamicData {
1067        let mut d = DynamicData::new(ty.clone());
1068        d.set_int32_value(0, -7).unwrap();
1069        d.set_uint64_value(1, 0xDEAD_BEEF_0000_0001).unwrap();
1070        d.set_string_value(2, "hello-dynamic").unwrap();
1071        d.set_float64_value(3, 3.5).unwrap();
1072        d
1073    }
1074
1075    fn roundtrip(ext: ExtensibilityKind, xcdr2: bool) {
1076        let ty = sample_type(ext);
1077        let src = populate(&ty);
1078        let bytes = encode_dynamic(&src, xcdr2, false).expect("encode");
1079        let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1080        // Field values survive.
1081        assert_eq!(back.get_int32_value(0).unwrap(), -7);
1082        assert_eq!(back.get_uint64_value(1).unwrap(), 0xDEAD_BEEF_0000_0001);
1083        assert_eq!(back.get_string_value(2).unwrap(), "hello-dynamic");
1084        assert!((back.get_float64_value(3).unwrap() - 3.5).abs() < f64::EPSILON);
1085        // Byte-exact round-trip: re-encoding the decoded value reproduces the wire.
1086        let bytes2 = encode_dynamic(&back, xcdr2, false).expect("re-encode");
1087        assert_eq!(
1088            bytes, bytes2,
1089            "byte-exact round-trip failed for {ext:?} xcdr2={xcdr2}"
1090        );
1091    }
1092
1093    #[test]
1094    fn roundtrip_final_xcdr2() {
1095        roundtrip(ExtensibilityKind::Final, true);
1096    }
1097    #[test]
1098    fn roundtrip_final_xcdr1() {
1099        roundtrip(ExtensibilityKind::Final, false);
1100    }
1101    #[test]
1102    fn roundtrip_appendable_xcdr2() {
1103        roundtrip(ExtensibilityKind::Appendable, true);
1104    }
1105    #[test]
1106    fn roundtrip_mutable_xcdr2() {
1107        roundtrip(ExtensibilityKind::Mutable, true);
1108    }
1109
1110    /// Union round-trips: discriminator selects the branch, the branch value
1111    /// survives, and re-encode is byte-exact.
1112    #[test]
1113    fn roundtrip_union() {
1114        let disc = TypeDescriptor::primitive(TypeKind::Int32, "int32");
1115        let mut b = DynamicTypeBuilderFactory::create_union("U", disc).expect("union builder");
1116        // case 1 → int32 a
1117        let mut m1 =
1118            MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1119        m1.index = 0;
1120        m1.label = alloc::vec![1];
1121        b.add_member_resolved(m1, DynamicType::new_primitive(TypeKind::Int32))
1122            .unwrap();
1123        // case 2 → uint32 u
1124        let mut m2 = MemberDescriptor::new(
1125            "u",
1126            1,
1127            TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1128        );
1129        m2.index = 1;
1130        m2.label = alloc::vec![2];
1131        b.add_member_resolved(m2, DynamicType::new_primitive(TypeKind::UInt32))
1132            .unwrap();
1133        let u = b.build().expect("build union");
1134
1135        // Select case 2 by setting member id 1.
1136        let mut d = DynamicData::new(u.clone());
1137        d.set_uint32_value(1, 0xABCD).unwrap();
1138        let bytes = encode_dynamic(&d, true, false).expect("encode");
1139        let back = decode_dynamic(&u, &bytes, true, false).expect("decode");
1140        assert_eq!(back.get_uint32_value(1).unwrap(), 0xABCD);
1141        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1142    }
1143
1144    /// MUTABLE (PL_CDR2) union: the discriminator is EMHEADER member id 0, the
1145    /// selected branch is its own EMHEADER member. Round-trips both a branch with
1146    /// id 0 (exercises the discriminator/branch id-0 collision handling) and a
1147    /// branch with id 1, byte-exact re-encode each time.
1148    #[test]
1149    fn roundtrip_union_mutable() {
1150        let build = || {
1151            let mut ud =
1152                TypeDescriptor::union("UM", TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1153            ud.extensibility_kind = ExtensibilityKind::Mutable;
1154            let mut b = DynamicTypeBuilderFactory::create_type(ud).expect("union");
1155            let mut m1 =
1156                MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1157            m1.index = 0;
1158            m1.label = alloc::vec![1];
1159            b.add_member_resolved(m1, DynamicType::new_primitive(TypeKind::Int32))
1160                .unwrap();
1161            let mut m2 = MemberDescriptor::new(
1162                "u",
1163                1,
1164                TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1165            );
1166            m2.index = 1;
1167            m2.label = alloc::vec![2];
1168            b.add_member_resolved(m2, DynamicType::new_primitive(TypeKind::UInt32))
1169                .unwrap();
1170            b.build().expect("build union")
1171        };
1172
1173        // case "a" (member id 0) — collides with the discriminator's reserved id.
1174        let u = build();
1175        let mut d = DynamicData::new(u.clone());
1176        d.set_int32_value(0, -42).unwrap();
1177        let bytes = encode_dynamic(&d, true, false).expect("encode a");
1178        let back = decode_dynamic(&u, &bytes, true, false).expect("decode a");
1179        assert_eq!(back.get_int32_value(0).unwrap(), -42);
1180        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1181
1182        // case "u" (member id 1).
1183        let mut d2 = DynamicData::new(u.clone());
1184        d2.set_uint32_value(1, 0xDEAD_BEEF).unwrap();
1185        let bytes2 = encode_dynamic(&d2, true, false).expect("encode u");
1186        let back2 = decode_dynamic(&u, &bytes2, true, false).expect("decode u");
1187        assert_eq!(back2.get_uint32_value(1).unwrap(), 0xDEAD_BEEF);
1188        assert_eq!(encode_dynamic(&back2, true, false).unwrap(), bytes2);
1189    }
1190
1191    /// (B) Non-int32 union discriminator: an int16-discriminated union with an
1192    /// int8 branch must encode the discriminator as 2 bytes (not 4) — guards the
1193    /// latent hard-coded-int32 bug the symmetric round-trip alone would mask.
1194    #[test]
1195    fn union_int16_discriminator_is_two_bytes() {
1196        let mut ud =
1197            TypeDescriptor::union("U16", TypeDescriptor::primitive(TypeKind::Int16, "int16"));
1198        ud.extensibility_kind = ExtensibilityKind::Final;
1199        let mut b = DynamicTypeBuilderFactory::create_type(ud).expect("union");
1200        let mut m =
1201            MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int8, "int8"));
1202        m.index = 0;
1203        m.label = alloc::vec![10];
1204        b.add_member_resolved(m, DynamicType::new_primitive(TypeKind::Int8))
1205            .unwrap();
1206        let u = b.build().expect("build");
1207
1208        let mut d = DynamicData::new(u.clone());
1209        d.set_int8_value(0, 7).unwrap();
1210        let bytes = encode_dynamic(&d, true, false).expect("encode");
1211        // [disc i16 LE = 0x000a][int8 = 0x07] => exactly 3 bytes.
1212        assert_eq!(
1213            bytes,
1214            alloc::vec![0x0a, 0x00, 0x07],
1215            "discriminator must be 2 bytes"
1216        );
1217        let back = decode_dynamic(&u, &bytes, true, false).expect("decode");
1218        assert_eq!(back.get_int8_value(0).unwrap(), 7);
1219    }
1220
1221    // Primitives have no member id 0, so the validating typed setters reject them;
1222    // scalar elements store their value raw (the codec does the same internally).
1223    fn scalar_i32(v: i32) -> DynamicData {
1224        let mut d = DynamicData::new(DynamicType::new_primitive(TypeKind::Int32));
1225        d.set_value_raw(0, DynamicValue::Int32(v));
1226        d
1227    }
1228
1229    /// (A) sequence<int32> round-trips.
1230    #[test]
1231    fn roundtrip_seq_int32() {
1232        let mut desc = TypeDescriptor::structure("S");
1233        desc.extensibility_kind = ExtensibilityKind::Final;
1234        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1235        let seq = TypeDescriptor::sequence(
1236            "seq",
1237            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
1238            0,
1239        );
1240        b.add_struct_member("xs", 0, seq).unwrap();
1241        let ty = b.build().unwrap();
1242
1243        let mut d = DynamicData::new(ty.clone());
1244        let items = alloc::vec![scalar_i32(11), scalar_i32(-22), scalar_i32(33),];
1245        d.set_sequence_value(0, items).unwrap();
1246        let bytes = encode_dynamic(&d, true, false).expect("encode");
1247        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1248        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1249            panic!("seq")
1250        };
1251        let got: alloc::vec::Vec<i32> = v.iter().map(|e| e.get_int32_value(0).unwrap()).collect();
1252        assert_eq!(got, alloc::vec![11, -22, 33]);
1253        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1254    }
1255
1256    /// (A) sequence<string> round-trips (exercises the XCDR2 collection DHEADER).
1257    #[test]
1258    fn roundtrip_seq_string() {
1259        let mut desc = TypeDescriptor::structure("S");
1260        desc.extensibility_kind = ExtensibilityKind::Final;
1261        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1262        let strd = DynamicTypeBuilderFactory::create_string_type(255)
1263            .descriptor()
1264            .clone();
1265        let seq = TypeDescriptor::sequence("seq", strd, 0);
1266        b.add_struct_member("ss", 0, seq).unwrap();
1267        let ty = b.build().unwrap();
1268
1269        let mut d = DynamicData::new(ty.clone());
1270        let mk = |s: &str| {
1271            let mut e = DynamicData::new(DynamicTypeBuilderFactory::create_string_type(255));
1272            e.set_value_raw(0, DynamicValue::String(s.into()));
1273            e
1274        };
1275        d.set_sequence_value(0, alloc::vec![mk("a"), mk("bb"), mk("ccc")])
1276            .unwrap();
1277        let bytes = encode_dynamic(&d, true, false).expect("encode");
1278        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1279        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1280            panic!("seq")
1281        };
1282        let got: alloc::vec::Vec<alloc::string::String> =
1283            v.iter().map(|e| e.get_string_value(0).unwrap()).collect();
1284        assert_eq!(
1285            got,
1286            alloc::vec!["a".to_string(), "bb".to_string(), "ccc".to_string()]
1287        );
1288        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1289    }
1290
1291    /// (A) fixed array int32[3] round-trips (no count prefix).
1292    #[test]
1293    fn roundtrip_array_int32() {
1294        let mut desc = TypeDescriptor::structure("S");
1295        desc.extensibility_kind = ExtensibilityKind::Final;
1296        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1297        let arr = TypeDescriptor::array(
1298            "arr",
1299            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
1300            alloc::vec![3],
1301        );
1302        b.add_struct_member("a", 0, arr).unwrap();
1303        let ty = b.build().unwrap();
1304
1305        let mut d = DynamicData::new(ty.clone());
1306        let items = alloc::vec![scalar_i32(7), scalar_i32(8), scalar_i32(9),];
1307        d.set_sequence_value(0, items).unwrap();
1308        let bytes = encode_dynamic(&d, true, false).expect("encode");
1309        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1310        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1311            panic!("arr")
1312        };
1313        let got: alloc::vec::Vec<i32> = v.iter().map(|e| e.get_int32_value(0).unwrap()).collect();
1314        assert_eq!(got, alloc::vec![7, 8, 9]);
1315        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1316    }
1317
1318    /// Composite-element collection: `sequence<Inner>` round-trips, each element
1319    /// is the decoded aggregate, and re-encode is byte-exact (XCDR2 collection
1320    /// DHEADER + per-element appendable DHEADERs).
1321    #[test]
1322    fn roundtrip_seq_of_struct() {
1323        use crate::dynamic::collection;
1324        let inner = sample_type(ExtensibilityKind::Final);
1325        let seq_ty = collection::sequence_of(inner.clone(), 0);
1326        let mut desc = TypeDescriptor::structure("S");
1327        desc.extensibility_kind = ExtensibilityKind::Final;
1328        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1329        let mut md = MemberDescriptor::new("xs", 0, seq_ty.descriptor().clone());
1330        md.index = 0;
1331        b.add_member_resolved(md, seq_ty).unwrap();
1332        let ty = b.build().unwrap();
1333
1334        let mut d = DynamicData::new(ty.clone());
1335        let e0 = populate(&inner);
1336        let mut e1 = DynamicData::new(inner.clone());
1337        e1.set_int32_value(0, 123).unwrap();
1338        e1.set_uint64_value(1, 7).unwrap();
1339        e1.set_string_value(2, "second").unwrap();
1340        e1.set_float64_value(3, -1.25).unwrap();
1341        d.set_sequence_value(0, alloc::vec![e0, e1]).unwrap();
1342
1343        for xcdr2 in [true, false] {
1344            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1345            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1346            let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1347                panic!("seq")
1348            };
1349            assert_eq!(v.len(), 2);
1350            // Composite element = the aggregate itself (kind Structure).
1351            assert_eq!(v[0].dynamic_type().kind(), TypeKind::Structure);
1352            assert_eq!(v[0].get_string_value(2).unwrap(), "hello-dynamic");
1353            assert_eq!(v[1].get_int32_value(0).unwrap(), 123);
1354            assert_eq!(v[1].get_string_value(2).unwrap(), "second");
1355            assert_eq!(
1356                encode_dynamic(&back, xcdr2, false).unwrap(),
1357                bytes,
1358                "byte-exact seq<struct> xcdr2={xcdr2}"
1359            );
1360        }
1361    }
1362
1363    /// Composite-element fixed array `Inner[2]` round-trips (no count prefix).
1364    #[test]
1365    fn roundtrip_array_of_struct() {
1366        use crate::dynamic::collection;
1367        let inner = sample_type(ExtensibilityKind::Final);
1368        let arr_ty = collection::array_of(inner.clone(), alloc::vec![2], "arr");
1369        let mut desc = TypeDescriptor::structure("S");
1370        desc.extensibility_kind = ExtensibilityKind::Final;
1371        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1372        let mut md = MemberDescriptor::new("a", 0, arr_ty.descriptor().clone());
1373        md.index = 0;
1374        b.add_member_resolved(md, arr_ty).unwrap();
1375        let ty = b.build().unwrap();
1376
1377        let mut d = DynamicData::new(ty.clone());
1378        let mut e0 = DynamicData::new(inner.clone());
1379        e0.set_int32_value(0, 1).unwrap();
1380        e0.set_uint64_value(1, 1).unwrap();
1381        e0.set_string_value(2, "x").unwrap();
1382        e0.set_float64_value(3, 1.0).unwrap();
1383        let mut e1 = DynamicData::new(inner.clone());
1384        e1.set_int32_value(0, 2).unwrap();
1385        e1.set_uint64_value(1, 2).unwrap();
1386        e1.set_string_value(2, "y").unwrap();
1387        e1.set_float64_value(3, 2.0).unwrap();
1388        d.set_sequence_value(0, alloc::vec![e0, e1]).unwrap();
1389
1390        let bytes = encode_dynamic(&d, true, false).expect("encode");
1391        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1392        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1393            panic!("arr")
1394        };
1395        assert_eq!(v.len(), 2);
1396        assert_eq!(v[0].get_string_value(2).unwrap(), "x");
1397        assert_eq!(v[1].get_int32_value(0).unwrap(), 2);
1398        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1399    }
1400
1401    /// `wstring` member round-trips (via the hardened `WString` codec).
1402    #[test]
1403    fn roundtrip_wstring() {
1404        let mut desc = TypeDescriptor::structure("W");
1405        desc.extensibility_kind = ExtensibilityKind::Final;
1406        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1407        let wdesc = DynamicTypeBuilderFactory::create_wstring_type(0)
1408            .descriptor()
1409            .clone();
1410        let mut md = MemberDescriptor::new("w", 0, wdesc);
1411        md.index = 0;
1412        b.add_member_resolved(md, DynamicTypeBuilderFactory::create_wstring_type(0))
1413            .unwrap();
1414        let ty = b.build().unwrap();
1415
1416        let mut d = DynamicData::new(ty.clone());
1417        let units: alloc::vec::Vec<u16> = "wïde→".encode_utf16().collect();
1418        d.set_value_raw(0, DynamicValue::WString(units.clone()));
1419        for xcdr2 in [true, false] {
1420            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1421            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1422            let DynamicValue::WString(got) = back.get_value(0).unwrap() else {
1423                panic!("wstring")
1424            };
1425            assert_eq!(*got, units);
1426            assert_eq!(encode_dynamic(&back, xcdr2, false).unwrap(), bytes);
1427        }
1428    }
1429
1430    /// Nested struct member round-trips (the member is a full DynamicType).
1431    #[test]
1432    fn roundtrip_nested_struct() {
1433        let inner = sample_type(ExtensibilityKind::Final);
1434        let mut desc = TypeDescriptor::structure("Outer");
1435        desc.extensibility_kind = ExtensibilityKind::Final;
1436        let mut b = DynamicTypeBuilderFactory::create_type(desc).expect("builder");
1437        b.add_struct_member(
1438            "id",
1439            0,
1440            TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1441        )
1442        .unwrap();
1443        let mut md = MemberDescriptor::new("inner", 1, inner.descriptor().clone());
1444        md.index = 1;
1445        b.add_member_resolved(md, inner.clone()).unwrap();
1446        let outer = b.build().expect("build outer");
1447
1448        let mut d = DynamicData::new(outer.clone());
1449        d.set_uint32_value(0, 99).unwrap();
1450        d.set_complex_value(1, populate(&inner)).unwrap();
1451
1452        let bytes = encode_dynamic(&d, true, false).expect("encode");
1453        let back = decode_dynamic(&outer, &bytes, true, false).expect("decode");
1454        assert_eq!(back.get_uint32_value(0).unwrap(), 99);
1455        let inner_back = back.get_complex_value(1).unwrap();
1456        assert_eq!(inner_back.get_string_value(2).unwrap(), "hello-dynamic");
1457        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1458    }
1459}