Skip to main content

zerodds_cdr/
composite.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Composite-type encoder/decoder (W2).
4//!
5//! XCDR2 wire-format conventions (OMG XTypes 1.3 §7.4):
6//!
7//! - **String** (§7.4.4): `uint32` length in bytes **including the null
8//!   terminator** + UTF-8 bytes + `\0`.
9//! - **Sequence** (§7.4.4.2): `uint32` element count + elements
10//!   (each element after its own alignment).
11//! - **Array** (§7.4.4.3): `N` elements without a length prefix.
12//! - **Optional** (§7.4.5.1.4): `uint8` present flag (0/1) + value
13//!   if present.
14
15// Module is only compiled under the `alloc` feature (the re-export in
16// lib.rs has the `cfg`); this file depends on `Vec`/`String`.
17
18extern crate alloc;
19use alloc::boxed::Box;
20use alloc::string::String;
21use alloc::vec::Vec;
22
23use crate::type_code::TypeCode;
24
25use crate::buffer::{BufferReader, BufferWriter, XCDR2_MAX_ALIGNMENT};
26use crate::encode::{CdrDecode, CdrEncode};
27use crate::endianness::Endianness;
28use crate::error::{DecodeError, EncodeError};
29
30// ============================================================================
31// XCDR2 DHEADER for collections with non-primitive elements
32// ============================================================================
33//
34// OMG XTypes 1.3 §7.4.3.5: a `sequence<T>` or an array `T[N]` gets a
35// DHEADER (uint32 = byte length of the following content) PREPENDED
36// under XCDR2 (PLAIN_CDR2) when `T` is NON-primitive (string, struct,
37// union, enum, sequence, array, map). Primitive elements
38// (int/float/bool/char/octet) get NO DHEADER. Verified against Cyclone
39// DDS (V-5 seq<long> without, V-6 seq<string>/seq<struct>/seq<enum>
40// with DHEADER). Under XCDR1 (max_alignment == 8) there is never a
41// DHEADER — hence the gate on `max_alignment == XCDR2_MAX_ALIGNMENT`.
42
43/// `true` when, under XCDR2, a collection DHEADER is needed for an
44/// element type with `elem_is_primitive`.
45#[inline]
46fn needs_collection_dheader(writer_max_alignment: usize, elem_is_primitive: bool) -> bool {
47    !elem_is_primitive && writer_max_alignment == XCDR2_MAX_ALIGNMENT
48}
49
50/// Serializes `body` into a sub-writer (same endianness + alignment
51/// cap), prepends a uint32 DHEADER (= body byte length) and writes both
52/// to `writer`. Alignment is equivalent because the DHEADER content
53/// always starts 4-aligned and XCDR2 caps at 4.
54fn write_with_dheader<F>(writer: &mut BufferWriter, body: F) -> Result<(), EncodeError>
55where
56    F: FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
57{
58    let mut sub = BufferWriter::new(writer.endianness()).with_max_alignment(writer.max_alignment());
59    body(&mut sub)?;
60    let bytes = sub.into_bytes();
61    let dheader = u32::try_from(bytes.len()).map_err(|_| EncodeError::ValueOutOfRange {
62        message: "collection DHEADER exceeds u32::MAX",
63    })?;
64    writer.write_u32(dheader)?;
65    writer.write_bytes(&bytes)
66}
67
68// ============================================================================
69// String / &str
70// ============================================================================
71
72impl CdrEncode for str {
73    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
74        let bytes = self.as_bytes();
75        // Length in bytes including the null terminator.
76        let len_with_nul = bytes
77            .len()
78            .checked_add(1)
79            .and_then(|n| u32::try_from(n).ok())
80            .ok_or(EncodeError::ValueOutOfRange {
81                message: "string length exceeds u32::MAX",
82            })?;
83        writer.write_u32(len_with_nul)?;
84        writer.write_bytes(bytes)?;
85        writer.write_u8(0)?;
86        Ok(())
87    }
88}
89
90impl CdrEncode for String {
91    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
92        self.as_str().encode(writer)
93    }
94}
95
96impl CdrDecode for String {
97    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
98        let len_with_nul = reader.read_u32()? as usize;
99        if len_with_nul == 0 {
100            return Err(DecodeError::LengthExceeded {
101                announced: 0,
102                remaining: reader.remaining(),
103                offset: reader.position(),
104            });
105        }
106        if len_with_nul > reader.remaining() {
107            return Err(DecodeError::LengthExceeded {
108                announced: len_with_nul,
109                remaining: reader.remaining(),
110                offset: reader.position(),
111            });
112        }
113        // Last byte must be the null terminator.
114        let payload_len = len_with_nul - 1;
115        let offset = reader.position();
116        let bytes = reader.read_bytes(payload_len)?;
117        let s = core::str::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8 { offset })?;
118        let owned = String::from(s);
119        // Consume the null terminator.
120        let nul = reader.read_u8()?;
121        if nul != 0 {
122            return Err(DecodeError::InvalidUtf8 { offset });
123        }
124        Ok(owned)
125    }
126}
127
128// ============================================================================
129// WString — IDL `wstring` (CORBA-GIOP-1.2-Wire, §9.3.2.7 / §15.3.2.7)
130// ============================================================================
131
132/// IDL `wstring` wrapper. Holds the text as a Rust `String` (Unicode), but the
133/// **wire format** differs from `string`: GIOP 1.2 encodes a `wstring` as a
134/// `uint32` length **in octets** (NOT characters, NOT incl. terminator)
135/// followed by the UTF-16 code units in the message byte order — **without** a
136/// null terminator. This makes `wstring` distinct from `string` (UTF-8) and
137/// interop-capable with ORBs whose transmission codeset is UTF-16 (the default
138/// for omniORB/TAO/JacORB).
139#[derive(Debug, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
140pub struct WString(pub String);
141
142impl WString {
143    /// Borrows the inner text.
144    #[must_use]
145    pub fn as_str(&self) -> &str {
146        &self.0
147    }
148
149    /// Encodes the GIOP 1.2 wstring with an **explicit** BOM choice,
150    /// bypassing the global [`corba_wstring_bom`] policy. `with_bom = true`
151    /// = omniORB/TAO form (a `0xFEFF` byte-order mark prepended, counted in
152    /// the octet length); `false` = JacORB form (no BOM, units in message
153    /// byte order). An empty wstring is length 0 either way.
154    ///
155    /// # Errors
156    /// `ValueOutOfRange` if the octet length exceeds `u32::MAX`.
157    pub fn encode_with_bom(
158        &self,
159        writer: &mut BufferWriter,
160        with_bom: bool,
161    ) -> Result<(), EncodeError> {
162        let units = self.0.encode_utf16().count();
163        if units == 0 {
164            writer.write_u32(0)?;
165            return Ok(());
166        }
167        let total_units = if with_bom {
168            units.saturating_add(1)
169        } else {
170            units
171        };
172        let octets = u32::try_from(total_units.saturating_mul(2)).map_err(|_| {
173            EncodeError::ValueOutOfRange {
174                message: "CDR wstring length exceeds u32::MAX",
175            }
176        })?;
177        writer.write_u32(octets)?;
178        // write_u16 respects endianness; align(2) is a no-op here, since the
179        // position after the uint32 is 4-aligned (and thus 2-aligned).
180        if with_bom {
181            writer.write_u16(UTF16_BOM)?;
182        }
183        for unit in self.0.encode_utf16() {
184            writer.write_u16(unit)?;
185        }
186        Ok(())
187    }
188}
189
190impl From<&str> for WString {
191    fn from(s: &str) -> Self {
192        Self(String::from(s))
193    }
194}
195
196impl From<String> for WString {
197    fn from(s: String) -> Self {
198        Self(s)
199    }
200}
201
202/// Byte-order mark for UTF-16 (§15.3.1.6): `0xFEFF`. A reader with the
203/// reverse byte order sees the mirrored `0xFFFE` and swaps.
204const UTF16_BOM: u16 = 0xFEFF;
205
206/// Process-global policy: does the GIOP `wstring` encoder prepend a UTF-16
207/// byte-order mark? `true` (default) = omniORB/TAO form (BOM, counted in the
208/// octet length); `false` = JacORB form (no BOM, units in message byte
209/// order). Underspecified by §15.3.1.6 (the BOM is optional) and real ORBs
210/// disagree, so this is a configuration switch rather than a fixed choice.
211///
212/// `AtomicBool` (not a thread-local) because `zerodds-cdr` is `no_std`; the
213/// wstring wire form is a deployment-wide interop setting, so a single
214/// process-global is the right granularity. The **decoder** always accepts
215/// both forms (see [`WString::decode`]), so this only affects what ZeroDDS
216/// *emits* — set it to match the peer ORB.
217static CORBA_WSTRING_BOM: core::sync::atomic::AtomicBool =
218    core::sync::atomic::AtomicBool::new(true);
219
220/// Sets the GIOP `wstring` BOM policy (see [`CORBA_WSTRING_BOM`]). Call once
221/// at startup to match the peer ORB: leave the default (`true`) for
222/// omniORB/TAO, pass `false` for JacORB-style (no BOM).
223pub fn set_corba_wstring_bom(with_bom: bool) {
224    CORBA_WSTRING_BOM.store(with_bom, core::sync::atomic::Ordering::Relaxed);
225}
226
227/// Current GIOP `wstring` BOM policy (default `true`).
228#[must_use]
229pub fn corba_wstring_bom() -> bool {
230    CORBA_WSTRING_BOM.load(core::sync::atomic::Ordering::Relaxed)
231}
232
233impl CdrEncode for WString {
234    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
235        // GIOP 1.2 wstring (§15.3.2.7): uint32 length **in octets**, then the
236        // UTF-16 code units, no null terminator. Per §15.3.1.6 a byte-order
237        // mark (0xFEFF) MAY be prepended; whether it is is governed by the
238        // process-global [`corba_wstring_bom`] policy (default `true` =
239        // omniORB/TAO; set `false` for JacORB).
240        self.encode_with_bom(writer, corba_wstring_bom())
241    }
242}
243
244impl CdrDecode for WString {
245    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
246        let octets = reader.read_u32()? as usize;
247        if octets % 2 != 0 || octets > reader.remaining() {
248            return Err(DecodeError::LengthExceeded {
249                announced: octets,
250                remaining: reader.remaining(),
251                offset: reader.position(),
252            });
253        }
254        if octets == 0 {
255            return Ok(Self(String::new()));
256        }
257        let offset = reader.position();
258        // §15.3.1.6: a leading BOM, if present, fixes the units' byte order;
259        // if absent, the message byte order applies (so a no-BOM wstring in a
260        // LE message reads LE, and JacORB's no-BOM units in a BE message read
261        // BE). This reads both omniORB/TAO (BOM) and JacORB (no BOM) and is
262        // self-consistent with the no-BOM form `encode_with_bom` emits.
263        let msg_big_endian = matches!(reader.endianness(), Endianness::Big);
264        let bytes = reader.read_bytes(octets)?;
265        let (start, big_endian) = match (bytes[0], bytes[1]) {
266            (0xFE, 0xFF) => (2, true),  // BOM big-endian
267            (0xFF, 0xFE) => (2, false), // BOM little-endian
268            _ => (0, msg_big_endian),   // no BOM -> message byte order
269        };
270        let mut units = Vec::with_capacity((octets - start) / 2);
271        let mut idx = start;
272        while idx + 1 < octets {
273            let pair = [bytes[idx], bytes[idx + 1]];
274            units.push(if big_endian {
275                u16::from_be_bytes(pair)
276            } else {
277                u16::from_le_bytes(pair)
278            });
279            idx += 2;
280        }
281        let s = String::from_utf16(&units).map_err(|_| DecodeError::InvalidUtf8 { offset })?;
282        Ok(Self(s))
283    }
284}
285
286// ============================================================================
287// WChar — IDL `wchar` (CORBA-GIOP-1.2-Wire, §15.3.1.6)
288// ============================================================================
289
290/// IDL `wchar` wrapper for the **CORBA/GIOP 1.2** wire form. Holds a single
291/// Unicode scalar (`char`), but unlike the DDS XCDR2 `wchar` (a bare 2-byte
292/// UTF-16 code unit, no prefix) the GIOP 1.2 form is a **1-octet length**
293/// (the octet count of the wchar in the transmission code set — 2 for a BMP
294/// character in UTF-16) followed by the UTF-16 code unit(s).
295///
296/// **Byte order:** the code units are emitted **big-endian** regardless of the
297/// message byte order. This is the consensus oracle-confirmed behaviour of
298/// both omniORB 4.3 (writes `02 00 41` for `'A'` even inside a little-endian
299/// encapsulation) and JacORB 3.9 (`02 00 41`, big-endian message). The default
300/// UTF-16 transmission wide code set has no per-`wchar` BOM, so network
301/// (big-endian) order is used. `wchar 'A'` (U+0041) = `02 0041`.
302///
303/// A CORBA `wchar` is a single transmission unit: real ORBs (omniORB
304/// `CORBA::WChar`, Java `char`) are 16-bit, so only BMP characters occur
305/// (length 2, one unit). A non-BMP scalar would need a UTF-16 surrogate pair
306/// (length 4, two units); the encoder handles that defensively even though the
307/// ORBs cannot produce it through `write_wchar`.
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
309pub struct WChar(pub char);
310
311impl WChar {
312    /// The inner Unicode scalar.
313    #[must_use]
314    pub fn as_char(&self) -> char {
315        self.0
316    }
317}
318
319impl From<char> for WChar {
320    fn from(c: char) -> Self {
321        Self(c)
322    }
323}
324
325impl CdrEncode for WChar {
326    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
327        // GIOP 1.2 wchar (§15.3.1.6): a 1-octet length (octet count in the
328        // transmission code set) followed by the UTF-16 code unit(s) in
329        // BIG-ENDIAN (network) order — independent of the message byte order
330        // (oracle-confirmed against omniORB 4.3 + JacORB 3.9).
331        let mut buf = [0u16; 2];
332        let units = self.0.encode_utf16(&mut buf);
333        let octets = units.len().saturating_mul(2);
334        let octets = u8::try_from(octets).map_err(|_| EncodeError::ValueOutOfRange {
335            message: "CDR wchar octet length exceeds u8::MAX",
336        })?;
337        writer.write_u8(octets)?;
338        for unit in units {
339            writer.write_bytes(&unit.to_be_bytes())?;
340        }
341        Ok(())
342    }
343}
344
345impl CdrDecode for WChar {
346    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
347        let offset = reader.position();
348        let octets = reader.read_u8()? as usize;
349        if octets == 0 || octets % 2 != 0 || octets > 4 || octets > reader.remaining() {
350            return Err(DecodeError::LengthExceeded {
351                announced: octets,
352                remaining: reader.remaining(),
353                offset,
354            });
355        }
356        let bytes = reader.read_bytes(octets)?;
357        let mut units = [0u16; 2];
358        let mut count = 0usize;
359        let mut idx = 0usize;
360        while idx + 1 < octets {
361            units[count] = u16::from_be_bytes([bytes[idx], bytes[idx + 1]]);
362            count += 1;
363            idx += 2;
364        }
365        // One scalar from the (1 or 2) UTF-16 units; a lone surrogate or an
366        // invalid pair is rejected.
367        let mut iter = char::decode_utf16(units[..count].iter().copied());
368        let c = iter
369            .next()
370            .and_then(Result::ok)
371            .filter(|_| iter.next().is_none())
372            .ok_or(DecodeError::InvalidUtf8 { offset })?;
373        Ok(Self(c))
374    }
375}
376
377// ============================================================================
378// CorbaAny — IDL `any` (CORBA-GIOP-Wire, §15.3.7: TypeCode + Value)
379// ============================================================================
380
381/// Value variants a [`CorbaAny`] can carry: all scalar IDL types +
382/// string/wstring **and** structured types (sequence/struct/enum + nested
383/// any). The structured variants carry enough type info that the full
384/// [`TypeCode`] (§15.3.5) can be derived from them (e.g. the element
385/// TypeCode even for an empty sequence).
386#[derive(Debug, Clone, PartialEq, Default)]
387pub enum AnyValue {
388    /// `tk_null`.
389    #[default]
390    Null,
391    /// `tk_boolean`.
392    Boolean(bool),
393    /// `tk_octet`.
394    Octet(u8),
395    /// `tk_char`.
396    Char(u8),
397    /// `tk_short`.
398    Short(i16),
399    /// `tk_ushort`.
400    UShort(u16),
401    /// `tk_long`.
402    Long(i32),
403    /// `tk_ulong`.
404    ULong(u32),
405    /// `tk_longlong`.
406    LongLong(i64),
407    /// `tk_ulonglong`.
408    ULongLong(u64),
409    /// `tk_float`.
410    Float(f32),
411    /// `tk_double`.
412    Double(f64),
413    /// `tk_wchar`.
414    WChar(u16),
415    /// `tk_string`.
416    Str(String),
417    /// `tk_wstring`.
418    WStr(WString),
419    /// `tk_sequence`: element TypeCode (needed even for an empty sequence) + items.
420    Seq {
421        /// TypeCode of the elements.
422        element: TypeCode,
423        /// Element values.
424        items: Vec<AnyValue>,
425    },
426    /// `tk_struct`: RepositoryId + name + ordered `(member_name, value)`.
427    Struct {
428        /// `IDL:…:1.0`.
429        repo_id: String,
430        /// Struct name.
431        name: String,
432        /// Members in declaration order.
433        members: Vec<(String, AnyValue)>,
434    },
435    /// `tk_enum`: RepositoryId + name + ordinal value + enumerator names.
436    Enum {
437        /// `IDL:…:1.0`.
438        repo_id: String,
439        /// Enum name.
440        name: String,
441        /// Ordinal value (index into `members`).
442        value: u32,
443        /// Enumerator names.
444        members: Vec<String>,
445    },
446    /// `tk_any`: nested `any`.
447    Any(Box<CorbaAny>),
448}
449
450impl AnyValue {
451    /// Derives the full [`TypeCode`] (§15.3.5) of this value.
452    #[must_use]
453    pub fn type_code(&self) -> TypeCode {
454        match self {
455            Self::Null => TypeCode::Null,
456            Self::Boolean(_) => TypeCode::Boolean,
457            Self::Octet(_) => TypeCode::Octet,
458            Self::Char(_) => TypeCode::Char,
459            Self::Short(_) => TypeCode::Short,
460            Self::UShort(_) => TypeCode::UShort,
461            Self::Long(_) => TypeCode::Long,
462            Self::ULong(_) => TypeCode::ULong,
463            Self::LongLong(_) => TypeCode::LongLong,
464            Self::ULongLong(_) => TypeCode::ULongLong,
465            Self::Float(_) => TypeCode::Float,
466            Self::Double(_) => TypeCode::Double,
467            Self::WChar(_) => TypeCode::WChar,
468            Self::Str(_) => TypeCode::String(0),
469            Self::WStr(_) => TypeCode::WString(0),
470            Self::Seq { element, .. } => TypeCode::Sequence {
471                element: Box::new(element.clone()),
472                bound: 0,
473            },
474            Self::Struct {
475                repo_id,
476                name,
477                members,
478            } => TypeCode::Struct {
479                repo_id: repo_id.clone(),
480                name: name.clone(),
481                members: members
482                    .iter()
483                    .map(|(n, v)| (n.clone(), v.type_code()))
484                    .collect(),
485                is_except: false,
486            },
487            Self::Enum {
488                repo_id,
489                name,
490                members,
491                ..
492            } => TypeCode::Enum {
493                repo_id: repo_id.clone(),
494                name: name.clone(),
495                members: members.clone(),
496            },
497            Self::Any(_) => TypeCode::Any,
498        }
499    }
500
501    /// Writes **only the value** (without the TypeCode), in its CDR representation.
502    fn encode_value(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
503        match self {
504            Self::Null => Ok(()),
505            Self::Boolean(v) => v.encode(w),
506            Self::Octet(v) => v.encode(w),
507            Self::Char(v) => v.encode(w),
508            Self::Short(v) => v.encode(w),
509            Self::UShort(v) => v.encode(w),
510            Self::Long(v) => v.encode(w),
511            Self::ULong(v) => v.encode(w),
512            Self::LongLong(v) => v.encode(w),
513            Self::ULongLong(v) => v.encode(w),
514            Self::Float(v) => v.encode(w),
515            Self::Double(v) => v.encode(w),
516            Self::WChar(v) => v.encode(w),
517            Self::Str(s) => s.encode(w),
518            Self::WStr(s) => s.encode(w),
519            Self::Seq { items, .. } => {
520                let len = u32::try_from(items.len()).map_err(|_| EncodeError::ValueOutOfRange {
521                    message: "any sequence length exceeds u32",
522                })?;
523                w.write_u32(len)?;
524                for it in items {
525                    it.encode_value(w)?;
526                }
527                Ok(())
528            }
529            Self::Struct { members, .. } => {
530                for (_, v) in members {
531                    v.encode_value(w)?;
532                }
533                Ok(())
534            }
535            Self::Enum { value, .. } => w.write_u32(*value),
536            Self::Any(inner) => inner.encode(w),
537        }
538    }
539
540    /// Reads **only the value**, guided by the TypeCode `tc`.
541    fn decode_value(tc: &TypeCode, r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
542        match tc {
543            TypeCode::Null | TypeCode::Void => Ok(Self::Null),
544            TypeCode::Boolean => Ok(Self::Boolean(bool::decode(r)?)),
545            TypeCode::Octet => Ok(Self::Octet(u8::decode(r)?)),
546            TypeCode::Char => Ok(Self::Char(u8::decode(r)?)),
547            TypeCode::Short => Ok(Self::Short(i16::decode(r)?)),
548            TypeCode::UShort => Ok(Self::UShort(u16::decode(r)?)),
549            TypeCode::Long => Ok(Self::Long(i32::decode(r)?)),
550            TypeCode::ULong => Ok(Self::ULong(u32::decode(r)?)),
551            TypeCode::LongLong => Ok(Self::LongLong(i64::decode(r)?)),
552            TypeCode::ULongLong => Ok(Self::ULongLong(u64::decode(r)?)),
553            TypeCode::Float => Ok(Self::Float(f32::decode(r)?)),
554            TypeCode::Double => Ok(Self::Double(f64::decode(r)?)),
555            TypeCode::WChar => Ok(Self::WChar(u16::decode(r)?)),
556            TypeCode::String(_) => Ok(Self::Str(String::decode(r)?)),
557            TypeCode::WString(_) => Ok(Self::WStr(WString::decode(r)?)),
558            TypeCode::Sequence { element, .. } => {
559                let count = r.read_u32()? as usize;
560                let mut items = Vec::with_capacity(count.min(4096));
561                for _ in 0..count {
562                    items.push(Self::decode_value(element, r)?);
563                }
564                Ok(Self::Seq {
565                    element: (**element).clone(),
566                    items,
567                })
568            }
569            TypeCode::Struct {
570                repo_id,
571                name,
572                members,
573                ..
574            } => {
575                let mut out = Vec::with_capacity(members.len());
576                for (mn, mt) in members {
577                    out.push((mn.clone(), Self::decode_value(mt, r)?));
578                }
579                Ok(Self::Struct {
580                    repo_id: repo_id.clone(),
581                    name: name.clone(),
582                    members: out,
583                })
584            }
585            TypeCode::Enum {
586                repo_id,
587                name,
588                members,
589            } => Ok(Self::Enum {
590                repo_id: repo_id.clone(),
591                name: name.clone(),
592                value: r.read_u32()?,
593                members: members.clone(),
594            }),
595            // typedef resolves transparently to the content.
596            TypeCode::Alias { content, .. } => Self::decode_value(content, r),
597            TypeCode::Any => Ok(Self::Any(Box::new(CorbaAny::decode(r)?))),
598            // ObjRef/TypeCode value inside an any: not yet supported.
599            TypeCode::ObjRef { .. } | TypeCode::TypeCodeTc => Err(DecodeError::InvalidEnum {
600                kind: "any value (objref/TypeCode value unsupported)",
601                value: tc.tckind(),
602            }),
603            // Recursive marker: a value can only be decoded against the
604            // RESOLVED type (a recursive any-value is a separate feature).
605            TypeCode::Recursive { .. } => Err(DecodeError::InvalidEnum {
606                kind: "any value against recursive TypeCode marker unsupported",
607                value: tc.tckind(),
608            }),
609        }
610    }
611}
612
613/// IDL `any` (§15.3.7): self-describing = `TypeCode` + `Value`. On the wire,
614/// the value in its representation follows the full [`TypeCode`] (§15.3.5) —
615/// wire-compatible with omniORB/TAO/JacORB, also for structured content
616/// (sequence/struct/enum/nested any).
617#[derive(Debug, Clone, PartialEq, Default)]
618pub struct CorbaAny(pub AnyValue);
619
620impl CdrEncode for CorbaAny {
621    fn encode(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
622        self.0.type_code().encode(w)?;
623        self.0.encode_value(w)
624    }
625}
626
627impl CdrDecode for CorbaAny {
628    fn decode(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
629        let tc = TypeCode::decode(r)?;
630        Ok(Self(AnyValue::decode_value(&tc, r)?))
631    }
632}
633
634// ============================================================================
635// Sequence (Vec<T>)
636// ============================================================================
637
638impl<T: CdrEncode> CdrEncode for Vec<T> {
639    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
640        let len = u32::try_from(self.len()).map_err(|_| EncodeError::ValueOutOfRange {
641            message: "sequence length exceeds u32::MAX",
642        })?;
643        if needs_collection_dheader(writer.max_alignment(), T::IS_PRIMITIVE) {
644            // XCDR2 §7.4.3.5: DHEADER covers [count + elements].
645            write_with_dheader(writer, |sub| {
646                sub.write_u32(len)?;
647                for item in self {
648                    item.encode(sub)?;
649                }
650                Ok(())
651            })
652        } else {
653            writer.write_u32(len)?;
654            for item in self {
655                item.encode(writer)?;
656            }
657            Ok(())
658        }
659    }
660}
661
662impl<T: CdrDecode> CdrDecode for Vec<T> {
663    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
664        if needs_collection_dheader(reader.max_alignment(), T::IS_PRIMITIVE) {
665            // XCDR2 §7.4.3.5: skip the DHEADER before [count + elements].
666            let _dheader = reader.read_u32()?;
667        }
668        let len = reader.read_u32()? as usize;
669        // Defensive sanity check: cannot have more elements than
670        // remaining bytes (at least 1 byte per element).
671        if len > reader.remaining() {
672            return Err(DecodeError::LengthExceeded {
673                announced: len,
674                remaining: reader.remaining(),
675                offset: reader.position(),
676            });
677        }
678        let mut out = Vec::with_capacity(len);
679        for _ in 0..len {
680            out.push(T::decode(reader)?);
681        }
682        Ok(out)
683    }
684}
685
686// ============================================================================
687// Array [T; N]
688// ============================================================================
689
690impl<T: CdrEncode, const N: usize> CdrEncode for [T; N] {
691    // XTypes 1.3 §7.4.3.5 rule (8) PARRAY_TYPE: an array of PRIMITIVE elements
692    // carries NO collection DHEADER, regardless of dimensionality. Propagating
693    // `T::IS_PRIMITIVE` makes a multi-dim primitive array (`[[i32; 3]; 2]`,
694    // T = `[i32; 3]`) report primitive → no DHEADER, while an array of structs
695    // (`[Pt; 2]`) stays non-primitive → DHEADER (rule (9) ARRAY_TYPE).
696    const IS_PRIMITIVE: bool = T::IS_PRIMITIVE;
697
698    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
699        if needs_collection_dheader(writer.max_alignment(), T::IS_PRIMITIVE) {
700            // XCDR2 §7.4.3.5: array without count, DHEADER covers only elements.
701            write_with_dheader(writer, |sub| {
702                for item in self {
703                    item.encode(sub)?;
704                }
705                Ok(())
706            })
707        } else {
708            for item in self {
709                item.encode(writer)?;
710            }
711            Ok(())
712        }
713    }
714}
715
716impl<T: CdrDecode + Default + Copy, const N: usize> CdrDecode for [T; N] {
717    // FINDING (4th, decode/encode asymmetry): must mirror `CdrEncode for
718    // [T; N]`'s `IS_PRIMITIVE = T::IS_PRIMITIVE` (line ~692). Without it a
719    // multi-dim PRIMITIVE array (`[[i32; 3]; 2]`, T = `[i32; 3]`) decoded as a
720    // `[T; N]` element of an OUTER array reported the trait-default `false`,
721    // so the outer decode read a phantom collection DHEADER that the encoder
722    // never wrote (it correctly saw the inner array as primitive) → desync /
723    // `UnexpectedEof`. Propagating it keeps the DHEADER decision symmetric.
724    const IS_PRIMITIVE: bool = T::IS_PRIMITIVE;
725
726    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
727        if needs_collection_dheader(reader.max_alignment(), T::IS_PRIMITIVE) {
728            // XCDR2 §7.4.3.5: DHEADER (uint32) before the array elements.
729            let _dheader = reader.read_u32()?;
730        }
731        let mut out = [T::default(); N];
732        for slot in &mut out {
733            *slot = T::decode(reader)?;
734        }
735        Ok(out)
736    }
737}
738
739// ============================================================================
740// Optional<T>
741// ============================================================================
742
743impl<T: CdrEncode> CdrEncode for Option<T> {
744    fn encode(&self, writer: &mut BufferWriter) -> Result<(), EncodeError> {
745        match self {
746            None => writer.write_u8(0),
747            Some(value) => {
748                writer.write_u8(1)?;
749                value.encode(writer)
750            }
751        }
752    }
753}
754
755impl<T: CdrDecode> CdrDecode for Option<T> {
756    fn decode(reader: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
757        let offset = reader.position();
758        let flag = reader.read_u8()?;
759        match flag {
760            0 => Ok(None),
761            1 => Ok(Some(T::decode(reader)?)),
762            // Other values are forbidden by the XCDR spec — we use
763            // InvalidBool as a pragmatic match (boolean semantics).
764            other => Err(DecodeError::InvalidBool {
765                value: other,
766                offset,
767            }),
768        }
769    }
770}
771
772// ============================================================================
773// Map<K, V> — XCDR2 §7.4.4.6
774// ============================================================================
775//
776// Wire format: 4-byte u32 entry count + N × (K, V) pairs. We
777// serialize entries in BTreeMap iteration order (which is key-sorted,
778// hence reproducible). Decode rebuilds a BTreeMap.
779
780use alloc::collections::BTreeMap;
781
782impl<K, V> CdrEncode for BTreeMap<K, V>
783where
784    K: CdrEncode + Ord,
785    V: CdrEncode,
786{
787    fn encode(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
788        let len = u32::try_from(self.len()).map_err(|_| EncodeError::ValueOutOfRange {
789            message: "map: entry-count > u32::MAX",
790        })?;
791        // XCDR2 §7.4.3.5: a map carries a collection DHEADER only when its
792        // key/value *pair* is non-primitive (a struct/string value), exactly
793        // like a sequence keys on its element type. A `map<primitive,primitive>`
794        // is fully descriptive and gets NO DHEADER. Verified byte-identical to
795        // Fast DDS + OpenDDS (which both omit it for primitive maps and emit it
796        // for map-of-struct). The pair is primitive iff both K and V are.
797        if needs_collection_dheader(w.max_alignment(), K::IS_PRIMITIVE && V::IS_PRIMITIVE) {
798            write_with_dheader(w, |sub| {
799                sub.write_u32(len)?;
800                for (k, v) in self {
801                    k.encode(sub)?;
802                    v.encode(sub)?;
803                }
804                Ok(())
805            })
806        } else {
807            w.write_u32(len)?;
808            for (k, v) in self {
809                k.encode(w)?;
810                v.encode(w)?;
811            }
812            Ok(())
813        }
814    }
815}
816
817impl<K, V> CdrDecode for BTreeMap<K, V>
818where
819    K: CdrDecode + Ord,
820    V: CdrDecode,
821{
822    fn decode(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
823        // Symmetric to encode: only a non-primitive-pair map carries a DHEADER.
824        if needs_collection_dheader(r.max_alignment(), K::IS_PRIMITIVE && V::IS_PRIMITIVE) {
825            let _dheader = r.read_u32()?;
826        }
827        let len = r.read_u32()? as usize;
828        let mut map = BTreeMap::new();
829        for _ in 0..len {
830            let k = K::decode(r)?;
831            let v = V::decode(r)?;
832            map.insert(k, v);
833        }
834        Ok(map)
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
841    use super::*;
842    use crate::Endianness;
843    use alloc::string::ToString;
844    use alloc::vec;
845
846    #[test]
847    fn map_primitive_pair_omits_dheader_xcdr2() {
848        // map<i32,i32> = {7:42}: key+value both primitive -> the map is fully
849        // descriptive, so NO collection DHEADER under XCDR2. Byte-identical to
850        // Fast DDS + OpenDDS (both omit it). Regression for the over-emitted
851        // DHEADER that the map-of-struct golden (which keeps a DHEADER) hid.
852        let mut m = BTreeMap::new();
853        m.insert(7i32, 42i32);
854        let mut w = BufferWriter::new(Endianness::Little).with_max_alignment(XCDR2_MAX_ALIGNMENT);
855        m.encode(&mut w).unwrap();
856        assert_eq!(
857            w.into_bytes(),
858            vec![1, 0, 0, 0, 7, 0, 0, 0, 42, 0, 0, 0],
859            "primitive map must be count+key+value with NO leading DHEADER"
860        );
861        // and it round-trips
862        let bytes = vec![1u8, 0, 0, 0, 7, 0, 0, 0, 42, 0, 0, 0];
863        let mut r =
864            BufferReader::new(&bytes, Endianness::Little).with_max_alignment(XCDR2_MAX_ALIGNMENT);
865        let back: BTreeMap<i32, i32> = BTreeMap::decode(&mut r).unwrap();
866        assert_eq!(back.get(&7), Some(&42));
867    }
868
869    #[test]
870    fn map_non_primitive_value_keeps_dheader_xcdr2() {
871        // map<i32,String>: value is non-primitive -> the map DOES carry a
872        // DHEADER (same rule as a sequence-of-non-primitive, and as the
873        // byte-anchored map-of-struct in the corpus). The first word is the
874        // DHEADER (byte length), not the entry count.
875        let mut m = BTreeMap::new();
876        m.insert(1i32, "x".to_string());
877        let mut w = BufferWriter::new(Endianness::Little).with_max_alignment(XCDR2_MAX_ALIGNMENT);
878        m.encode(&mut w).unwrap();
879        let b = w.into_bytes();
880        assert_ne!(
881            &b[0..4],
882            &[1, 0, 0, 0],
883            "non-primitive map must lead with a DHEADER"
884        );
885        let mut r =
886            BufferReader::new(&b, Endianness::Little).with_max_alignment(XCDR2_MAX_ALIGNMENT);
887        let back: BTreeMap<i32, alloc::string::String> = BTreeMap::decode(&mut r).unwrap();
888        assert_eq!(back.get(&1).map(|s| s.as_str()), Some("x"));
889    }
890
891    #[test]
892    fn wstring_giop12_wire_format_and_roundtrip() {
893        // "Aü€" -> UTF-16: 0x0041, 0x00FC, 0x20AC -> 3 units + BOM = 4 units = 8 octets.
894        let ws = WString::from("Aü€");
895        let mut w = BufferWriter::new(Endianness::Big);
896        ws.encode(&mut w).unwrap();
897        let bytes = w.into_bytes();
898        // Wire: uint32 length-in-octets (8, BE) + BOM(0xFEFF) + 3×u16 (BE), NO terminator.
899        assert_eq!(
900            &bytes[0..4],
901            &[0, 0, 0, 8],
902            "length in octets incl. BOM, not characters"
903        );
904        assert_eq!(
905            &bytes[4..],
906            &[0xFE, 0xFF, 0x00, 0x41, 0x00, 0xFC, 0x20, 0xAC]
907        );
908        assert_eq!(bytes.len(), 12, "no null terminator");
909
910        let mut r = BufferReader::new(&bytes, Endianness::Big);
911        assert_eq!(WString::decode(&mut r).unwrap(), ws);
912    }
913
914    #[test]
915    fn wstring_decodes_foreign_byte_order_via_bom() {
916        // BE message, but the UTF-16 units carry an LE BOM (0xFFFE) and
917        // are little-endian encoded (permitted per §15.3.1.6 — the BOM
918        // controls the unit order independent of the message order). The
919        // length prefix is message order (BE). "Aü€" = 0x0041,0x00FC,0x20AC + BOM = 8 octets.
920        let mut bytes = vec![0, 0, 0, 8]; // length BE, incl. BOM
921        bytes.extend_from_slice(&[0xFF, 0xFE]); // BOM little-endian
922        bytes.extend_from_slice(&[0x41, 0x00, 0xFC, 0x00, 0xAC, 0x20]); // units LE
923        let mut r = BufferReader::new(&bytes, Endianness::Big);
924        assert_eq!(WString::decode(&mut r).unwrap(), WString::from("Aü€"));
925    }
926
927    #[test]
928    fn wstring_decodes_jacorb_style_no_bom_big_endian() {
929        // JacORB sends UTF-16 big-endian WITHOUT a BOM in a BE message. The
930        // default BE path must apply. "Aü€" = 6 octets (no BOM).
931        let mut bytes = vec![0, 0, 0, 6];
932        bytes.extend_from_slice(&[0x00, 0x41, 0x00, 0xFC, 0x20, 0xAC]); // "Aü€" BE
933        let mut r = BufferReader::new(&bytes, Endianness::Big);
934        assert_eq!(WString::decode(&mut r).unwrap(), WString::from("Aü€"));
935    }
936
937    #[test]
938    fn wstring_little_endian_roundtrips() {
939        let ws = WString::from("hello wörld 🌍");
940        let mut w = BufferWriter::new(Endianness::Little);
941        ws.encode(&mut w).unwrap();
942        let bytes = w.into_bytes();
943        let mut r = BufferReader::new(&bytes, Endianness::Little);
944        assert_eq!(WString::decode(&mut r).unwrap(), ws);
945    }
946
947    #[test]
948    fn corba_any_simple_types_roundtrip() {
949        for (label, v) in [
950            ("long", AnyValue::Long(-123456)),
951            ("ulong", AnyValue::ULong(4_000_000_000)),
952            ("double", AnyValue::Double(2.5)),
953            ("boolean", AnyValue::Boolean(true)),
954            ("octet", AnyValue::Octet(0xAB)),
955            ("longlong", AnyValue::LongLong(-1_000_000_000_000)),
956            ("short", AnyValue::Short(-7)),
957            ("char", AnyValue::Char(b'Q')),
958        ] {
959            for e in [Endianness::Big, Endianness::Little] {
960                let any = CorbaAny(v.clone());
961                let mut w = BufferWriter::new(e);
962                any.encode(&mut w).unwrap();
963                let bytes = w.into_bytes();
964                let mut r = BufferReader::new(&bytes, e);
965                assert_eq!(CorbaAny::decode(&mut r).unwrap(), any, "{label}/{e:?}");
966            }
967        }
968    }
969
970    #[test]
971    fn corba_any_long_wire_format() {
972        // tk_long (3) + i32 value. BE: [0,0,0,3][0,0,0,42].
973        let any = CorbaAny(AnyValue::Long(42));
974        let mut w = BufferWriter::new(Endianness::Big);
975        any.encode(&mut w).unwrap();
976        assert_eq!(w.into_bytes(), vec![0, 0, 0, 3, 0, 0, 0, 42]);
977    }
978
979    #[test]
980    fn corba_any_string_roundtrip() {
981        let any = CorbaAny(AnyValue::Str("héllo".to_string()));
982        let mut w = BufferWriter::new(Endianness::Little);
983        any.encode(&mut w).unwrap();
984        let bytes = w.into_bytes();
985        // tk_string (18) + bound (0) + CDR string.
986        assert_eq!(&bytes[0..8], &[18, 0, 0, 0, 0, 0, 0, 0]);
987        let mut r = BufferReader::new(&bytes, Endianness::Little);
988        assert_eq!(CorbaAny::decode(&mut r).unwrap(), any);
989    }
990
991    fn any_rt(v: AnyValue) {
992        for e in [Endianness::Big, Endianness::Little] {
993            let any = CorbaAny(v.clone());
994            let mut w = BufferWriter::new(e);
995            any.encode(&mut w).unwrap();
996            let bytes = w.into_bytes();
997            let mut r = BufferReader::new(&bytes, e);
998            assert_eq!(CorbaAny::decode(&mut r).unwrap(), any, "{v:?} / {e:?}");
999        }
1000    }
1001
1002    #[test]
1003    fn corba_any_sequence_of_long() {
1004        any_rt(AnyValue::Seq {
1005            element: TypeCode::Long,
1006            items: vec![AnyValue::Long(1), AnyValue::Long(-2), AnyValue::Long(3)],
1007        });
1008        // Empty sequence — element TypeCode is preserved.
1009        any_rt(AnyValue::Seq {
1010            element: TypeCode::Double,
1011            items: vec![],
1012        });
1013    }
1014
1015    #[test]
1016    fn corba_any_struct_mixed_members() {
1017        any_rt(AnyValue::Struct {
1018            repo_id: "IDL:Point:1.0".to_string(),
1019            name: "Point".to_string(),
1020            members: vec![
1021                ("x".to_string(), AnyValue::Long(10)),
1022                ("y".to_string(), AnyValue::Long(-20)),
1023                ("label".to_string(), AnyValue::Str("p1".to_string())),
1024                ("active".to_string(), AnyValue::Boolean(true)),
1025            ],
1026        });
1027    }
1028
1029    #[test]
1030    fn corba_any_enum_and_nested_any_and_seq_of_struct() {
1031        any_rt(AnyValue::Enum {
1032            repo_id: "IDL:Color:1.0".to_string(),
1033            name: "Color".to_string(),
1034            value: 2,
1035            members: vec!["RED".to_string(), "GREEN".to_string(), "BLUE".to_string()],
1036        });
1037        // any-in-any.
1038        any_rt(AnyValue::Any(Box::new(CorbaAny(AnyValue::Double(2.5)))));
1039        // sequence<struct> — complex element, nested encaps + values.
1040        let mk = |x: i32| AnyValue::Struct {
1041            repo_id: "IDL:Pair:1.0".to_string(),
1042            name: "Pair".to_string(),
1043            members: vec![
1044                ("k".to_string(), AnyValue::Long(x)),
1045                ("v".to_string(), AnyValue::Str(alloc::format!("v{x}"))),
1046            ],
1047        };
1048        any_rt(AnyValue::Seq {
1049            element: mk(0).type_code(),
1050            items: vec![mk(1), mk(2)],
1051        });
1052    }
1053
1054    #[test]
1055    fn corba_any_wstring_roundtrip() {
1056        let any = CorbaAny(AnyValue::WStr(WString::from("wíde€")));
1057        let mut w = BufferWriter::new(Endianness::Big);
1058        any.encode(&mut w).unwrap();
1059        let bytes = w.into_bytes();
1060        let mut r = BufferReader::new(&bytes, Endianness::Big);
1061        assert_eq!(CorbaAny::decode(&mut r).unwrap(), any);
1062    }
1063
1064    #[test]
1065    fn wchar_giop12_wire_format_pinned_bytes() {
1066        // wchar 'A' (U+0041) = 1-octet length (2) + UTF-16 unit big-endian.
1067        // Pinned native bytes per omniORB 4.3 + JacORB 3.9: 02 0041.
1068        let wc = WChar('A');
1069        let mut w = BufferWriter::new(Endianness::Big);
1070        wc.encode(&mut w).unwrap();
1071        assert_eq!(w.into_bytes(), vec![0x02, 0x00, 0x41]);
1072    }
1073
1074    #[test]
1075    fn wchar_units_are_big_endian_even_in_le_message() {
1076        // The UTF-16 unit is ALWAYS big-endian, independent of message order
1077        // (omniORB emits 02 00 41 inside a little-endian encapsulation).
1078        let wc = WChar('A');
1079        let mut w = BufferWriter::new(Endianness::Little);
1080        wc.encode(&mut w).unwrap();
1081        assert_eq!(w.into_bytes(), vec![0x02, 0x00, 0x41]);
1082    }
1083
1084    #[test]
1085    fn wchar_non_ascii_bmp() {
1086        // 'ü' U+00FC -> 02 00fc ; '€' U+20AC -> 02 20ac (oracle-confirmed).
1087        for (c, want) in [('ü', vec![0x02, 0x00, 0xFC]), ('€', vec![0x02, 0x20, 0xAC])] {
1088            let mut w = BufferWriter::new(Endianness::Big);
1089            WChar(c).encode(&mut w).unwrap();
1090            assert_eq!(w.into_bytes(), want, "wchar {c:?}");
1091        }
1092    }
1093
1094    #[test]
1095    fn wchar_roundtrip_both_orders() {
1096        for c in ['A', 'ü', '€', '\u{4E2D}'] {
1097            for e in [Endianness::Big, Endianness::Little] {
1098                let wc = WChar(c);
1099                let mut w = BufferWriter::new(e);
1100                wc.encode(&mut w).unwrap();
1101                let bytes = w.into_bytes();
1102                let mut r = BufferReader::new(&bytes, e);
1103                assert_eq!(WChar::decode(&mut r).unwrap(), wc, "{c:?}/{e:?}");
1104                assert_eq!(r.remaining(), 0);
1105            }
1106        }
1107    }
1108
1109    #[test]
1110    fn wchar_astral_surrogate_pair_length_4() {
1111        // A non-BMP scalar (U+1F310 🌐) needs a UTF-16 surrogate pair: the
1112        // length octet is 4 and two big-endian units follow. Real ORBs cannot
1113        // produce this via write_wchar (16-bit WChar), but the codec handles
1114        // it self-consistently.
1115        let wc = WChar('\u{1F310}');
1116        let mut w = BufferWriter::new(Endianness::Big);
1117        wc.encode(&mut w).unwrap();
1118        let bytes = w.into_bytes();
1119        assert_eq!(bytes[0], 0x04, "length octet = 4 (two units)");
1120        assert_eq!(&bytes[1..], &[0xD8, 0x3C, 0xDF, 0x10]); // UTF-16BE surrogate pair
1121        let mut r = BufferReader::new(&bytes, Endianness::Big);
1122        assert_eq!(WChar::decode(&mut r).unwrap(), wc);
1123    }
1124
1125    #[test]
1126    fn wchar_decode_rejects_odd_or_oversized_length() {
1127        // odd length
1128        let mut r = BufferReader::new(&[0x01, 0x00], Endianness::Big);
1129        assert!(matches!(
1130            WChar::decode(&mut r),
1131            Err(DecodeError::LengthExceeded { .. })
1132        ));
1133        // zero length
1134        let mut r = BufferReader::new(&[0x00], Endianness::Big);
1135        assert!(matches!(
1136            WChar::decode(&mut r),
1137            Err(DecodeError::LengthExceeded { .. })
1138        ));
1139        // lone high surrogate -> invalid scalar
1140        let mut r = BufferReader::new(&[0x02, 0xD8, 0x3C], Endianness::Big);
1141        assert!(matches!(
1142            WChar::decode(&mut r),
1143            Err(DecodeError::InvalidUtf8 { .. })
1144        ));
1145    }
1146
1147    #[test]
1148    fn wstring_empty() {
1149        let ws = WString::from("");
1150        let mut w = BufferWriter::new(Endianness::Big);
1151        ws.encode(&mut w).unwrap();
1152        let bytes = w.into_bytes();
1153        assert_eq!(
1154            bytes,
1155            vec![0, 0, 0, 0],
1156            "empty wstring = length 0, no bytes"
1157        );
1158        let mut r = BufferReader::new(&bytes, Endianness::Big);
1159        assert_eq!(WString::decode(&mut r).unwrap(), ws);
1160    }
1161
1162    #[test]
1163    fn wstring_bom_policy_default_is_omniorb_form() {
1164        // Default policy = BOM present (omniORB/TAO). Read-only check — does
1165        // NOT flip the global, so it cannot race the byte-asserting tests.
1166        assert!(corba_wstring_bom(), "default GIOP wstring policy is BOM-on");
1167    }
1168
1169    #[test]
1170    fn wstring_encode_with_bom_omniorb_form() {
1171        // BOM form: octet length counts the BOM (units+1)*2; the 2 bytes
1172        // after the uint32 length are the 0xFEFF mark in message byte order.
1173        let ws = WString::from("AB");
1174        let mut w = BufferWriter::new(Endianness::Little);
1175        ws.encode_with_bom(&mut w, true).unwrap();
1176        let bytes = w.into_bytes();
1177        // octets = (2 units + BOM) * 2 = 6; LE length word then BOM 0xFEFF.
1178        assert_eq!(&bytes[0..4], &[6, 0, 0, 0]);
1179        assert_eq!(&bytes[4..6], &[0xFF, 0xFE]); // 0xFEFF little-endian
1180        assert_eq!(bytes.len(), 4 + 6);
1181        // Decoder (BOM-tolerant) round-trips it.
1182        let mut r = BufferReader::new(&bytes, Endianness::Little);
1183        assert_eq!(WString::decode(&mut r).unwrap(), ws);
1184    }
1185
1186    #[test]
1187    fn wstring_encode_with_bom_jacorb_form() {
1188        // No-BOM form (JacORB): octet length = units*2, no 0xFEFF prefix.
1189        let ws = WString::from("AB");
1190        let mut w = BufferWriter::new(Endianness::Little);
1191        ws.encode_with_bom(&mut w, false).unwrap();
1192        let bytes = w.into_bytes();
1193        assert_eq!(&bytes[0..4], &[4, 0, 0, 0]); // octets = 2*2 = 4
1194        assert_eq!(&bytes[4..8], &[0x41, 0x00, 0x42, 0x00]); // 'A','B' LE, no BOM
1195        assert_eq!(bytes.len(), 4 + 4);
1196        let mut r = BufferReader::new(&bytes, Endianness::Little);
1197        assert_eq!(WString::decode(&mut r).unwrap(), ws);
1198    }
1199
1200    #[test]
1201    fn wstring_empty_both_bom_policies_are_length_zero() {
1202        let ws = WString::from("");
1203        for with_bom in [true, false] {
1204            let mut w = BufferWriter::new(Endianness::Little);
1205            ws.encode_with_bom(&mut w, with_bom).unwrap();
1206            assert_eq!(w.into_bytes(), vec![0, 0, 0, 0]);
1207        }
1208    }
1209
1210    fn rt_le<T>(value: T)
1211    where
1212        T: CdrEncode + CdrDecode + PartialEq + core::fmt::Debug,
1213    {
1214        let mut w = BufferWriter::new(Endianness::Little);
1215        value.encode(&mut w).expect("encode");
1216        let bytes = w.into_bytes();
1217        let mut r = BufferReader::new(&bytes, Endianness::Little);
1218        let decoded = T::decode(&mut r).expect("decode");
1219        assert_eq!(decoded, value);
1220        assert_eq!(r.remaining(), 0);
1221    }
1222
1223    // ---- String ----
1224
1225    #[test]
1226    fn string_roundtrip_ascii() {
1227        rt_le(String::from("hello"));
1228    }
1229
1230    #[test]
1231    fn string_roundtrip_unicode() {
1232        rt_le(String::from("Hällo, 🌍 Welt"));
1233    }
1234
1235    #[test]
1236    fn string_roundtrip_empty() {
1237        rt_le(String::new());
1238    }
1239
1240    #[test]
1241    fn string_wire_format_includes_null_terminator() {
1242        let mut w = BufferWriter::new(Endianness::Little);
1243        "ab".encode(&mut w).unwrap();
1244        let bytes = w.into_bytes();
1245        // u32 len = 3 (ab + null) | 'a' 'b' null
1246        assert_eq!(bytes, vec![3, 0, 0, 0, b'a', b'b', 0]);
1247    }
1248
1249    #[test]
1250    fn string_decode_rejects_zero_length() {
1251        let bytes = [0u8, 0, 0, 0]; // u32 len = 0 — no null terminator present
1252        let mut r = BufferReader::new(&bytes, Endianness::Little);
1253        let res = String::decode(&mut r);
1254        assert!(matches!(res, Err(DecodeError::LengthExceeded { .. })));
1255    }
1256
1257    #[test]
1258    fn string_decode_rejects_announced_overrun() {
1259        let bytes = [100u8, 0, 0, 0, b'x'];
1260        let mut r = BufferReader::new(&bytes, Endianness::Little);
1261        let res = String::decode(&mut r);
1262        assert!(matches!(res, Err(DecodeError::LengthExceeded { .. })));
1263    }
1264
1265    #[test]
1266    fn string_decode_rejects_missing_null_terminator() {
1267        // Length 3 (a, b, x) — last byte is 'x' instead of 0.
1268        let bytes = [3u8, 0, 0, 0, b'a', b'b', b'x'];
1269        let mut r = BufferReader::new(&bytes, Endianness::Little);
1270        let res = String::decode(&mut r);
1271        assert!(matches!(res, Err(DecodeError::InvalidUtf8 { .. })));
1272    }
1273
1274    // ---- Sequence (Vec<T>) ----
1275
1276    #[test]
1277    fn sequence_u8_roundtrip() {
1278        rt_le::<Vec<u8>>(vec![1, 2, 3, 4, 5]);
1279    }
1280
1281    #[test]
1282    fn sequence_u32_roundtrip() {
1283        rt_le::<Vec<u32>>(vec![0xDEAD, 0xBEEF, 0x1234]);
1284    }
1285
1286    #[test]
1287    fn sequence_empty_roundtrip() {
1288        rt_le::<Vec<u32>>(vec![]);
1289    }
1290
1291    #[test]
1292    fn sequence_string_roundtrip() {
1293        rt_le::<Vec<String>>(vec!["alpha".to_string(), "beta".to_string()]);
1294    }
1295
1296    #[test]
1297    fn sequence_decode_rejects_overrun_length() {
1298        // Length 999 in 4 bytes Vec<u8>
1299        let bytes = [0xE7u8, 0x03, 0, 0, b'x']; // 999 announced, 1 byte data
1300        let mut r = BufferReader::new(&bytes, Endianness::Little);
1301        let res = Vec::<u8>::decode(&mut r);
1302        assert!(matches!(res, Err(DecodeError::LengthExceeded { .. })));
1303    }
1304
1305    #[test]
1306    fn sequence_alignment_4_byte_prefix() {
1307        // u8 + Vec<u8> → u8 + 3 pad + u32 len + bytes
1308        let mut w = BufferWriter::new(Endianness::Little);
1309        1u8.encode(&mut w).unwrap();
1310        vec![10u8, 20, 30].encode(&mut w).unwrap();
1311        let bytes = w.into_bytes();
1312        assert_eq!(bytes[0], 1); // u8
1313        assert_eq!(&bytes[1..4], &[0, 0, 0]); // padding
1314        assert_eq!(&bytes[4..8], &[3, 0, 0, 0]); // u32 length
1315        assert_eq!(&bytes[8..11], &[10, 20, 30]); // payload
1316    }
1317
1318    // ---- Array ----
1319
1320    #[test]
1321    fn array_u8_roundtrip() {
1322        rt_le::<[u8; 4]>([1, 2, 3, 4]);
1323    }
1324
1325    #[test]
1326    fn array_u32_roundtrip() {
1327        rt_le::<[u32; 3]>([100, 200, 300]);
1328    }
1329
1330    #[test]
1331    fn array_no_length_prefix() {
1332        let mut w = BufferWriter::new(Endianness::Little);
1333        [1u8, 2, 3].encode(&mut w).unwrap();
1334        // No u32 length — only elements.
1335        assert_eq!(w.into_bytes(), vec![1, 2, 3]);
1336    }
1337
1338    #[test]
1339    fn array_zero_size() {
1340        let arr: [u32; 0] = [];
1341        let mut w = BufferWriter::new(Endianness::Little);
1342        arr.encode(&mut w).unwrap();
1343        assert!(w.into_bytes().is_empty());
1344    }
1345
1346    // ---- Optional ----
1347
1348    #[test]
1349    fn optional_none_roundtrip() {
1350        rt_le::<Option<u32>>(None);
1351    }
1352
1353    #[test]
1354    fn optional_some_roundtrip() {
1355        rt_le::<Option<u32>>(Some(42));
1356    }
1357
1358    #[test]
1359    fn optional_some_string_roundtrip() {
1360        rt_le::<Option<String>>(Some("hi".to_string()));
1361    }
1362
1363    #[test]
1364    fn optional_wire_format_none_is_zero_byte() {
1365        let mut w = BufferWriter::new(Endianness::Little);
1366        Option::<u32>::None.encode(&mut w).unwrap();
1367        assert_eq!(w.into_bytes(), vec![0]);
1368    }
1369
1370    #[test]
1371    fn optional_wire_format_some_is_one_then_value() {
1372        let mut w = BufferWriter::new(Endianness::Little);
1373        Some(0xABCDu32).encode(&mut w).unwrap();
1374        let bytes = w.into_bytes();
1375        assert_eq!(bytes[0], 1); // present-flag
1376        // 3 byte padding + 4 byte u32
1377        assert_eq!(&bytes[1..4], &[0, 0, 0]);
1378        assert_eq!(&bytes[4..8], &[0xCD, 0xAB, 0, 0]);
1379    }
1380
1381    #[test]
1382    fn optional_decode_rejects_invalid_flag() {
1383        let bytes = [0xFFu8];
1384        let mut r = BufferReader::new(&bytes, Endianness::Little);
1385        let res = Option::<u32>::decode(&mut r);
1386        assert!(matches!(res, Err(DecodeError::InvalidBool { .. })));
1387    }
1388
1389    // ---- Mixed nested ----
1390
1391    #[test]
1392    fn nested_optional_sequence_string() {
1393        let value: Option<Vec<String>> = Some(vec!["a".to_string(), "bb".to_string()]);
1394        rt_le(value);
1395    }
1396
1397    #[test]
1398    fn nested_array_of_optionals() {
1399        let value: [Option<u32>; 3] = [Some(1), None, Some(3)];
1400        rt_le(value);
1401    }
1402}