Skip to main content

zerodds_corba_ior/
components.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! TaggedComponent + structured decoders for important tags.
5//!
6//! Spec §13.6.7.3 + §15.6.6:
7//! ```text
8//! struct TaggedComponent {
9//!     ComponentId      tag;
10//!     sequence<octet>  component_data;
11//! };
12//! ```
13//!
14//! `component_data` is a CDR encapsulation (endianness octet + body),
15//! its content is tag-specific.
16
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use zerodds_cdr::{BufferReader, BufferWriter, Endianness};
21use zerodds_corba_csiv2::CompoundSecMechList;
22use zerodds_corba_iiop::profile_body::CdrError;
23
24use crate::component_tags::ComponentId;
25
26/// `TaggedComponent` — tag + encapsulation.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct TaggedComponent {
29    /// Component tag.
30    pub tag: ComponentId,
31    /// Encapsulation bytes (endianness octet + tag-specific body).
32    pub component_data: Vec<u8>,
33}
34
35impl TaggedComponent {
36    /// CDR encode.
37    ///
38    /// # Errors
39    /// Buffer write error.
40    pub fn encode(&self, w: &mut BufferWriter) -> Result<(), CdrError> {
41        w.write_u32(self.tag.as_u32())?;
42        let n = u32::try_from(self.component_data.len()).map_err(|_| CdrError::Overflow)?;
43        w.write_u32(n)?;
44        w.write_bytes(&self.component_data)?;
45        Ok(())
46    }
47
48    /// CDR decode.
49    ///
50    /// # Errors
51    /// Buffer read error.
52    pub fn decode(r: &mut BufferReader<'_>) -> Result<Self, CdrError> {
53        let tag = ComponentId::from_u32(r.read_u32()?);
54        let n = r.read_u32()? as usize;
55        let bytes = r.read_bytes(n)?;
56        Ok(Self {
57            tag,
58            component_data: bytes.to_vec(),
59        })
60    }
61
62    /// Attempts to decode the component as a known structured type.
63    ///
64    /// # Errors
65    /// CDR error in the encapsulation body.
66    pub fn structured(&self) -> Result<StructuredComponent, CdrError> {
67        StructuredComponent::decode(self.tag, &self.component_data)
68    }
69}
70
71// -------------------------------------------------------------------
72// Structured component bodies for the most important tags.
73// -------------------------------------------------------------------
74
75/// `TAG_ORB_TYPE` (spec §13.6.6.1).
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct OrbType(pub u32);
78
79/// Code-set component (spec §13.10.2.4).
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct CodeSetComponent {
82    /// Native code set (e.g. `0x00010001` = ISO-8859-1).
83    pub native_code_set: u32,
84    /// Conversion code sets — we hold the list header (count); the full
85    /// content is at the caller. CodeSetComponentInfo models that fully.
86    pub conversion_code_sets: Vec<u32>,
87}
88
89/// `CodeSetComponentInfo` (spec §13.10.2.4).
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct CodeSetComponentInfo {
92    /// Char codeset.
93    pub for_char_data: CodeSetComponent,
94    /// Wide-char codeset.
95    pub for_wchar_data: CodeSetComponent,
96}
97
98/// `TAG_ALTERNATE_IIOP_ADDRESS` (spec §15.7.4).
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct AlternateIiopAddress {
101    /// Host name.
102    pub host: String,
103    /// TCP port.
104    pub port: u16,
105}
106
107/// `TAG_SSL_SEC_TRANS` (OMG `Security/SSLIOP` module spec).
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct Ssl {
110    /// `target_supports` — bitmask of supported AssociationOptions.
111    pub target_supports: u16,
112    /// `target_requires` — bitmask of enforced AssociationOptions.
113    pub target_requires: u16,
114    /// SSL port.
115    pub port: u16,
116}
117
118/// `TAG_TLS_SEC_TRANS` (OMG `Security/TLSIOP` module spec) — wire layout
119/// identical to SSL_SEC_TRANS plus a TransportAddressList.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct TlsSecTrans {
122    /// `target_supports`.
123    pub target_supports: u16,
124    /// `target_requires`.
125    pub target_requires: u16,
126    /// `addresses` — list of alternative TLS endpoints.
127    pub addresses: Vec<AlternateIiopAddress>,
128}
129
130/// `TAG_RMI_CUSTOM_MAX_STREAM_FORMAT` (spec §13.6.7.3 + JavaToIDL).
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct StreamFormatVersion(pub u8);
133
134/// Structured form for the most important components.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum StructuredComponent {
137    /// `TAG_ORB_TYPE`.
138    OrbType(OrbType),
139    /// `TAG_CODE_SETS`.
140    CodeSets(CodeSetComponentInfo),
141    /// `TAG_ALTERNATE_IIOP_ADDRESS`.
142    AlternateIiopAddress(AlternateIiopAddress),
143    /// `TAG_SSL_SEC_TRANS`.
144    Ssl(Ssl),
145    /// `TAG_TLS_SEC_TRANS`.
146    TlsSecTrans(TlsSecTrans),
147    /// `TAG_CSI_SEC_MECH_LIST = 33` (spec CORBA 3.3 Part 2 §10.5).
148    CsiSecMechList(CompoundSecMechList),
149    /// `TAG_RMI_CUSTOM_MAX_STREAM_FORMAT`.
150    StreamFormatVersion(StreamFormatVersion),
151    /// `TAG_JAVA_CODEBASE` (spec §13.6.6.7) — list of codebase URLs.
152    JavaCodebase(String),
153    /// Other tags — opaque encapsulation.
154    Opaque {
155        /// Tag.
156        tag: ComponentId,
157        /// Body.
158        bytes: Vec<u8>,
159    },
160}
161
162impl StructuredComponent {
163    /// Decodes a component encapsulation into a structured form, as far
164    /// as the tag is known.
165    ///
166    /// # Errors
167    /// CDR decode error in the body.
168    pub fn decode(tag: ComponentId, encap: &[u8]) -> Result<Self, CdrError> {
169        let endianness = read_endianness(encap)?;
170        let body = &encap[1..];
171        match tag {
172            ComponentId::OrbType => {
173                let mut r = BufferReader::new(body, endianness);
174                Ok(Self::OrbType(OrbType(r.read_u32()?)))
175            }
176            ComponentId::CodeSets => {
177                let mut r = BufferReader::new(body, endianness);
178                let for_char = decode_code_set_component(&mut r)?;
179                let for_wchar = decode_code_set_component(&mut r)?;
180                Ok(Self::CodeSets(CodeSetComponentInfo {
181                    for_char_data: for_char,
182                    for_wchar_data: for_wchar,
183                }))
184            }
185            ComponentId::AlternateIiopAddress => {
186                let mut r = BufferReader::new(body, endianness);
187                let host = r.read_string()?;
188                let port = r.read_u16()?;
189                Ok(Self::AlternateIiopAddress(AlternateIiopAddress {
190                    host,
191                    port,
192                }))
193            }
194            ComponentId::SslSecTrans => {
195                let mut r = BufferReader::new(body, endianness);
196                Ok(Self::Ssl(Ssl {
197                    target_supports: r.read_u16()?,
198                    target_requires: r.read_u16()?,
199                    port: r.read_u16()?,
200                }))
201            }
202            ComponentId::TlsSecTrans => {
203                let mut r = BufferReader::new(body, endianness);
204                let target_supports = r.read_u16()?;
205                let target_requires = r.read_u16()?;
206                let n = r.read_u32()? as usize;
207                let mut addresses = Vec::with_capacity(n.min(32));
208                for _ in 0..n {
209                    let host = r.read_string()?;
210                    let port = r.read_u16()?;
211                    addresses.push(AlternateIiopAddress { host, port });
212                }
213                Ok(Self::TlsSecTrans(TlsSecTrans {
214                    target_supports,
215                    target_requires,
216                    addresses,
217                }))
218            }
219            ComponentId::CsiSecMechList => {
220                let mut r = BufferReader::new(body, endianness);
221                Ok(Self::CsiSecMechList(CompoundSecMechList::decode(&mut r)?))
222            }
223            ComponentId::RmiCustomMaxStreamFormat => {
224                let mut r = BufferReader::new(body, endianness);
225                Ok(Self::StreamFormatVersion(StreamFormatVersion(r.read_u8()?)))
226            }
227            ComponentId::JavaCodebase => {
228                let mut r = BufferReader::new(body, endianness);
229                Ok(Self::JavaCodebase(r.read_string()?))
230            }
231            other => Ok(Self::Opaque {
232                tag: other,
233                bytes: encap.to_vec(),
234            }),
235        }
236    }
237
238    /// Encodes a structured component into an encapsulation
239    /// (endianness octet + body).
240    ///
241    /// # Errors
242    /// Buffer write error.
243    pub fn encode_encapsulation(&self, endianness: Endianness) -> Result<Vec<u8>, CdrError> {
244        let mut out = Vec::with_capacity(64);
245        out.push(endianness_to_byte(endianness));
246        let mut w = BufferWriter::new(endianness);
247        match self {
248            Self::OrbType(OrbType(v)) => w.write_u32(*v)?,
249            Self::CodeSets(info) => {
250                encode_code_set_component(&mut w, &info.for_char_data)?;
251                encode_code_set_component(&mut w, &info.for_wchar_data)?;
252            }
253            Self::AlternateIiopAddress(a) => {
254                w.write_string(&a.host)?;
255                w.write_u16(a.port)?;
256            }
257            Self::Ssl(s) => {
258                w.write_u16(s.target_supports)?;
259                w.write_u16(s.target_requires)?;
260                w.write_u16(s.port)?;
261            }
262            Self::TlsSecTrans(t) => {
263                w.write_u16(t.target_supports)?;
264                w.write_u16(t.target_requires)?;
265                let n = u32::try_from(t.addresses.len()).map_err(|_| CdrError::Overflow)?;
266                w.write_u32(n)?;
267                for a in &t.addresses {
268                    w.write_string(&a.host)?;
269                    w.write_u16(a.port)?;
270                }
271            }
272            Self::CsiSecMechList(list) => list.encode(&mut w)?,
273            Self::StreamFormatVersion(StreamFormatVersion(v)) => w.write_u8(*v)?,
274            Self::JavaCodebase(s) => w.write_string(s)?,
275            Self::Opaque { bytes, .. } => {
276                // The body in `bytes` already includes the endianness
277                // octet — we just return it verbatim.
278                return Ok(bytes.clone());
279            }
280        }
281        out.extend_from_slice(w.as_bytes());
282        Ok(out)
283    }
284}
285
286fn read_endianness(encap: &[u8]) -> Result<Endianness, CdrError> {
287    if encap.is_empty() {
288        return Err(CdrError::Truncated);
289    }
290    match encap[0] {
291        0 => Ok(Endianness::Big),
292        1 => Ok(Endianness::Little),
293        _ => Err(CdrError::InvalidEndianness),
294    }
295}
296
297const fn endianness_to_byte(e: Endianness) -> u8 {
298    match e {
299        Endianness::Big => 0,
300        Endianness::Little => 1,
301    }
302}
303
304fn decode_code_set_component(r: &mut BufferReader<'_>) -> Result<CodeSetComponent, CdrError> {
305    let native_code_set = r.read_u32()?;
306    let n = r.read_u32()? as usize;
307    let mut conversion = Vec::with_capacity(n.min(16));
308    for _ in 0..n {
309        conversion.push(r.read_u32()?);
310    }
311    Ok(CodeSetComponent {
312        native_code_set,
313        conversion_code_sets: conversion,
314    })
315}
316
317/// Well-known `CodeSetId` fallbacks for the negotiation (OSF registry).
318const CS_UTF_8: u32 = 0x0501_0001;
319const CS_UTF_16: u32 = 0x0001_0109;
320
321impl CodeSetComponent {
322    /// Selects the transmission codeset between client and server for *one*
323    /// codeset axis (char OR wchar), per the algorithm from spec §13.10.2.6:
324    ///
325    /// 1. Native match → native (no conversion).
326    /// 2. Server-native ∈ client conversion → server-native (client converts).
327    /// 3. Client-native ∈ server conversion → client-native (server converts).
328    /// 4. Common conversion codeset → that one.
329    /// 5. `fallback` (e.g. UTF-8/UTF-16) supported by both → `fallback`.
330    /// 6. otherwise incompatible → `None` (caller throws `CODESET_INCOMPATIBLE`).
331    ///
332    /// `self` = client component, `server` = server component (from its IOR).
333    #[must_use]
334    pub fn negotiate(&self, server: &CodeSetComponent, fallback: u32) -> Option<u32> {
335        let cn = self.native_code_set;
336        let sn = server.native_code_set;
337        let supports = |c: &CodeSetComponent, id: u32| {
338            id != 0 && (c.native_code_set == id || c.conversion_code_sets.contains(&id))
339        };
340        // 1. Native match.
341        if cn != 0 && cn == sn {
342            return Some(cn);
343        }
344        // 2. Client can convert to server-native.
345        if sn != 0 && self.conversion_code_sets.contains(&sn) {
346            return Some(sn);
347        }
348        // 3. Server can convert to client-native.
349        if cn != 0 && server.conversion_code_sets.contains(&cn) {
350            return Some(cn);
351        }
352        // 4. Common conversion codeset.
353        if let Some(common) = self
354            .conversion_code_sets
355            .iter()
356            .find(|id| server.conversion_code_sets.contains(id))
357        {
358            return Some(*common);
359        }
360        // 5. Universal fallback, if both carry it (as native OR conversion).
361        if supports(self, fallback) && supports(server, fallback) {
362            return Some(fallback);
363        }
364        None
365    }
366}
367
368impl CodeSetComponentInfo {
369    /// Negotiates both axes (char + wchar) against the server component from
370    /// its IOR. `self` = client capabilities. Returns `(TCSC, TCSW)` or
371    /// `None` if one of the axes is incompatible (→ `CODESET_INCOMPATIBLE`).
372    /// Fallbacks: UTF-8 for `char`, UTF-16 for `wchar`.
373    #[must_use]
374    pub fn negotiate(&self, server: &CodeSetComponentInfo) -> Option<(u32, u32)> {
375        let tcsc = self
376            .for_char_data
377            .negotiate(&server.for_char_data, CS_UTF_8)?;
378        let tcsw = self
379            .for_wchar_data
380            .negotiate(&server.for_wchar_data, CS_UTF_16)?;
381        Some((tcsc, tcsw))
382    }
383}
384
385fn encode_code_set_component(w: &mut BufferWriter, c: &CodeSetComponent) -> Result<(), CdrError> {
386    w.write_u32(c.native_code_set)?;
387    let n = u32::try_from(c.conversion_code_sets.len()).map_err(|_| CdrError::Overflow)?;
388    w.write_u32(n)?;
389    for cs in &c.conversion_code_sets {
390        w.write_u32(*cs)?;
391    }
392    Ok(())
393}
394
395#[cfg(test)]
396#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn orb_type_round_trip() {
402        let s = StructuredComponent::OrbType(OrbType(0x4F4D_4732)); // "OMG2" vendor style
403        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
404        let decoded = StructuredComponent::decode(ComponentId::OrbType, &bytes).unwrap();
405        assert_eq!(decoded, s);
406    }
407
408    #[test]
409    fn code_sets_round_trip_le() {
410        let info = CodeSetComponentInfo {
411            for_char_data: CodeSetComponent {
412                native_code_set: 0x0001_0001,
413                conversion_code_sets: alloc::vec![0x0001_0109],
414            },
415            for_wchar_data: CodeSetComponent {
416                native_code_set: 0x0001_0109,
417                conversion_code_sets: alloc::vec![],
418            },
419        };
420        let s = StructuredComponent::CodeSets(info.clone());
421        let bytes = s.encode_encapsulation(Endianness::Little).unwrap();
422        let decoded = StructuredComponent::decode(ComponentId::CodeSets, &bytes).unwrap();
423        match decoded {
424            StructuredComponent::CodeSets(d) => assert_eq!(d, info),
425            other => panic!("expected CodeSets, got {other:?}"),
426        }
427    }
428
429    fn cs(native: u32, conv: &[u32]) -> CodeSetComponent {
430        CodeSetComponent {
431            native_code_set: native,
432            conversion_code_sets: conv.to_vec(),
433        }
434    }
435
436    #[test]
437    fn negotiate_native_match() {
438        // Both native ISO-8859-1 → exactly that.
439        let client = cs(0x0001_0001, &[]);
440        let server = cs(0x0001_0001, &[]);
441        assert_eq!(client.negotiate(&server, CS_UTF_8), Some(0x0001_0001));
442    }
443
444    #[test]
445    fn negotiate_client_converts_to_server_native() {
446        // Server-native (UTF-8) is in the client conversion set → server-native.
447        let client = cs(0x0001_0001, &[CS_UTF_8]);
448        let server = cs(CS_UTF_8, &[]);
449        assert_eq!(client.negotiate(&server, CS_UTF_8), Some(CS_UTF_8));
450    }
451
452    #[test]
453    fn negotiate_server_converts_to_client_native() {
454        // Client-native (ISO) is in the server conversion set → client-native.
455        let client = cs(0x0001_0001, &[]);
456        let server = cs(CS_UTF_8, &[0x0001_0001]);
457        assert_eq!(client.negotiate(&server, CS_UTF_8), Some(0x0001_0001));
458    }
459
460    #[test]
461    fn negotiate_common_conversion_set() {
462        // No native bridge, but UTF-8 in both conversion lists.
463        let client = cs(0x0001_0002, &[CS_UTF_8]);
464        let server = cs(0x0001_0003, &[CS_UTF_8]);
465        assert_eq!(client.negotiate(&server, 0xDEAD), Some(CS_UTF_8));
466    }
467
468    #[test]
469    fn negotiate_universal_fallback() {
470        // Disjoint sets, but both carry the fallback (UTF-16) as native/conv.
471        let client = cs(CS_UTF_16, &[]);
472        let server = cs(0x0001_0100, &[CS_UTF_16]);
473        assert_eq!(client.negotiate(&server, CS_UTF_16), Some(CS_UTF_16));
474    }
475
476    #[test]
477    fn negotiate_incompatible_yields_none() {
478        let client = cs(0x0001_0002, &[0x0001_0004]);
479        let server = cs(0x0001_0003, &[0x0001_0005]);
480        assert_eq!(client.negotiate(&server, 0xBEEF), None);
481    }
482
483    #[test]
484    fn negotiate_info_both_axes_default_fallbacks() {
485        // Disjoint natives, but UTF-8/UTF-16 universal → default pair.
486        let client = CodeSetComponentInfo {
487            for_char_data: cs(0x0001_0001, &[CS_UTF_8]),
488            for_wchar_data: cs(CS_UTF_16, &[]),
489        };
490        let server = CodeSetComponentInfo {
491            for_char_data: cs(CS_UTF_8, &[]),
492            for_wchar_data: cs(CS_UTF_16, &[]),
493        };
494        assert_eq!(client.negotiate(&server), Some((CS_UTF_8, CS_UTF_16)));
495    }
496
497    #[test]
498    fn negotiate_info_none_if_one_axis_fails() {
499        let client = CodeSetComponentInfo {
500            for_char_data: cs(CS_UTF_8, &[]),
501            for_wchar_data: cs(0x0001_0002, &[]), // incompatible
502        };
503        let server = CodeSetComponentInfo {
504            for_char_data: cs(CS_UTF_8, &[]),
505            for_wchar_data: cs(0x0001_0003, &[]),
506        };
507        assert_eq!(client.negotiate(&server), None);
508    }
509
510    #[test]
511    fn alternate_iiop_address_round_trip() {
512        let s = StructuredComponent::AlternateIiopAddress(AlternateIiopAddress {
513            host: "alt.host".into(),
514            port: 1234,
515        });
516        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
517        let decoded =
518            StructuredComponent::decode(ComponentId::AlternateIiopAddress, &bytes).unwrap();
519        assert_eq!(decoded, s);
520    }
521
522    #[test]
523    fn ssl_sec_trans_round_trip() {
524        let s = StructuredComponent::Ssl(Ssl {
525            target_supports: 0x0040,
526            target_requires: 0x0020,
527            port: 4242,
528        });
529        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
530        let decoded = StructuredComponent::decode(ComponentId::SslSecTrans, &bytes).unwrap();
531        assert_eq!(decoded, s);
532    }
533
534    #[test]
535    fn tls_sec_trans_with_addresses_round_trip() {
536        let s = StructuredComponent::TlsSecTrans(TlsSecTrans {
537            target_supports: 0x0040,
538            target_requires: 0x0040,
539            addresses: alloc::vec![
540                AlternateIiopAddress {
541                    host: "tls-a.lab".into(),
542                    port: 443,
543                },
544                AlternateIiopAddress {
545                    host: "tls-b.lab".into(),
546                    port: 8443,
547                },
548            ],
549        });
550        let bytes = s.encode_encapsulation(Endianness::Little).unwrap();
551        let decoded = StructuredComponent::decode(ComponentId::TlsSecTrans, &bytes).unwrap();
552        assert_eq!(decoded, s);
553    }
554
555    #[test]
556    fn csi_sec_mech_list_round_trip() {
557        use zerodds_corba_csiv2::{
558            AsContextSec, AssociationOptions, CompoundSecMech, CompoundSecMechList, SasContextSec,
559        };
560        let list = CompoundSecMechList {
561            stateful: true,
562            mechanism_list: alloc::vec![CompoundSecMech {
563                target_requires: AssociationOptions(
564                    AssociationOptions::INTEGRITY | AssociationOptions::CONFIDENTIALITY,
565                ),
566                transport_mech_tag: 36, // TAG_TLS_SEC_TRANS
567                transport_mech_data: alloc::vec![0x01, 0x02, 0x03],
568                as_context: AsContextSec {
569                    target_supports: AssociationOptions(0x0040),
570                    target_requires: AssociationOptions(0x0040),
571                    client_authentication_mech: alloc::vec![0xAA, 0xBB],
572                    target_name: alloc::vec![0xCC],
573                },
574                sas_context: SasContextSec {
575                    target_supports: AssociationOptions(0x0080),
576                    target_requires: AssociationOptions(0x0080),
577                    privilege_authorities: alloc::vec![alloc::vec![0xDE, 0xAD]],
578                    supported_naming_mechanisms: alloc::vec![alloc::vec![0xBE, 0xEF]],
579                    supported_identity_types: 0x0001_0203,
580                },
581            }],
582        };
583        let s = StructuredComponent::CsiSecMechList(list.clone());
584        let bytes = s.encode_encapsulation(Endianness::Little).unwrap();
585        let decoded = StructuredComponent::decode(ComponentId::CsiSecMechList, &bytes).unwrap();
586        match decoded {
587            StructuredComponent::CsiSecMechList(d) => assert_eq!(d, list),
588            other => panic!("expected CsiSecMechList, got {other:?}"),
589        }
590    }
591
592    #[test]
593    fn stream_format_version_round_trip() {
594        let s = StructuredComponent::StreamFormatVersion(StreamFormatVersion(2));
595        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
596        let decoded =
597            StructuredComponent::decode(ComponentId::RmiCustomMaxStreamFormat, &bytes).unwrap();
598        assert_eq!(decoded, s);
599    }
600
601    #[test]
602    fn java_codebase_round_trip() {
603        let s = StructuredComponent::JavaCodebase("http://server/codebase.jar".into());
604        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
605        let decoded = StructuredComponent::decode(ComponentId::JavaCodebase, &bytes).unwrap();
606        assert_eq!(decoded, s);
607    }
608
609    #[test]
610    fn opaque_unknown_tag_pass_through() {
611        let raw = alloc::vec![1, 0xff, 0xee, 0xdd];
612        let s = StructuredComponent::decode(ComponentId::Other(9999), &raw).unwrap();
613        match s {
614            StructuredComponent::Opaque { tag, bytes } => {
615                assert_eq!(tag, ComponentId::Other(9999));
616                assert_eq!(bytes, raw);
617            }
618            other => panic!("expected Opaque, got {other:?}"),
619        }
620    }
621
622    #[test]
623    fn invalid_endianness_byte_is_diagnostic() {
624        let bytes = alloc::vec![0xff, 0, 0, 0, 1];
625        let err = StructuredComponent::decode(ComponentId::OrbType, &bytes).unwrap_err();
626        assert!(matches!(err, CdrError::InvalidEndianness));
627    }
628
629    #[test]
630    fn tagged_component_round_trip() {
631        let s = StructuredComponent::OrbType(OrbType(42));
632        let bytes = s.encode_encapsulation(Endianness::Big).unwrap();
633        let tc = TaggedComponent {
634            tag: ComponentId::OrbType,
635            component_data: bytes,
636        };
637        let mut w = BufferWriter::new(Endianness::Big);
638        tc.encode(&mut w).unwrap();
639        let buf = w.into_bytes();
640        let mut r = BufferReader::new(&buf, Endianness::Big);
641        let decoded = TaggedComponent::decode(&mut r).unwrap();
642        assert_eq!(decoded, tc);
643    }
644}