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).
122/// Resolve an alias (typedef) to its target type. Typedefs are transparent, so
123/// the TypeObject bridge already resolves alias members to their target — this
124/// defensive fallback rebuilds a scalar target from the shallow
125/// `base_type`/`element_type` descriptor for any alias node reaching the codec.
126fn alias_target(ty: &DynamicType) -> R<DynamicType> {
127    let desc = ty.descriptor();
128    desc.base_type
129        .as_ref()
130        .or(desc.element_type.as_ref())
131        .and_then(|b| scalar_dynamic_type(b))
132        .ok_or_else(|| {
133            CodecError::NotSupported(
134                "alias target: composite typedefs are resolved transparently by the bridge; \
135                 a bare composite alias node reaching the codec is unsupported"
136                    .to_string(),
137            )
138        })
139}
140
141fn scalar_dynamic_type(desc: &crate::dynamic::descriptor::TypeDescriptor) -> Option<DynamicType> {
142    use crate::dynamic::builder::DynamicTypeBuilderFactory as F;
143    match desc.kind {
144        TypeKind::String8 => Some(F::create_string_type(
145            desc.bound.first().copied().unwrap_or(0),
146        )),
147        TypeKind::String16 => Some(F::create_wstring_type(
148            desc.bound.first().copied().unwrap_or(0),
149        )),
150        k if is_primitive_kind(k) => Some(DynamicType::new_primitive(k)),
151        _ => None,
152    }
153}
154
155/// The reserved EMHEADER member id of a union's discriminator in PL_CDR(2)
156/// framing (DDS-XTypes 1.3 §7.2.2.4.4.4.6: `DISCRIMINATOR_ID`).
157const DISCRIMINATOR_MEMBER_ID: u32 = 0;
158
159/// Read an integral union discriminator of `kind`, returned as `i64` (signed
160/// kinds are sign-extended). Mirrors the typed codec's per-kind discriminator
161/// encoding — NOT hard-coded to int32.
162fn read_discriminator(r: &mut BufferReader<'_>, kind: TypeKind) -> R<i64> {
163    let w = |e: DecodeError| CodecError::Wire(e.to_string());
164    Ok(match kind {
165        TypeKind::Boolean | TypeKind::Byte | TypeKind::UInt8 | TypeKind::Char8 => {
166            i64::from(r.read_u8().map_err(w)?)
167        }
168        TypeKind::Int8 => i64::from(r.read_u8().map_err(w)? as i8),
169        TypeKind::UInt16 | TypeKind::Char16 => i64::from(r.read_u16().map_err(w)?),
170        TypeKind::Int16 => i64::from(r.read_u16().map_err(w)? as i16),
171        TypeKind::UInt32 | TypeKind::Enumeration => i64::from(r.read_u32().map_err(w)?),
172        TypeKind::Int32 => i64::from(r.read_u32().map_err(w)? as i32),
173        TypeKind::UInt64 => r.read_u64().map_err(w)? as i64,
174        TypeKind::Int64 => r.read_u64().map_err(w)? as i64,
175        other => {
176            return Err(CodecError::NotSupported(alloc::format!(
177                "union discriminator kind {other:?}"
178            )));
179        }
180    })
181}
182
183/// Write an integral union discriminator `val` as `kind`.
184fn write_discriminator(w: &mut BufferWriter, kind: TypeKind, val: i64) -> R<()> {
185    let e = |x: EncodeError| CodecError::Wire(x.to_string());
186    match kind {
187        TypeKind::Boolean | TypeKind::Byte | TypeKind::UInt8 | TypeKind::Char8 | TypeKind::Int8 => {
188            w.write_u8(val as u8).map_err(e)
189        }
190        TypeKind::UInt16 | TypeKind::Char16 | TypeKind::Int16 => w.write_u16(val as u16).map_err(e),
191        TypeKind::UInt32 | TypeKind::Enumeration | TypeKind::Int32 => {
192            w.write_u32(val as u32).map_err(e)
193        }
194        TypeKind::UInt64 | TypeKind::Int64 => w.write_u64(val as u64).map_err(e),
195        other => Err(CodecError::NotSupported(alloc::format!(
196            "union discriminator kind {other:?}"
197        ))),
198    }
199}
200
201/// The discriminator kind of a union type (from its descriptor), defaulting to
202/// `Int32` when absent.
203fn union_discriminator_kind(ty: &DynamicType) -> TypeKind {
204    ty.descriptor()
205        .discriminator_type
206        .as_ref()
207        .map_or(TypeKind::Int32, |d| d.kind)
208}
209
210// ----------------------------------------------------------------------------
211// Decode
212// ----------------------------------------------------------------------------
213
214/// Decode `bytes` (a CDR payload WITHOUT the encapsulation header) into a
215/// [`DynamicData`] of `ty`. `xcdr2` selects XCDR2 vs XCDR1 framing; `big_endian`
216/// the byte order — both come from the sample's encapsulation id on the wire.
217///
218/// # Errors
219/// [`CodecError`] on a wire failure, a `DynamicData` failure, or an unsupported
220/// construct.
221pub fn decode_dynamic(
222    ty: &DynamicType,
223    bytes: &[u8],
224    xcdr2: bool,
225    big_endian: bool,
226) -> R<DynamicData> {
227    let mut r = BufferReader::new(bytes, endian(big_endian));
228    if xcdr2 {
229        r = r.xcdr2();
230    }
231    decode_aggregate(ty, &mut r, xcdr2)
232}
233
234fn decode_aggregate(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
235    match ty.kind() {
236        TypeKind::Structure => decode_struct(ty, r, xcdr2),
237        TypeKind::Union => decode_union(ty, r, xcdr2),
238        other => Err(CodecError::NotSupported(alloc::format!(
239            "top-level type kind {other:?} is not an aggregate"
240        ))),
241    }
242}
243
244fn decode_struct(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
245    match ty.descriptor().extensibility_kind {
246        ExtensibilityKind::Final => {
247            // FINAL never carries a DHEADER — read members in declared order.
248            let mut data = DynamicData::new(ty.clone());
249            read_members_in_order(ty, r, xcdr2, &mut data)?;
250            Ok(data)
251        }
252        ExtensibilityKind::Appendable => {
253            // XCDR1: appendable is plain (no DHEADER), exactly like final.
254            if !xcdr2 {
255                let mut data = DynamicData::new(ty.clone());
256                read_members_in_order(ty, r, xcdr2, &mut data)?;
257                return Ok(data);
258            }
259            let mut inner: Option<CodecError> = None;
260            let res = decode_appendable(r, |sub| {
261                let mut data = DynamicData::new(ty.clone());
262                match read_members_in_order(ty, sub, xcdr2, &mut data) {
263                    Ok(()) => Ok(data),
264                    Err(e) => {
265                        inner = Some(e);
266                        Err(dec_sentinel())
267                    }
268                }
269            });
270            match res {
271                Ok(d) => Ok(d),
272                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
273            }
274        }
275        ExtensibilityKind::Mutable => {
276            if xcdr2 {
277                decode_struct_mutable(ty, r, xcdr2)
278            } else {
279                decode_struct_pl_cdr1(ty, r)
280            }
281        }
282    }
283}
284
285fn read_members_in_order(
286    ty: &DynamicType,
287    r: &mut BufferReader<'_>,
288    xcdr2: bool,
289    data: &mut DynamicData,
290) -> R<()> {
291    for i in 0..ty.member_count() {
292        let m = ty
293            .member_by_index(i)
294            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
295        let mt = m.dynamic_type().clone();
296        let id = m.id();
297        let val = read_value(&mt, r, xcdr2)?;
298        data.set_value_raw(id, val);
299    }
300    Ok(())
301}
302
303fn decode_struct_mutable(
304    ty: &DynamicType,
305    r: &mut BufferReader<'_>,
306    xcdr2: bool,
307) -> R<DynamicData> {
308    // PL_CDR2: a DHEADER frames the EMHEADER parameter list (XTypes 1.3
309    // §7.4.3.4.2). `decode_appendable` strips that DHEADER and bounds the
310    // sub-reader so a nested mutable member stops at its own frame — exactly
311    // what the typed codec does (`decode_appendable` around
312    // `read_all_mutable_members`).
313    let mut err: Option<CodecError> = None;
314    let res = decode_appendable(r, |sub| {
315        let members = read_all_mutable_members(sub)?;
316        let mut data = DynamicData::new(ty.clone());
317        for mm in members {
318            let Some(m) = ty.member_by_id(mm.member_id) else {
319                continue; // unknown member id → skip (forward-compat)
320            };
321            let mt = m.dynamic_type().clone();
322            let mut bsub = BufferReader::new(mm.body, sub.endianness());
323            if xcdr2 {
324                bsub = bsub.xcdr2();
325            }
326            match read_value(&mt, &mut bsub, xcdr2) {
327                Ok(val) => data.set_value_raw(m.id(), val),
328                Err(e) => {
329                    err = Some(e);
330                    return Err(dec_sentinel());
331                }
332            }
333        }
334        Ok(data)
335    });
336    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
337}
338
339/// XCDR1 (`PL_CDR`) decode of a `@mutable` struct: the 16-bit-PID parameter list
340/// terminated by `PID_LIST_END`. XCDR1 alignment (max-align 8) is the default of
341/// the per-member sub-reader.
342fn decode_struct_pl_cdr1(ty: &DynamicType, r: &mut BufferReader<'_>) -> R<DynamicData> {
343    let members = read_all_pl_cdr1_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
344    let mut data = DynamicData::new(ty.clone());
345    for mm in members {
346        let Some(m) = ty.member_by_id(mm.member_id) else {
347            continue; // unknown member id → skip
348        };
349        let mt = m.dynamic_type().clone();
350        let mut bsub = BufferReader::new(&mm.body, r.endianness());
351        let val = read_value(&mt, &mut bsub, false)?;
352        data.set_value_raw(m.id(), val);
353    }
354    Ok(data)
355}
356
357/// XCDR1 (`PL_CDR`) decode of a `@mutable` union: discriminator = PID 0, the
358/// single other PID = the selected branch (matched by position, vendor-agnostic).
359fn decode_union_pl_cdr1(ty: &DynamicType, r: &mut BufferReader<'_>) -> R<DynamicData> {
360    let members = read_all_pl_cdr1_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
361    let mut data = DynamicData::new(ty.clone());
362    let disc_idx = members
363        .iter()
364        .position(|m| m.member_id == DISCRIMINATOR_MEMBER_ID)
365        .ok_or_else(|| {
366            CodecError::Wire("mutable union (PL_CDR1): no discriminator member id 0".to_string())
367        })?;
368    let mut dsub = BufferReader::new(&members[disc_idx].body, r.endianness());
369    let disc = read_discriminator(&mut dsub, union_discriminator_kind(ty))?;
370
371    let mut chosen = None;
372    let mut default = None;
373    for m in ty.members() {
374        let d = m.descriptor();
375        if d.is_default_label {
376            default = Some(m);
377        }
378        if d.label.contains(&disc) {
379            chosen = Some(m);
380            break;
381        }
382    }
383    if let Some(m) = chosen.or(default) {
384        if let Some(bw) = members
385            .iter()
386            .enumerate()
387            .find(|(i, _)| *i != disc_idx)
388            .map(|(_, w)| w)
389        {
390            let mt = m.dynamic_type().clone();
391            let mut bsub = BufferReader::new(&bw.body, r.endianness());
392            let val = read_value(&mt, &mut bsub, false)?;
393            data.set_value_raw(m.id(), val);
394        }
395    }
396    Ok(data)
397}
398
399fn decode_union(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
400    match ty.descriptor().extensibility_kind {
401        ExtensibilityKind::Final => decode_union_body(ty, r, xcdr2),
402        ExtensibilityKind::Appendable => {
403            // XCDR1: appendable union = plain disc+branch (no DHEADER).
404            if !xcdr2 {
405                return decode_union_body(ty, r, xcdr2);
406            }
407            let mut inner: Option<CodecError> = None;
408            let res = decode_appendable(r, |sub| match decode_union_body(ty, sub, xcdr2) {
409                Ok(d) => Ok(d),
410                Err(e) => {
411                    inner = Some(e);
412                    Err(dec_sentinel())
413                }
414            });
415            match res {
416                Ok(d) => Ok(d),
417                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
418            }
419        }
420        ExtensibilityKind::Mutable => {
421            if xcdr2 {
422                decode_union_mutable(ty, r, xcdr2)
423            } else {
424                decode_union_pl_cdr1(ty, r)
425            }
426        }
427    }
428}
429
430/// XTypes 1.3 PL_CDR(2) union (§7.4.3.4 / §7.2.2.4.4.4.6): a parameter list of
431/// EMHEADER-framed members, exactly like a mutable struct. The discriminator is
432/// the member with the reserved id [`DISCRIMINATOR_MEMBER_ID`] (0); the selected
433/// branch is the member whose id matches the chosen case member. Fully
434/// data-driven — branch ids are read from the wire and matched against the
435/// `DynamicType` (both derived from the same TypeObject), so no id convention is
436/// assumed beyond the spec-fixed discriminator id 0.
437fn decode_union_mutable(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
438    // PL_CDR2 frames the parameter list with a DHEADER (like a mutable struct) —
439    // strip + bound it via `decode_appendable` so a nested union member stops at
440    // its own frame.
441    let mut err: Option<CodecError> = None;
442    let res = decode_appendable(r, |sub| match decode_union_mutable_body(ty, sub, xcdr2) {
443        Ok(d) => Ok(d),
444        Err(e) => {
445            err = Some(e);
446            Err(dec_sentinel())
447        }
448    });
449    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
450}
451
452/// The PL_CDR2 union member walk, on a reader already scoped to the DHEADER body.
453fn decode_union_mutable_body(
454    ty: &DynamicType,
455    r: &mut BufferReader<'_>,
456    xcdr2: bool,
457) -> R<DynamicData> {
458    let members = read_all_mutable_members(r).map_err(|e| CodecError::Wire(e.to_string()))?;
459    let mut data = DynamicData::new(ty.clone());
460
461    // The discriminator is the member with the reserved id 0. Track its wire slot
462    // so a branch that happens to also carry id 0 (an out-of-band IDL may number a
463    // case 0; vendors reserve 0) is matched from the REMAINING slots.
464    let disc_idx = members
465        .iter()
466        .position(|m| m.member_id == DISCRIMINATOR_MEMBER_ID)
467        .ok_or_else(|| {
468            CodecError::Wire(
469                "mutable union: no discriminator member (id 0) on the wire".to_string(),
470            )
471        })?;
472    let mut dsub = BufferReader::new(members[disc_idx].body, r.endianness());
473    if xcdr2 {
474        dsub = dsub.xcdr2();
475    }
476    let disc = read_discriminator(&mut dsub, union_discriminator_kind(ty))?;
477
478    let mut chosen = None;
479    let mut default = None;
480    for m in ty.members() {
481        let d = m.descriptor();
482        if d.is_default_label {
483            default = Some(m);
484        }
485        if d.label.contains(&disc) {
486            chosen = Some(m);
487            break;
488        }
489    }
490    if let Some(m) = chosen.or(default) {
491        // A union serializes the discriminator + at most ONE selected branch, so
492        // the single non-discriminator wire member IS that branch — regardless of
493        // the member id the writer assigned it. (Vendors number union cases from 1
494        // after the id-0 discriminator, e.g. FastDDS emits case id 2 for the 2nd
495        // case; a DynamicType built from an out-of-band IDL numbers them from 0.
496        // Matching by position, not id, makes the decode vendor-agnostic.) A
497        // valueless branch is simply absent.
498        if let Some(bw) = members
499            .iter()
500            .enumerate()
501            .find(|(i, _)| *i != disc_idx)
502            .map(|(_, w)| w)
503        {
504            let mt = m.dynamic_type().clone();
505            let mut bsub = BufferReader::new(bw.body, r.endianness());
506            if xcdr2 {
507                bsub = bsub.xcdr2();
508            }
509            let val = read_value(&mt, &mut bsub, xcdr2)?;
510            data.set_value_raw(m.id(), val);
511        }
512    }
513    Ok(data)
514}
515
516fn decode_union_body(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
517    let disc = read_discriminator(r, union_discriminator_kind(ty))?;
518    let mut data = DynamicData::new(ty.clone());
519    let mut chosen = None;
520    let mut default = None;
521    for m in ty.members() {
522        let d = m.descriptor();
523        if d.is_default_label {
524            default = Some(m);
525        }
526        if d.label.contains(&disc) {
527            chosen = Some(m);
528            break;
529        }
530    }
531    if let Some(m) = chosen.or(default) {
532        let mt = m.dynamic_type().clone();
533        let val = read_value(&mt, r, xcdr2)?;
534        data.set_value_raw(m.id(), val);
535    }
536    Ok(data)
537}
538
539/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
540fn read_value(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicValue> {
541    let w = |e: DecodeError| CodecError::Wire(e.to_string());
542    Ok(match ty.kind() {
543        TypeKind::Boolean => DynamicValue::Bool(r.read_u8().map_err(w)? != 0),
544        TypeKind::Byte => DynamicValue::Byte(r.read_u8().map_err(w)?),
545        TypeKind::UInt8 => DynamicValue::UInt8(r.read_u8().map_err(w)?),
546        TypeKind::Int8 => DynamicValue::Int8(r.read_u8().map_err(w)? as i8),
547        TypeKind::Int16 => DynamicValue::Int16(r.read_u16().map_err(w)? as i16),
548        TypeKind::UInt16 => DynamicValue::UInt16(r.read_u16().map_err(w)?),
549        TypeKind::Int32 => DynamicValue::Int32(r.read_u32().map_err(w)? as i32),
550        TypeKind::UInt32 => DynamicValue::UInt32(r.read_u32().map_err(w)?),
551        TypeKind::Enumeration => DynamicValue::Int32(r.read_u32().map_err(w)? as i32),
552        TypeKind::Int64 => DynamicValue::Int64(r.read_u64().map_err(w)? as i64),
553        TypeKind::UInt64 => DynamicValue::UInt64(r.read_u64().map_err(w)?),
554        TypeKind::Float32 => DynamicValue::Float32(f32::from_bits(r.read_u32().map_err(w)?)),
555        TypeKind::Float64 => DynamicValue::Float64(f64::from_bits(r.read_u64().map_err(w)?)),
556        TypeKind::Char8 => DynamicValue::Char8(r.read_u8().map_err(w)?),
557        TypeKind::Char16 => DynamicValue::Char16(r.read_u16().map_err(w)?),
558        TypeKind::String8 => DynamicValue::String(r.read_string().map_err(w)?),
559        TypeKind::String16 => {
560            // wstring: reuse the hardened WString codec (reads BOM + no-BOM).
561            let ws = WString::decode(r).map_err(w)?;
562            DynamicValue::WString(ws.0.encode_utf16().collect())
563        }
564        TypeKind::Structure | TypeKind::Union => {
565            DynamicValue::Complex(Box::new(decode_aggregate(ty, r, xcdr2)?))
566        }
567        TypeKind::Sequence => read_collection(ty, r, xcdr2, None)?,
568        TypeKind::Array => {
569            let n: usize = ty.descriptor().bound.iter().copied().product::<u32>() as usize;
570            read_collection(ty, r, xcdr2, Some(n))?
571        }
572        TypeKind::Map => read_map(ty, r, xcdr2)?,
573        TypeKind::Alias => read_value(&alias_target(ty)?, r, xcdr2)?,
574        other => {
575            return Err(CodecError::NotSupported(alloc::format!(
576                "member kind {other:?}"
577            )));
578        }
579    })
580}
581
582/// Decode a sequence (`fixed_len == None`) or array (`Some(n)`) element-by-element.
583///
584/// Scalar elements (primitive/enum/string/wstring) are stored as a scalar
585/// `DynamicData` with the value under member id 0; COMPOSITE elements
586/// (struct/union) are stored as the decoded aggregate `DynamicData` directly —
587/// both matching what `data_to_json` / the SQLite sink expect. The resolved
588/// element type comes from [`crate::dynamic::collection::resolved_element`] (set
589/// by the IDL lowering for composite elements), falling back to rebuilding a
590/// scalar element from the shallow descriptor.
591/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
592fn read_collection(
593    ty: &DynamicType,
594    r: &mut BufferReader<'_>,
595    xcdr2: bool,
596    fixed_len: Option<usize>,
597) -> R<DynamicValue> {
598    let desc = ty.descriptor();
599    let elem_desc = desc
600        .element_type
601        .as_ref()
602        .ok_or_else(|| CodecError::Dynamic("collection has no element_type".to_string()))?;
603    let elem_kind = elem_desc.kind;
604    let elem_ty = element_type(ty, elem_desc)?;
605    let w = |e: DecodeError| CodecError::Wire(e.to_string());
606    if xcdr2 && !is_primitive_kind(elem_kind) {
607        let _dheader = r.read_u32().map_err(w)?; // covers count+elements / elements
608    }
609    let count = match fixed_len {
610        Some(n) => n,
611        None => r.read_u32().map_err(w)? as usize,
612    };
613    let mut out = Vec::with_capacity(count.min(4096));
614    for _ in 0..count {
615        out.push(read_element(&elem_ty, r, xcdr2)?);
616    }
617    Ok(DynamicValue::Sequence(out))
618}
619
620/// Read one collection element / map key / map value of type `elem_ty` into a
621/// `DynamicData`: a composite (struct/union) is the decoded aggregate; a scalar
622/// is wrapped under member id 0. Shared by [`read_collection`] and [`read_map`].
623fn read_element(elem_ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicData> {
624    Ok(match read_value(elem_ty, r, xcdr2)? {
625        DynamicValue::Complex(d) => *d,
626        scalar => {
627            let mut d = DynamicData::new(elem_ty.clone());
628            d.set_value_raw(0, scalar);
629            d
630        }
631    })
632}
633
634/// Decode a `map<K,V>` — mirror of the compiled `BTreeMap` codec
635/// (`zerodds-cdr` §7.4.4.6): optional collection DHEADER (XCDR2, only when the
636/// key/value PAIR is non-primitive), then a `u32` entry count, then `count`
637/// flat `(key, value)` pairs in wire order. Key + value types are the fully
638/// resolved ones retained by [`crate::dynamic::collection::map_of`], falling
639/// back to a scalar rebuild from the shallow descriptor.
640/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
641fn read_map(ty: &DynamicType, r: &mut BufferReader<'_>, xcdr2: bool) -> R<DynamicValue> {
642    let (key_ty, val_ty, pair_primitive) = map_kv_types(ty)?;
643    let w = |e: DecodeError| CodecError::Wire(e.to_string());
644    if xcdr2 && !pair_primitive {
645        let _dheader = r.read_u32().map_err(w)?;
646    }
647    let count = r.read_u32().map_err(w)? as usize;
648    let mut out = Vec::with_capacity(count.min(4096));
649    for _ in 0..count {
650        let k = read_element(&key_ty, r, xcdr2)?;
651        let v = read_element(&val_ty, r, xcdr2)?;
652        out.push((k, v));
653    }
654    Ok(DynamicValue::Map(out))
655}
656
657/// Resolve a `map<K,V>`'s key type, value type, and whether the pair is fully
658/// primitive (drives the XCDR2 collection-DHEADER rule, matching the compiled
659/// `K::IS_PRIMITIVE && V::IS_PRIMITIVE`).
660fn map_kv_types(ty: &DynamicType) -> R<(DynamicType, DynamicType, bool)> {
661    let desc = ty.descriptor();
662    let key_desc = desc
663        .key_element_type
664        .as_ref()
665        .ok_or_else(|| CodecError::Dynamic("map has no key type".to_string()))?;
666    let val_desc = desc
667        .element_type
668        .as_ref()
669        .ok_or_else(|| CodecError::Dynamic("map has no value type".to_string()))?;
670    let key_ty = crate::dynamic::collection::resolved_map_key(ty)
671        .cloned()
672        .or_else(|| scalar_dynamic_type(key_desc))
673        .ok_or_else(|| CodecError::NotSupported("map key (no resolved type)".to_string()))?;
674    let val_ty = crate::dynamic::collection::resolved_map_value(ty)
675        .cloned()
676        .or_else(|| scalar_dynamic_type(val_desc))
677        .ok_or_else(|| CodecError::NotSupported("map value (no resolved type)".to_string()))?;
678    let pair_primitive = is_primitive_kind(key_desc.kind) && is_primitive_kind(val_desc.kind);
679    Ok((key_ty, val_ty, pair_primitive))
680}
681
682/// Resolve the element `DynamicType` of a collection: the fully-resolved element
683/// (composite-capable) if attached, else a scalar element rebuilt from the
684/// shallow descriptor.
685fn element_type(
686    ty: &DynamicType,
687    elem_desc: &crate::dynamic::descriptor::TypeDescriptor,
688) -> R<DynamicType> {
689    crate::dynamic::collection::resolved_element(ty)
690        .cloned()
691        .or_else(|| scalar_dynamic_type(elem_desc))
692        .ok_or_else(|| {
693            CodecError::NotSupported(alloc::format!(
694                "collection of {:?} (no resolved element type)",
695                elem_desc.kind
696            ))
697        })
698}
699
700/// Encode a sequence/array, mirroring [`read_collection`] (count for sequences;
701/// XCDR2 collection DHEADER for non-primitive elements; composite elements
702/// written as aggregates, scalars from member id 0).
703/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
704fn write_collection(
705    items: &[DynamicData],
706    ty: &DynamicType,
707    w: &mut BufferWriter,
708    xcdr2: bool,
709) -> R<()> {
710    let desc = ty.descriptor();
711    let elem_desc = desc
712        .element_type
713        .as_ref()
714        .ok_or_else(|| CodecError::Dynamic("collection has no element_type".to_string()))?;
715    let elem_kind = elem_desc.kind;
716    let elem_ty = element_type(ty, elem_desc)?;
717    let is_array = ty.kind() == TypeKind::Array;
718    let e = |x: EncodeError| CodecError::Wire(x.to_string());
719
720    let write_body = |w: &mut BufferWriter| -> R<()> {
721        if !is_array {
722            w.write_u32(items.len() as u32).map_err(e)?;
723        }
724        for d in items {
725            write_element(d, &elem_ty, w, xcdr2)?;
726        }
727        Ok(())
728    };
729
730    if xcdr2 && !is_primitive_kind(elem_kind) {
731        // DHEADER = byte length of the body; encode to a sub-writer, prepend.
732        let mut sub = BufferWriter::new(w.endianness());
733        if xcdr2 {
734            sub = sub.xcdr2();
735        }
736        write_body(&mut sub)?;
737        let body = sub.into_bytes();
738        w.write_u32(body.len() as u32).map_err(e)?;
739        w.write_bytes(&body).map_err(e)?;
740        Ok(())
741    } else {
742        write_body(w)
743    }
744}
745
746/// Write one collection element / map key / map value: composite (struct/union)
747/// as an aggregate; scalar as the value under member id 0. Shared by
748/// [`write_collection`] and [`write_map`].
749fn write_element(
750    d: &DynamicData,
751    elem_ty: &DynamicType,
752    w: &mut BufferWriter,
753    xcdr2: bool,
754) -> R<()> {
755    if matches!(elem_ty.kind(), TypeKind::Structure | TypeKind::Union) {
756        write_aggregate(d, w, xcdr2)
757    } else {
758        let v = d
759            .get_value(0)
760            .ok_or_else(|| CodecError::Dynamic("scalar element unset".to_string()))?;
761        write_value(&v.clone(), elem_ty, w, xcdr2)
762    }
763}
764
765/// Encode a `map<K,V>`, the mirror of [`read_map`]: optional XCDR2 collection
766/// DHEADER (only for a non-primitive key/value pair), a `u32` entry count, then
767/// flat `(key, value)` pairs in the stored order.
768/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
769fn write_map(
770    entries: &[(DynamicData, DynamicData)],
771    ty: &DynamicType,
772    w: &mut BufferWriter,
773    xcdr2: bool,
774) -> R<()> {
775    let (key_ty, val_ty, pair_primitive) = map_kv_types(ty)?;
776    let e = |x: EncodeError| CodecError::Wire(x.to_string());
777    let write_body = |w: &mut BufferWriter| -> R<()> {
778        w.write_u32(entries.len() as u32).map_err(e)?;
779        for (k, v) in entries {
780            write_element(k, &key_ty, w, xcdr2)?;
781            write_element(v, &val_ty, w, xcdr2)?;
782        }
783        Ok(())
784    };
785    if xcdr2 && !pair_primitive {
786        let mut sub = BufferWriter::new(w.endianness());
787        sub = sub.xcdr2();
788        write_body(&mut sub)?;
789        let body = sub.into_bytes();
790        w.write_u32(body.len() as u32).map_err(e)?;
791        w.write_bytes(&body).map_err(e)?;
792        Ok(())
793    } else {
794        write_body(w)
795    }
796}
797
798// ----------------------------------------------------------------------------
799// Encode (mirror)
800// ----------------------------------------------------------------------------
801
802/// Encode `data` to a CDR payload (WITHOUT the encapsulation header), the mirror
803/// of [`decode_dynamic`]: `encode_dynamic(decode_dynamic(ty, b, x, e)?, x, e)? == b`.
804///
805/// # Errors
806/// [`CodecError`].
807pub fn encode_dynamic(data: &DynamicData, xcdr2: bool, big_endian: bool) -> R<Vec<u8>> {
808    let mut w = BufferWriter::new(endian(big_endian));
809    if xcdr2 {
810        w = w.xcdr2();
811    }
812    write_aggregate(data, &mut w, xcdr2)?;
813    Ok(w.into_bytes())
814}
815
816fn write_aggregate(data: &DynamicData, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
817    let ty = data.dynamic_type().clone();
818    match ty.kind() {
819        TypeKind::Structure => write_struct(data, &ty, w, xcdr2),
820        TypeKind::Union => write_union(data, &ty, w, xcdr2),
821        other => Err(CodecError::NotSupported(alloc::format!(
822            "top-level type kind {other:?} is not an aggregate"
823        ))),
824    }
825}
826
827fn write_struct(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
828    match ty.descriptor().extensibility_kind {
829        ExtensibilityKind::Final => write_members_in_order(data, ty, w, xcdr2),
830        ExtensibilityKind::Appendable => {
831            // XCDR1 (classic CDR): an @appendable struct is serialized exactly
832            // like @final — plain, no DHEADER (the DHEADER frame is XCDR2-only).
833            if !xcdr2 {
834                return write_members_in_order(data, ty, w, xcdr2);
835            }
836            let mut inner: Option<CodecError> = None;
837            let res = encode_appendable(w, |sub| {
838                match write_members_in_order(data, ty, sub, xcdr2) {
839                    Ok(()) => Ok(()),
840                    Err(e) => {
841                        inner = Some(e);
842                        Err(enc_sentinel())
843                    }
844                }
845            });
846            match res {
847                Ok(()) => Ok(()),
848                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
849            }
850        }
851        ExtensibilityKind::Mutable => {
852            // XCDR1: a @mutable struct is PL_CDR1 — a 16-bit-PID/length parameter
853            // list terminated by PID_LIST_END (not the XCDR2 EMHEADER/DHEADER).
854            if !xcdr2 {
855                return write_struct_pl_cdr1(data, ty, w);
856            }
857            // PL_CDR2: the EMHEADER parameter list is framed by a DHEADER (so a
858            // nested mutable member is self-delimiting). Mirror the typed codec,
859            // which wraps `encode_mutable_member`s in `encode_appendable`.
860            let mut outer: Option<CodecError> = None;
861            let res = encode_appendable(w, |fw| {
862                for i in 0..ty.member_count() {
863                    let m = match ty.member_by_index(i).ok_or_else(|| {
864                        CodecError::Dynamic(alloc::format!("missing member index {i}"))
865                    }) {
866                        Ok(m) => m,
867                        Err(e) => {
868                            outer = Some(e);
869                            return Err(enc_sentinel());
870                        }
871                    };
872                    let id = m.id();
873                    let Some(val) = data.get_value(id).cloned() else {
874                        continue;
875                    };
876                    let mt = m.dynamic_type().clone();
877                    let mu = m.descriptor().is_must_understand;
878                    let res = encode_mutable_member(fw, id, mu, |sub| {
879                        match write_value(&val, &mt, sub, xcdr2) {
880                            Ok(()) => Ok(()),
881                            Err(e) => {
882                                outer = Some(e);
883                                Err(enc_sentinel())
884                            }
885                        }
886                    });
887                    if let Err(e) = res {
888                        if outer.is_none() {
889                            outer = Some(CodecError::Wire(e.to_string()));
890                        }
891                        return Err(enc_sentinel());
892                    }
893                }
894                Ok(())
895            });
896            res.map_err(|e| outer.unwrap_or_else(|| CodecError::Wire(e.to_string())))
897        }
898    }
899}
900
901fn write_members_in_order(
902    data: &DynamicData,
903    ty: &DynamicType,
904    w: &mut BufferWriter,
905    xcdr2: bool,
906) -> R<()> {
907    for i in 0..ty.member_count() {
908        let m = ty
909            .member_by_index(i)
910            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
911        let id = m.id();
912        let val = data
913            .get_value(id)
914            .ok_or_else(|| CodecError::Dynamic(alloc::format!("member {id} unset on encode")))?
915            .clone();
916        let mt = m.dynamic_type().clone();
917        write_value(&val, &mt, w, xcdr2)?;
918    }
919    Ok(())
920}
921
922/// XCDR1 (`PL_CDR`) encode of a `@mutable` struct: each set member as a
923/// 16-bit-PID/length parameter, terminated by `PID_LIST_END` (XTypes 1.3
924/// §7.4.1.2). Mirrors the typed codec (`encode_pl_cdr1_member` + sentinel).
925fn write_struct_pl_cdr1(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter) -> R<()> {
926    for i in 0..ty.member_count() {
927        let m = ty
928            .member_by_index(i)
929            .ok_or_else(|| CodecError::Dynamic(alloc::format!("missing member index {i}")))?;
930        let id = m.id();
931        let Some(val) = data.get_value(id).cloned() else {
932            continue;
933        };
934        let mt = m.dynamic_type().clone();
935        let mut err: Option<CodecError> = None;
936        let res = encode_pl_cdr1_member(w, id, |sub| match write_value(&val, &mt, sub, false) {
937            Ok(()) => Ok(()),
938            Err(e) => {
939                err = Some(e);
940                Err(enc_sentinel())
941            }
942        });
943        if let Err(e) = res {
944            return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
945        }
946    }
947    write_pl_cdr1_sentinel(w).map_err(|e| CodecError::Wire(e.to_string()))
948}
949
950/// XCDR1 (`PL_CDR`) encode of a `@mutable` union: discriminator as PID 0, then
951/// the selected branch as its own PID, terminated by `PID_LIST_END`.
952fn write_union_pl_cdr1(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter) -> R<()> {
953    let disc_kind = union_discriminator_kind(ty);
954    let m = ty
955        .members()
956        .find(|m| data.get_value(m.id()).is_some())
957        .ok_or_else(|| CodecError::Dynamic("union has no branch set".to_string()))?;
958    let disc = m.descriptor().label.first().copied().unwrap_or(0);
959    let val = data
960        .get_value(m.id())
961        .cloned()
962        .ok_or_else(|| CodecError::Dynamic("union branch value vanished".to_string()))?;
963    let mt = m.dynamic_type().clone();
964
965    let mut err: Option<CodecError> = None;
966    let r1 = encode_pl_cdr1_member(w, DISCRIMINATOR_MEMBER_ID, |sub| match write_discriminator(
967        sub, disc_kind, disc,
968    ) {
969        Ok(()) => Ok(()),
970        Err(e) => {
971            err = Some(e);
972            Err(enc_sentinel())
973        }
974    });
975    if let Err(e) = r1 {
976        return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
977    }
978    let r2 = encode_pl_cdr1_member(w, m.id(), |sub| match write_value(&val, &mt, sub, false) {
979        Ok(()) => Ok(()),
980        Err(e) => {
981            err = Some(e);
982            Err(enc_sentinel())
983        }
984    });
985    if let Err(e) = r2 {
986        return Err(err.unwrap_or_else(|| CodecError::Wire(e.to_string())));
987    }
988    write_pl_cdr1_sentinel(w).map_err(|e| CodecError::Wire(e.to_string()))
989}
990
991fn write_union(data: &DynamicData, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
992    match ty.descriptor().extensibility_kind {
993        ExtensibilityKind::Final => write_union_body(data, ty, w, xcdr2),
994        ExtensibilityKind::Appendable => {
995            // XCDR1: appendable union = plain disc+branch (no DHEADER), like final.
996            if !xcdr2 {
997                return write_union_body(data, ty, w, xcdr2);
998            }
999            let mut inner: Option<CodecError> = None;
1000            let res = encode_appendable(w, |sub| match write_union_body(data, ty, sub, xcdr2) {
1001                Ok(()) => Ok(()),
1002                Err(e) => {
1003                    inner = Some(e);
1004                    Err(enc_sentinel())
1005                }
1006            });
1007            match res {
1008                Ok(()) => Ok(()),
1009                Err(e) => Err(inner.unwrap_or_else(|| CodecError::Wire(e.to_string()))),
1010            }
1011        }
1012        ExtensibilityKind::Mutable => {
1013            if xcdr2 {
1014                write_union_mutable(data, ty, w, xcdr2)
1015            } else {
1016                write_union_pl_cdr1(data, ty, w)
1017            }
1018        }
1019    }
1020}
1021
1022fn write_union_body(
1023    data: &DynamicData,
1024    ty: &DynamicType,
1025    w: &mut BufferWriter,
1026    xcdr2: bool,
1027) -> R<()> {
1028    let disc_kind = union_discriminator_kind(ty);
1029    for m in ty.members() {
1030        let id = m.id();
1031        if let Some(val) = data.get_value(id).cloned() {
1032            let disc = m.descriptor().label.first().copied().unwrap_or(0);
1033            write_discriminator(w, disc_kind, disc)?;
1034            let mt = m.dynamic_type().clone();
1035            return write_value(&val, &mt, w, xcdr2);
1036        }
1037    }
1038    Err(CodecError::Dynamic("union has no branch set".to_string()))
1039}
1040
1041/// PL_CDR(2) union encode (inverse of [`decode_union_mutable`]): the
1042/// discriminator as EMHEADER member [`DISCRIMINATOR_MEMBER_ID`] (always
1043/// must-understand), then the selected branch as its own EMHEADER member. The
1044/// parameter list is bounded by the enclosing frame, exactly like a mutable
1045/// struct (`encode_mutable_member` writes no outer DHEADER itself).
1046fn write_union_mutable(
1047    data: &DynamicData,
1048    ty: &DynamicType,
1049    w: &mut BufferWriter,
1050    xcdr2: bool,
1051) -> R<()> {
1052    let disc_kind = union_discriminator_kind(ty);
1053    let m = ty
1054        .members()
1055        .find(|m| data.get_value(m.id()).is_some())
1056        .ok_or_else(|| CodecError::Dynamic("union has no branch set".to_string()))?;
1057    let disc = m.descriptor().label.first().copied().unwrap_or(0);
1058    let val = data
1059        .get_value(m.id())
1060        .cloned()
1061        .ok_or_else(|| CodecError::Dynamic("union branch value vanished".to_string()))?;
1062    let mt = m.dynamic_type().clone();
1063
1064    let mu = m.descriptor().is_must_understand;
1065    // PL_CDR2: DHEADER-framed parameter list — (1) discriminator as reserved
1066    // EMHEADER member 0 (must-understand), (2) the selected branch as its own
1067    // EMHEADER member. The DHEADER comes from `encode_appendable`.
1068    let mut err: Option<CodecError> = None;
1069    let res =
1070        encode_appendable(w, |fw| {
1071            let r1 = encode_mutable_member(fw, DISCRIMINATOR_MEMBER_ID, true, |sub| {
1072                match write_discriminator(sub, disc_kind, disc) {
1073                    Ok(()) => Ok(()),
1074                    Err(e) => {
1075                        err = Some(e);
1076                        Err(enc_sentinel())
1077                    }
1078                }
1079            });
1080            if let Err(e) = r1 {
1081                if err.is_none() {
1082                    err = Some(CodecError::Wire(e.to_string()));
1083                }
1084                return Err(enc_sentinel());
1085            }
1086            let r2 = encode_mutable_member(fw, m.id(), mu, |sub| {
1087                match write_value(&val, &mt, sub, xcdr2) {
1088                    Ok(()) => Ok(()),
1089                    Err(e) => {
1090                        err = Some(e);
1091                        Err(enc_sentinel())
1092                    }
1093                }
1094            });
1095            if let Err(e) = r2 {
1096                if err.is_none() {
1097                    err = Some(CodecError::Wire(e.to_string()));
1098                }
1099                return Err(enc_sentinel());
1100            }
1101            Ok(())
1102        });
1103    res.map_err(|e| err.unwrap_or_else(|| CodecError::Wire(e.to_string())))
1104}
1105
1106/// zerodds-lint: recursion-depth 64 (runtime DynamicData codec; bounded by type nesting).
1107fn write_value(v: &DynamicValue, ty: &DynamicType, w: &mut BufferWriter, xcdr2: bool) -> R<()> {
1108    let e = |x: EncodeError| CodecError::Wire(x.to_string());
1109    match v {
1110        DynamicValue::Bool(b) => w.write_u8(u8::from(*b)).map_err(e),
1111        DynamicValue::Byte(x) | DynamicValue::UInt8(x) | DynamicValue::Char8(x) => {
1112            w.write_u8(*x).map_err(e)
1113        }
1114        DynamicValue::Int8(x) => w.write_u8(*x as u8).map_err(e),
1115        DynamicValue::Int16(x) => w.write_u16(*x as u16).map_err(e),
1116        DynamicValue::UInt16(x) | DynamicValue::Char16(x) => w.write_u16(*x).map_err(e),
1117        DynamicValue::Int32(x) => w.write_u32(*x as u32).map_err(e),
1118        DynamicValue::UInt32(x) => w.write_u32(*x).map_err(e),
1119        DynamicValue::Int64(x) => w.write_u64(*x as u64).map_err(e),
1120        DynamicValue::UInt64(x) => w.write_u64(*x).map_err(e),
1121        DynamicValue::Float32(x) => w.write_u32(x.to_bits()).map_err(e),
1122        DynamicValue::Float64(x) => w.write_u64(x.to_bits()).map_err(e),
1123        DynamicValue::String(s) => w.write_string(s).map_err(e),
1124        DynamicValue::WString(units) => {
1125            // wstring: reuse the hardened WString codec (global BOM policy —
1126            // matches what zerodds typed writers emit).
1127            WString(String::from_utf16_lossy(units))
1128                .encode(w)
1129                .map_err(e)
1130        }
1131        DynamicValue::Complex(d) => write_aggregate(d, w, xcdr2),
1132        DynamicValue::Sequence(items) => write_collection(items, ty, w, xcdr2),
1133        DynamicValue::Map(entries) => write_map(entries, ty, w, xcdr2),
1134        DynamicValue::None => Ok(()),
1135    }
1136}
1137
1138#[cfg(test)]
1139#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1140mod tests {
1141    use super::*;
1142    use crate::dynamic::builder::DynamicTypeBuilderFactory;
1143    use crate::dynamic::descriptor::{MemberDescriptor, TypeDescriptor};
1144
1145    /// Build a struct type `{ a: int32, b: uint64, s: string, f: float64 }` with
1146    /// the given extensibility.
1147    fn sample_type(ext: ExtensibilityKind) -> DynamicType {
1148        let mut desc = TypeDescriptor::structure("Sample");
1149        desc.extensibility_kind = ext;
1150        let mut b = DynamicTypeBuilderFactory::create_type(desc).expect("builder");
1151        b.add_struct_member("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"))
1152            .unwrap();
1153        b.add_struct_member(
1154            "b",
1155            1,
1156            TypeDescriptor::primitive(TypeKind::UInt64, "uint64"),
1157        )
1158        .unwrap();
1159        let string_desc = DynamicTypeBuilderFactory::create_string_type(255)
1160            .descriptor()
1161            .clone();
1162        b.add_struct_member("s", 2, string_desc).unwrap();
1163        b.add_struct_member(
1164            "f",
1165            3,
1166            TypeDescriptor::primitive(TypeKind::Float64, "float64"),
1167        )
1168        .unwrap();
1169        b.build().expect("build")
1170    }
1171
1172    fn populate(ty: &DynamicType) -> DynamicData {
1173        let mut d = DynamicData::new(ty.clone());
1174        d.set_int32_value(0, -7).unwrap();
1175        d.set_uint64_value(1, 0xDEAD_BEEF_0000_0001).unwrap();
1176        d.set_string_value(2, "hello-dynamic").unwrap();
1177        d.set_float64_value(3, 3.5).unwrap();
1178        d
1179    }
1180
1181    fn roundtrip(ext: ExtensibilityKind, xcdr2: bool) {
1182        let ty = sample_type(ext);
1183        let src = populate(&ty);
1184        let bytes = encode_dynamic(&src, xcdr2, false).expect("encode");
1185        let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1186        // Field values survive.
1187        assert_eq!(back.get_int32_value(0).unwrap(), -7);
1188        assert_eq!(back.get_uint64_value(1).unwrap(), 0xDEAD_BEEF_0000_0001);
1189        assert_eq!(back.get_string_value(2).unwrap(), "hello-dynamic");
1190        assert!((back.get_float64_value(3).unwrap() - 3.5).abs() < f64::EPSILON);
1191        // Byte-exact round-trip: re-encoding the decoded value reproduces the wire.
1192        let bytes2 = encode_dynamic(&back, xcdr2, false).expect("re-encode");
1193        assert_eq!(
1194            bytes, bytes2,
1195            "byte-exact round-trip failed for {ext:?} xcdr2={xcdr2}"
1196        );
1197    }
1198
1199    #[test]
1200    fn roundtrip_final_xcdr2() {
1201        roundtrip(ExtensibilityKind::Final, true);
1202    }
1203    #[test]
1204    fn roundtrip_final_xcdr1() {
1205        roundtrip(ExtensibilityKind::Final, false);
1206    }
1207    #[test]
1208    fn roundtrip_appendable_xcdr2() {
1209        roundtrip(ExtensibilityKind::Appendable, true);
1210    }
1211    #[test]
1212    fn roundtrip_mutable_xcdr2() {
1213        roundtrip(ExtensibilityKind::Mutable, true);
1214    }
1215
1216    /// Union round-trips: discriminator selects the branch, the branch value
1217    /// survives, and re-encode is byte-exact.
1218    #[test]
1219    fn roundtrip_union() {
1220        let disc = TypeDescriptor::primitive(TypeKind::Int32, "int32");
1221        let mut b = DynamicTypeBuilderFactory::create_union("U", disc).expect("union builder");
1222        // case 1 → int32 a
1223        let mut m1 =
1224            MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1225        m1.index = 0;
1226        m1.label = alloc::vec![1];
1227        b.add_member_resolved(m1, DynamicType::new_primitive(TypeKind::Int32))
1228            .unwrap();
1229        // case 2 → uint32 u
1230        let mut m2 = MemberDescriptor::new(
1231            "u",
1232            1,
1233            TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1234        );
1235        m2.index = 1;
1236        m2.label = alloc::vec![2];
1237        b.add_member_resolved(m2, DynamicType::new_primitive(TypeKind::UInt32))
1238            .unwrap();
1239        let u = b.build().expect("build union");
1240
1241        // Select case 2 by setting member id 1.
1242        let mut d = DynamicData::new(u.clone());
1243        d.set_uint32_value(1, 0xABCD).unwrap();
1244        let bytes = encode_dynamic(&d, true, false).expect("encode");
1245        let back = decode_dynamic(&u, &bytes, true, false).expect("decode");
1246        assert_eq!(back.get_uint32_value(1).unwrap(), 0xABCD);
1247        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1248    }
1249
1250    /// MUTABLE (PL_CDR2) union: the discriminator is EMHEADER member id 0, the
1251    /// selected branch is its own EMHEADER member. Round-trips both a branch with
1252    /// id 0 (exercises the discriminator/branch id-0 collision handling) and a
1253    /// branch with id 1, byte-exact re-encode each time.
1254    #[test]
1255    fn roundtrip_union_mutable() {
1256        let build = || {
1257            let mut ud =
1258                TypeDescriptor::union("UM", TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1259            ud.extensibility_kind = ExtensibilityKind::Mutable;
1260            let mut b = DynamicTypeBuilderFactory::create_type(ud).expect("union");
1261            let mut m1 =
1262                MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
1263            m1.index = 0;
1264            m1.label = alloc::vec![1];
1265            b.add_member_resolved(m1, DynamicType::new_primitive(TypeKind::Int32))
1266                .unwrap();
1267            let mut m2 = MemberDescriptor::new(
1268                "u",
1269                1,
1270                TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1271            );
1272            m2.index = 1;
1273            m2.label = alloc::vec![2];
1274            b.add_member_resolved(m2, DynamicType::new_primitive(TypeKind::UInt32))
1275                .unwrap();
1276            b.build().expect("build union")
1277        };
1278
1279        // case "a" (member id 0) — collides with the discriminator's reserved id.
1280        let u = build();
1281        let mut d = DynamicData::new(u.clone());
1282        d.set_int32_value(0, -42).unwrap();
1283        let bytes = encode_dynamic(&d, true, false).expect("encode a");
1284        let back = decode_dynamic(&u, &bytes, true, false).expect("decode a");
1285        assert_eq!(back.get_int32_value(0).unwrap(), -42);
1286        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1287
1288        // case "u" (member id 1).
1289        let mut d2 = DynamicData::new(u.clone());
1290        d2.set_uint32_value(1, 0xDEAD_BEEF).unwrap();
1291        let bytes2 = encode_dynamic(&d2, true, false).expect("encode u");
1292        let back2 = decode_dynamic(&u, &bytes2, true, false).expect("decode u");
1293        assert_eq!(back2.get_uint32_value(1).unwrap(), 0xDEAD_BEEF);
1294        assert_eq!(encode_dynamic(&back2, true, false).unwrap(), bytes2);
1295    }
1296
1297    /// (B) Non-int32 union discriminator: an int16-discriminated union with an
1298    /// int8 branch must encode the discriminator as 2 bytes (not 4) — guards the
1299    /// latent hard-coded-int32 bug the symmetric round-trip alone would mask.
1300    #[test]
1301    fn union_int16_discriminator_is_two_bytes() {
1302        let mut ud =
1303            TypeDescriptor::union("U16", TypeDescriptor::primitive(TypeKind::Int16, "int16"));
1304        ud.extensibility_kind = ExtensibilityKind::Final;
1305        let mut b = DynamicTypeBuilderFactory::create_type(ud).expect("union");
1306        let mut m =
1307            MemberDescriptor::new("a", 0, TypeDescriptor::primitive(TypeKind::Int8, "int8"));
1308        m.index = 0;
1309        m.label = alloc::vec![10];
1310        b.add_member_resolved(m, DynamicType::new_primitive(TypeKind::Int8))
1311            .unwrap();
1312        let u = b.build().expect("build");
1313
1314        let mut d = DynamicData::new(u.clone());
1315        d.set_int8_value(0, 7).unwrap();
1316        let bytes = encode_dynamic(&d, true, false).expect("encode");
1317        // [disc i16 LE = 0x000a][int8 = 0x07] => exactly 3 bytes.
1318        assert_eq!(
1319            bytes,
1320            alloc::vec![0x0a, 0x00, 0x07],
1321            "discriminator must be 2 bytes"
1322        );
1323        let back = decode_dynamic(&u, &bytes, true, false).expect("decode");
1324        assert_eq!(back.get_int8_value(0).unwrap(), 7);
1325    }
1326
1327    // Primitives have no member id 0, so the validating typed setters reject them;
1328    // scalar elements store their value raw (the codec does the same internally).
1329    fn scalar_i32(v: i32) -> DynamicData {
1330        let mut d = DynamicData::new(DynamicType::new_primitive(TypeKind::Int32));
1331        d.set_value_raw(0, DynamicValue::Int32(v));
1332        d
1333    }
1334
1335    /// (A) sequence<int32> round-trips.
1336    #[test]
1337    fn roundtrip_seq_int32() {
1338        let mut desc = TypeDescriptor::structure("S");
1339        desc.extensibility_kind = ExtensibilityKind::Final;
1340        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1341        let seq = TypeDescriptor::sequence(
1342            "seq",
1343            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
1344            0,
1345        );
1346        b.add_struct_member("xs", 0, seq).unwrap();
1347        let ty = b.build().unwrap();
1348
1349        let mut d = DynamicData::new(ty.clone());
1350        let items = alloc::vec![scalar_i32(11), scalar_i32(-22), scalar_i32(33),];
1351        d.set_sequence_value(0, items).unwrap();
1352        let bytes = encode_dynamic(&d, true, false).expect("encode");
1353        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1354        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1355            panic!("seq")
1356        };
1357        let got: alloc::vec::Vec<i32> = v.iter().map(|e| e.get_int32_value(0).unwrap()).collect();
1358        assert_eq!(got, alloc::vec![11, -22, 33]);
1359        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1360    }
1361
1362    /// (A) sequence<string> round-trips (exercises the XCDR2 collection DHEADER).
1363    #[test]
1364    fn roundtrip_seq_string() {
1365        let mut desc = TypeDescriptor::structure("S");
1366        desc.extensibility_kind = ExtensibilityKind::Final;
1367        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1368        let strd = DynamicTypeBuilderFactory::create_string_type(255)
1369            .descriptor()
1370            .clone();
1371        let seq = TypeDescriptor::sequence("seq", strd, 0);
1372        b.add_struct_member("ss", 0, seq).unwrap();
1373        let ty = b.build().unwrap();
1374
1375        let mut d = DynamicData::new(ty.clone());
1376        let mk = |s: &str| {
1377            let mut e = DynamicData::new(DynamicTypeBuilderFactory::create_string_type(255));
1378            e.set_value_raw(0, DynamicValue::String(s.into()));
1379            e
1380        };
1381        d.set_sequence_value(0, alloc::vec![mk("a"), mk("bb"), mk("ccc")])
1382            .unwrap();
1383        let bytes = encode_dynamic(&d, true, false).expect("encode");
1384        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1385        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1386            panic!("seq")
1387        };
1388        let got: alloc::vec::Vec<alloc::string::String> =
1389            v.iter().map(|e| e.get_string_value(0).unwrap()).collect();
1390        assert_eq!(
1391            got,
1392            alloc::vec!["a".to_string(), "bb".to_string(), "ccc".to_string()]
1393        );
1394        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1395    }
1396
1397    /// (A) fixed array int32[3] round-trips (no count prefix).
1398    #[test]
1399    fn roundtrip_array_int32() {
1400        let mut desc = TypeDescriptor::structure("S");
1401        desc.extensibility_kind = ExtensibilityKind::Final;
1402        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1403        let arr = TypeDescriptor::array(
1404            "arr",
1405            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
1406            alloc::vec![3],
1407        );
1408        b.add_struct_member("a", 0, arr).unwrap();
1409        let ty = b.build().unwrap();
1410
1411        let mut d = DynamicData::new(ty.clone());
1412        let items = alloc::vec![scalar_i32(7), scalar_i32(8), scalar_i32(9),];
1413        d.set_sequence_value(0, items).unwrap();
1414        let bytes = encode_dynamic(&d, true, false).expect("encode");
1415        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1416        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1417            panic!("arr")
1418        };
1419        let got: alloc::vec::Vec<i32> = v.iter().map(|e| e.get_int32_value(0).unwrap()).collect();
1420        assert_eq!(got, alloc::vec![7, 8, 9]);
1421        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1422    }
1423
1424    /// Composite-element collection: `sequence<Inner>` round-trips, each element
1425    /// is the decoded aggregate, and re-encode is byte-exact (XCDR2 collection
1426    /// DHEADER + per-element appendable DHEADERs).
1427    #[test]
1428    fn roundtrip_seq_of_struct() {
1429        use crate::dynamic::collection;
1430        let inner = sample_type(ExtensibilityKind::Final);
1431        let seq_ty = collection::sequence_of(inner.clone(), 0);
1432        let mut desc = TypeDescriptor::structure("S");
1433        desc.extensibility_kind = ExtensibilityKind::Final;
1434        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1435        let mut md = MemberDescriptor::new("xs", 0, seq_ty.descriptor().clone());
1436        md.index = 0;
1437        b.add_member_resolved(md, seq_ty).unwrap();
1438        let ty = b.build().unwrap();
1439
1440        let mut d = DynamicData::new(ty.clone());
1441        let e0 = populate(&inner);
1442        let mut e1 = DynamicData::new(inner.clone());
1443        e1.set_int32_value(0, 123).unwrap();
1444        e1.set_uint64_value(1, 7).unwrap();
1445        e1.set_string_value(2, "second").unwrap();
1446        e1.set_float64_value(3, -1.25).unwrap();
1447        d.set_sequence_value(0, alloc::vec![e0, e1]).unwrap();
1448
1449        for xcdr2 in [true, false] {
1450            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1451            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1452            let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1453                panic!("seq")
1454            };
1455            assert_eq!(v.len(), 2);
1456            // Composite element = the aggregate itself (kind Structure).
1457            assert_eq!(v[0].dynamic_type().kind(), TypeKind::Structure);
1458            assert_eq!(v[0].get_string_value(2).unwrap(), "hello-dynamic");
1459            assert_eq!(v[1].get_int32_value(0).unwrap(), 123);
1460            assert_eq!(v[1].get_string_value(2).unwrap(), "second");
1461            assert_eq!(
1462                encode_dynamic(&back, xcdr2, false).unwrap(),
1463                bytes,
1464                "byte-exact seq<struct> xcdr2={xcdr2}"
1465            );
1466        }
1467    }
1468
1469    /// Composite-element fixed array `Inner[2]` round-trips (no count prefix).
1470    #[test]
1471    fn roundtrip_array_of_struct() {
1472        use crate::dynamic::collection;
1473        let inner = sample_type(ExtensibilityKind::Final);
1474        let arr_ty = collection::array_of(inner.clone(), alloc::vec![2], "arr");
1475        let mut desc = TypeDescriptor::structure("S");
1476        desc.extensibility_kind = ExtensibilityKind::Final;
1477        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1478        let mut md = MemberDescriptor::new("a", 0, arr_ty.descriptor().clone());
1479        md.index = 0;
1480        b.add_member_resolved(md, arr_ty).unwrap();
1481        let ty = b.build().unwrap();
1482
1483        let mut d = DynamicData::new(ty.clone());
1484        let mut e0 = DynamicData::new(inner.clone());
1485        e0.set_int32_value(0, 1).unwrap();
1486        e0.set_uint64_value(1, 1).unwrap();
1487        e0.set_string_value(2, "x").unwrap();
1488        e0.set_float64_value(3, 1.0).unwrap();
1489        let mut e1 = DynamicData::new(inner.clone());
1490        e1.set_int32_value(0, 2).unwrap();
1491        e1.set_uint64_value(1, 2).unwrap();
1492        e1.set_string_value(2, "y").unwrap();
1493        e1.set_float64_value(3, 2.0).unwrap();
1494        d.set_sequence_value(0, alloc::vec![e0, e1]).unwrap();
1495
1496        let bytes = encode_dynamic(&d, true, false).expect("encode");
1497        let back = decode_dynamic(&ty, &bytes, true, false).expect("decode");
1498        let DynamicValue::Sequence(v) = back.get_value(0).unwrap() else {
1499            panic!("arr")
1500        };
1501        assert_eq!(v.len(), 2);
1502        assert_eq!(v[0].get_string_value(2).unwrap(), "x");
1503        assert_eq!(v[1].get_int32_value(0).unwrap(), 2);
1504        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1505    }
1506
1507    /// `wstring` member round-trips (via the hardened `WString` codec).
1508    #[test]
1509    fn roundtrip_wstring() {
1510        let mut desc = TypeDescriptor::structure("W");
1511        desc.extensibility_kind = ExtensibilityKind::Final;
1512        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1513        let wdesc = DynamicTypeBuilderFactory::create_wstring_type(0)
1514            .descriptor()
1515            .clone();
1516        let mut md = MemberDescriptor::new("w", 0, wdesc);
1517        md.index = 0;
1518        b.add_member_resolved(md, DynamicTypeBuilderFactory::create_wstring_type(0))
1519            .unwrap();
1520        let ty = b.build().unwrap();
1521
1522        let mut d = DynamicData::new(ty.clone());
1523        let units: alloc::vec::Vec<u16> = "wïde→".encode_utf16().collect();
1524        d.set_value_raw(0, DynamicValue::WString(units.clone()));
1525        for xcdr2 in [true, false] {
1526            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1527            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1528            let DynamicValue::WString(got) = back.get_value(0).unwrap() else {
1529                panic!("wstring")
1530            };
1531            assert_eq!(*got, units);
1532            assert_eq!(encode_dynamic(&back, xcdr2, false).unwrap(), bytes);
1533        }
1534    }
1535
1536    /// Nested struct member round-trips (the member is a full DynamicType).
1537    #[test]
1538    fn roundtrip_nested_struct() {
1539        let inner = sample_type(ExtensibilityKind::Final);
1540        let mut desc = TypeDescriptor::structure("Outer");
1541        desc.extensibility_kind = ExtensibilityKind::Final;
1542        let mut b = DynamicTypeBuilderFactory::create_type(desc).expect("builder");
1543        b.add_struct_member(
1544            "id",
1545            0,
1546            TypeDescriptor::primitive(TypeKind::UInt32, "uint32"),
1547        )
1548        .unwrap();
1549        let mut md = MemberDescriptor::new("inner", 1, inner.descriptor().clone());
1550        md.index = 1;
1551        b.add_member_resolved(md, inner.clone()).unwrap();
1552        let outer = b.build().expect("build outer");
1553
1554        let mut d = DynamicData::new(outer.clone());
1555        d.set_uint32_value(0, 99).unwrap();
1556        d.set_complex_value(1, populate(&inner)).unwrap();
1557
1558        let bytes = encode_dynamic(&d, true, false).expect("encode");
1559        let back = decode_dynamic(&outer, &bytes, true, false).expect("decode");
1560        assert_eq!(back.get_uint32_value(0).unwrap(), 99);
1561        let inner_back = back.get_complex_value(1).unwrap();
1562        assert_eq!(inner_back.get_string_value(2).unwrap(), "hello-dynamic");
1563        assert_eq!(encode_dynamic(&back, true, false).unwrap(), bytes);
1564    }
1565
1566    // ------------------------------------------------------------------
1567    // map<K,V> — the reflective codec must be byte-identical to the compiled
1568    // `BTreeMap` CdrEncode (the same wire the generated types + cross-vendor
1569    // stacks emit). These are the authoritative oracle tests.
1570    // ------------------------------------------------------------------
1571
1572    fn scalar_dd(ty: &DynamicType, v: DynamicValue) -> DynamicData {
1573        let mut d = DynamicData::new(ty.clone());
1574        d.set_value_raw(0, v);
1575        d
1576    }
1577
1578    #[test]
1579    fn map_primitive_pair_byte_identical_to_compiled() {
1580        use alloc::collections::BTreeMap;
1581        let kt = DynamicType::new_primitive(TypeKind::Int32);
1582        let vt = DynamicType::new_primitive(TypeKind::Int32);
1583        let map_ty = crate::dynamic::collection::map_of(kt.clone(), vt.clone(), 0, "m");
1584        // Key-sorted, matching BTreeMap iteration order.
1585        let entries = alloc::vec![
1586            (
1587                scalar_dd(&kt, DynamicValue::Int32(1)),
1588                scalar_dd(&vt, DynamicValue::Int32(2))
1589            ),
1590            (
1591                scalar_dd(&kt, DynamicValue::Int32(7)),
1592                scalar_dd(&vt, DynamicValue::Int32(42))
1593            ),
1594        ];
1595        let mut oracle: BTreeMap<i32, i32> = BTreeMap::new();
1596        oracle.insert(7, 42);
1597        oracle.insert(1, 2);
1598
1599        for xcdr2 in [true, false] {
1600            let mut wr = BufferWriter::new(Endianness::Little);
1601            let mut wo = BufferWriter::new(Endianness::Little);
1602            if xcdr2 {
1603                wr = wr.xcdr2();
1604                wo = wo.xcdr2();
1605            }
1606            write_map(&entries, &map_ty, &mut wr, xcdr2).unwrap();
1607            oracle.encode(&mut wo).unwrap();
1608            assert_eq!(
1609                wr.into_bytes(),
1610                wo.into_bytes(),
1611                "map<i32,i32> byte-identical to compiled BTreeMap, xcdr2={xcdr2}"
1612            );
1613        }
1614    }
1615
1616    #[test]
1617    fn map_nonprimitive_value_byte_identical_to_compiled() {
1618        use alloc::collections::BTreeMap;
1619        let kt = DynamicType::new_primitive(TypeKind::Int32);
1620        let vt = DynamicTypeBuilderFactory::create_string_type(0);
1621        let map_ty = crate::dynamic::collection::map_of(kt.clone(), vt.clone(), 0, "m");
1622        let entries = alloc::vec![
1623            (
1624                scalar_dd(&kt, DynamicValue::Int32(1)),
1625                scalar_dd(&vt, DynamicValue::String("one".to_string()))
1626            ),
1627            (
1628                scalar_dd(&kt, DynamicValue::Int32(7)),
1629                scalar_dd(&vt, DynamicValue::String("seven".to_string()))
1630            ),
1631        ];
1632        let mut oracle: BTreeMap<i32, String> = BTreeMap::new();
1633        oracle.insert(7, "seven".to_string());
1634        oracle.insert(1, "one".to_string());
1635
1636        for xcdr2 in [true, false] {
1637            let mut wr = BufferWriter::new(Endianness::Little);
1638            let mut wo = BufferWriter::new(Endianness::Little);
1639            if xcdr2 {
1640                wr = wr.xcdr2();
1641                wo = wo.xcdr2();
1642            }
1643            write_map(&entries, &map_ty, &mut wr, xcdr2).unwrap();
1644            oracle.encode(&mut wo).unwrap();
1645            // The non-primitive-value pair carries the XCDR2 collection DHEADER.
1646            assert_eq!(
1647                wr.into_bytes(),
1648                wo.into_bytes(),
1649                "map<i32,string> byte-identical to compiled BTreeMap, xcdr2={xcdr2}"
1650            );
1651        }
1652    }
1653
1654    /// Full struct-wrapped round-trip: `M { m: map<i32,i32> }` encode → decode →
1655    /// re-encode byte-exact, values preserved.
1656    #[test]
1657    fn roundtrip_map_of_scalar() {
1658        let kt = DynamicType::new_primitive(TypeKind::Int32);
1659        let vt = DynamicType::new_primitive(TypeKind::Int32);
1660        let map_ty = crate::dynamic::collection::map_of(kt.clone(), vt.clone(), 0, "m");
1661        let mut desc = TypeDescriptor::structure("M");
1662        desc.extensibility_kind = ExtensibilityKind::Final;
1663        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1664        let mut md = MemberDescriptor::new("m", 0, map_ty.descriptor().clone());
1665        md.index = 0;
1666        b.add_member_resolved(md, map_ty).unwrap();
1667        let ty = b.build().unwrap();
1668
1669        let mut d = DynamicData::new(ty.clone());
1670        d.set_map_value(
1671            0,
1672            alloc::vec![
1673                (
1674                    scalar_dd(&kt, DynamicValue::Int32(1)),
1675                    scalar_dd(&vt, DynamicValue::Int32(2))
1676                ),
1677                (
1678                    scalar_dd(&kt, DynamicValue::Int32(7)),
1679                    scalar_dd(&vt, DynamicValue::Int32(42))
1680                ),
1681            ],
1682        )
1683        .unwrap();
1684
1685        for xcdr2 in [true, false] {
1686            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1687            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1688            let DynamicValue::Map(got) = back.get_value(0).unwrap() else {
1689                panic!("m is a map")
1690            };
1691            assert_eq!(got.len(), 2);
1692            assert_eq!(got[0].0.get_int32_value(0).unwrap(), 1);
1693            assert_eq!(got[1].1.get_int32_value(0).unwrap(), 42);
1694            assert_eq!(
1695                encode_dynamic(&back, xcdr2, false).unwrap(),
1696                bytes,
1697                "byte-exact map<i32,i32> round-trip xcdr2={xcdr2}"
1698            );
1699        }
1700    }
1701
1702    /// `map<i32,Struct>` — composite value carries its members and round-trips
1703    /// byte-exact (the non-primitive pair takes the XCDR2 DHEADER path).
1704    #[test]
1705    fn roundtrip_map_of_struct() {
1706        let kt = DynamicType::new_primitive(TypeKind::Int32);
1707        let vt = sample_type(ExtensibilityKind::Final);
1708        let map_ty = crate::dynamic::collection::map_of(kt.clone(), vt.clone(), 0, "m");
1709        let mut desc = TypeDescriptor::structure("M");
1710        desc.extensibility_kind = ExtensibilityKind::Final;
1711        let mut b = DynamicTypeBuilderFactory::create_type(desc).unwrap();
1712        let mut md = MemberDescriptor::new("m", 0, map_ty.descriptor().clone());
1713        md.index = 0;
1714        b.add_member_resolved(md, map_ty).unwrap();
1715        let ty = b.build().unwrap();
1716
1717        let entry_val = populate(&vt);
1718        let mut d = DynamicData::new(ty.clone());
1719        d.set_map_value(
1720            0,
1721            alloc::vec![(scalar_dd(&kt, DynamicValue::Int32(5)), entry_val)],
1722        )
1723        .unwrap();
1724
1725        for xcdr2 in [true, false] {
1726            let bytes = encode_dynamic(&d, xcdr2, false).expect("encode");
1727            let back = decode_dynamic(&ty, &bytes, xcdr2, false).expect("decode");
1728            let DynamicValue::Map(got) = back.get_value(0).unwrap() else {
1729                panic!("map")
1730            };
1731            assert_eq!(got.len(), 1);
1732            assert_eq!(got[0].0.get_int32_value(0).unwrap(), 5);
1733            // The value is the composite aggregate itself.
1734            assert_eq!(got[0].1.dynamic_type().kind(), TypeKind::Structure);
1735            assert_eq!(got[0].1.get_string_value(2).unwrap(), "hello-dynamic");
1736            assert_eq!(
1737                encode_dynamic(&back, xcdr2, false).unwrap(),
1738                bytes,
1739                "byte-exact map<i32,struct> round-trip xcdr2={xcdr2}"
1740            );
1741        }
1742    }
1743}