Skip to main content

zerodds_xml/
typeobject_bridge.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XML → TypeObject Bridge (Cluster C4.5-b).
4//!
5//! Konvertiert das interne XML-Datenmodell aus [`xtypes_def`] in das
6//! XTypes-1.3-TypeObject-Format aus `zerodds-types`. Wire-/Hash-Kompatibel
7//! mit dem IDL-Lowering aus `zerodds-idl::semantics::to_typeobject`.
8//!
9//! # Spec-Quellen
10//!
11//! - OMG XTypes 1.3 §7.3.4.x — TypeObject + Minimal/Complete-Variants.
12//! - OMG XTypes 1.3 §7.3.4.5 — MinimalStructType / EnumType / UnionType.
13//! - OMG XTypes 1.3 Annex A — XML-Mapping.
14//!
15//! # Scope C4.5-b (diese Stufe)
16//!
17//! - Nur `MinimalTypeObject`. `CompleteTypeObject` nicht implementiert.
18//! - Top-Level-Mapping (struct, enum, union, typedef, bitmask, bitset).
19//! - Member-Typen via [`TypeRef::Primitive`] (direkt) oder
20//!   [`TypeRef::Named`] (Lookup in [`TypeLibrary`] mit Hash-Vorberechnung).
21//! - Modifier `arrayDimensions`/`sequenceMaxLength`/`stringMaxLength`
22//!   wickeln den Member-TypeIdentifier in PlainArray-/PlainSequence-/
23//!   String-Bound-Variants.
24//! - Member-IDs: `@id` aus XSD wenn gesetzt, sonst sequentiell ab 1
25//!   (AUTOID_SEQUENTIAL, Spec §7.3.1.2.1.1).
26//! - Extensibility: `final`/`appendable`/`mutable` aus
27//!   [`Extensibility`] auf `StructTypeFlag`/`UnionTypeFlag` mappen.
28//!
29//! # Bewusst nicht im Crate
30//!
31//! - `CompleteTypeObject` (kommt in Phase 6).
32//! - Forward-Declarations / Cross-Library-Refs.
33//! - Inheritance via `baseType` — der Bridge setzt zwar `base_type` als
34//!   `EquivalenceHashMinimal`, aber Cycle-Detection bleibt Aufgabe des
35//!   bestehenden [`crate::inheritance`]-Moduls.
36//! - `@autoid(HASH)` — XML-Schema kennt keine entsprechende Annotation.
37
38use alloc::collections::BTreeMap;
39use alloc::string::{String, ToString};
40use alloc::vec::Vec;
41use core::fmt;
42
43use zerodds_types::builder::{Extensibility as TypeExt, TypeObjectBuilder};
44use zerodds_types::hash::compute_minimal_hash;
45use zerodds_types::type_object::flags::{
46    CollectionElementFlag, CollectionTypeFlag, EnumLiteralFlag, EnumTypeFlag, StructTypeFlag,
47    UnionDiscriminatorFlag, UnionMemberFlag, UnionTypeFlag,
48};
49use zerodds_types::type_object::minimal::{
50    CommonCollectionElement, CommonDiscriminatorMember, CommonEnumeratedHeader,
51    CommonEnumeratedLiteral, MinimalArrayType, MinimalCollectionElement,
52    MinimalDiscriminatorMember, MinimalEnumeratedHeader, MinimalEnumeratedLiteral,
53    MinimalEnumeratedType, MinimalSequenceType, MinimalUnionMember, MinimalUnionType,
54};
55use zerodds_types::{MinimalTypeObject, PrimitiveKind, TypeIdentifier, TypeObject};
56
57use crate::xtypes_def::{
58    BitField, BitValue, BitmaskType, BitsetType, EnumLiteral, EnumType, Extensibility,
59    PrimitiveType as XmlPrimitive, StructMember, StructType, TypeDef, TypeLibrary, TypeRef,
60    TypedefType, UnionDiscriminator, UnionType,
61};
62
63/// Fehler beim XML→TypeObject-Mapping.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum BridgeError {
66    /// Ein XSD-Konstrukt ist (in C4.5-b) noch nicht abbildbar — etwa
67    /// inline-Maps oder Self-Recursion.
68    UnsupportedXsdConstruct(String),
69    /// Eine `TypeRef::Named`-Referenz konnte nicht aufgeloest werden,
70    /// weil sie nicht in der mitgelieferten [`TypeLibrary`] (oder via
71    /// `bridge_with_resolver`) auftaucht.
72    UnresolvedReference(String),
73    /// Ein numerischer Wert (z.B. ein Union-Discriminator-Label) ist
74    /// nicht parsebar.
75    InvalidLiteral(String),
76    /// Die EquivalenceHash-Berechnung ist fehlgeschlagen (Buffer-Overflow
77    /// im internen Encoder).
78    HashFailed(String),
79    /// Modul-Eintraege sind kein Top-Level-Type — das XML hat ein
80    /// `<module>` an einer Stelle, an der nur ein Type stehen darf.
81    ModuleAtTopLevel(String),
82}
83
84impl fmt::Display for BridgeError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::UnsupportedXsdConstruct(s) => {
88                write!(f, "unsupported XSD construct: {s}")
89            }
90            Self::UnresolvedReference(s) => write!(f, "unresolved reference: {s}"),
91            Self::InvalidLiteral(s) => write!(f, "invalid literal: {s}"),
92            Self::HashFailed(s) => write!(f, "TypeIdentifier hashing failed: {s}"),
93            Self::ModuleAtTopLevel(s) => {
94                write!(f, "<module> not allowed at this position: {s}")
95            }
96        }
97    }
98}
99
100#[cfg(feature = "std")]
101impl std::error::Error for BridgeError {}
102
103// ============================================================================
104// Public API
105// ============================================================================
106
107/// Konvertiert ein einzelnes XmlType-[`TypeDef`] in einen [`TypeObject`]
108/// (Minimal-Variant).
109///
110/// Named-Member-Refs werden als Null-Hash-Placeholder
111/// (`TypeIdentifier::EquivalenceHashMinimal([0; 14])`) abgelegt — der
112/// Caller, der eine ganze Library auf einmal aufloesen will, sollte
113/// stattdessen [`bridge_library`] benutzen.
114///
115/// # Errors
116/// `BridgeError::UnsupportedXsdConstruct` fuer Konstrukte, die in dieser
117/// Stufe nicht abgebildet werden; `BridgeError::ModuleAtTopLevel` wenn
118/// ein `<module>` direkt uebergeben wurde (Module sind keine Types).
119pub fn xml_type_to_typeobject(xml: &TypeDef) -> Result<TypeObject, BridgeError> {
120    let mto = xml_type_to_minimal_typeobject(xml)?;
121    Ok(TypeObject::Minimal(mto))
122}
123
124/// Wie [`xml_type_to_typeobject`], aber direkt als
125/// [`MinimalTypeObject`] (ohne Discriminator-Wrapper).
126///
127/// # Errors
128/// Siehe [`xml_type_to_typeobject`].
129pub fn xml_type_to_minimal_typeobject(xml: &TypeDef) -> Result<MinimalTypeObject, BridgeError> {
130    let resolver = NullResolver;
131    bridge_typedef(xml, &resolver)
132}
133
134/// Konvertiert eine ganze [`TypeLibrary`] in eine Map
135/// `Name → MinimalTypeObject`. Named-Refs zwischen Types innerhalb der
136/// Library werden in einem zweistufigen Pass aufgeloest:
137///
138/// 1. Pre-Pass: jeder Top-Level-Type wird mit Null-Hash-Placeholdern
139///    abgebildet, dann sein EquivalenceHashMinimal berechnet.
140/// 2. Final-Pass: alle TypeIdentifier-Refs auf Named-Types werden mit
141///    den Pre-Pass-Hashes ersetzt.
142///
143/// `<module>`-Eintraege werden rekursiv abgeflacht. Der Map-Key fuer
144/// nested types ist der scoped Name (`Module::Inner`).
145///
146/// # Errors
147/// `BridgeError` fuer alle Fehler aus den einzelnen Type-Mappings.
148pub fn bridge_library(
149    lib: &TypeLibrary,
150) -> Result<BTreeMap<String, MinimalTypeObject>, BridgeError> {
151    let flat = flatten(&lib.types, "");
152
153    // Pre-Pass: alle Types mit Null-Hash bauen, dann EquivalenceHashMinimal.
154    let pre_resolver = NullResolver;
155    let mut pre: BTreeMap<String, MinimalTypeObject> = BTreeMap::new();
156    let mut pre_hashes: BTreeMap<String, TypeIdentifier> = BTreeMap::new();
157    for (scoped, td) in &flat {
158        let mto = bridge_typedef(td, &pre_resolver)?;
159        let h = compute_minimal_hash(&mto)
160            .map_err(|e| BridgeError::HashFailed(alloc::format!("{e:?}")))?;
161        pre_hashes.insert(scoped.clone(), TypeIdentifier::EquivalenceHashMinimal(h));
162        pre.insert(scoped.clone(), mto);
163    }
164
165    // Final-Pass: alle Named-Refs ueber pre_hashes ersetzen.
166    let resolver = MapResolver { named: &pre_hashes };
167    let mut out: BTreeMap<String, MinimalTypeObject> = BTreeMap::new();
168    for (scoped, td) in &flat {
169        let mto = bridge_typedef(td, &resolver)?;
170        out.insert(scoped.clone(), mto);
171    }
172    Ok(out)
173}
174
175// ============================================================================
176// Resolver — Strategie fuer Named-References
177// ============================================================================
178
179/// Strategie um `TypeRef::Named(...)` auf einen [`TypeIdentifier`]
180/// abzubilden. Implementierungen bestimmen, wie unaufloesbare Refs
181/// behandelt werden (Null-Hash vs. Fehler).
182trait NameResolver {
183    fn resolve(&self, name: &str) -> TypeIdentifier;
184}
185
186/// Liefert immer `EquivalenceHashMinimal([0; 14])`. Wird im Pre-Pass
187/// und bei Single-Type-Bridge benutzt, wenn keine Library bekannt ist.
188struct NullResolver;
189
190impl NameResolver for NullResolver {
191    fn resolve(&self, _name: &str) -> TypeIdentifier {
192        TypeIdentifier::EquivalenceHashMinimal(zerodds_types::EquivalenceHash([0; 14]))
193    }
194}
195
196/// Liefert die in `named` registrierten Hashes; Fallback Null-Hash.
197struct MapResolver<'a> {
198    named: &'a BTreeMap<String, TypeIdentifier>,
199}
200
201impl NameResolver for MapResolver<'_> {
202    fn resolve(&self, name: &str) -> TypeIdentifier {
203        self.named
204            .get(name)
205            .cloned()
206            .unwrap_or(TypeIdentifier::EquivalenceHashMinimal(
207                zerodds_types::EquivalenceHash([0; 14]),
208            ))
209    }
210}
211
212// ============================================================================
213// Flattening
214// ============================================================================
215
216/// zerodds-lint: recursion-depth 32
217///
218/// XML-Module-Hierarchien sind selten tiefer als ~8 Ebenen
219/// (`org::omg::dds::core::policy`-Stil). Cap auf 32 deckt auch
220/// pathologisch verschachtelte Test-Fixtures.
221fn flatten<'a>(types: &'a [TypeDef], prefix: &str) -> Vec<(String, &'a TypeDef)> {
222    let mut out: Vec<(String, &TypeDef)> = Vec::new();
223    for t in types {
224        match t {
225            TypeDef::Module(m) => {
226                let new_prefix = if prefix.is_empty() {
227                    m.name.clone()
228                } else {
229                    alloc::format!("{prefix}::{}", m.name)
230                };
231                out.extend(flatten(&m.types, &new_prefix));
232            }
233            other => {
234                let scoped = if prefix.is_empty() {
235                    other.name().to_string()
236                } else {
237                    alloc::format!("{prefix}::{}", other.name())
238                };
239                out.push((scoped, other));
240            }
241        }
242    }
243    out
244}
245
246// ============================================================================
247// Top-Level Dispatcher
248// ============================================================================
249
250fn bridge_typedef<R: NameResolver>(
251    xml: &TypeDef,
252    res: &R,
253) -> Result<MinimalTypeObject, BridgeError> {
254    match xml {
255        TypeDef::Struct(s) => bridge_struct(s, res),
256        TypeDef::Enum(e) => Ok(MinimalTypeObject::Enumerated(bridge_enum(e))),
257        TypeDef::Union(u) => bridge_union(u, res),
258        TypeDef::Typedef(t) => bridge_typedef_alias(t, res),
259        TypeDef::Bitmask(b) => Ok(MinimalTypeObject::Bitmask(bridge_bitmask(b))),
260        TypeDef::Bitset(b) => bridge_bitset(b),
261        TypeDef::Module(m) => Err(BridgeError::ModuleAtTopLevel(m.name.clone())),
262        TypeDef::Include(_) | TypeDef::ForwardDcl(_) | TypeDef::Const(_) => {
263            Err(BridgeError::UnsupportedXsdConstruct(alloc::format!(
264                "non-bridgeable XML element kind: {}",
265                xml.name()
266            )))
267        }
268    }
269}
270
271// ============================================================================
272// Struct
273// ============================================================================
274
275fn bridge_struct<R: NameResolver>(
276    s: &StructType,
277    res: &R,
278) -> Result<MinimalTypeObject, BridgeError> {
279    let ext = map_extensibility(s.extensibility.unwrap_or_default());
280    let mut builder = TypeObjectBuilder::struct_type(s.name.clone()).extensibility(ext);
281
282    if let Some(base) = &s.base_type {
283        builder = builder.base(res.resolve(base));
284    }
285
286    for m in &s.members {
287        let ti = wrap_member_type(m, res)?;
288        let key = m.key;
289        let optional = m.optional;
290        let must_understand = m.must_understand;
291        let explicit_id = m.id;
292        let name = m.name.clone();
293        builder = builder.member(name, ti, |mut mb| {
294            if key {
295                mb = mb.key();
296            }
297            if optional {
298                mb = mb.optional();
299            }
300            if must_understand {
301                mb = mb.must_understand();
302            }
303            if let Some(id) = explicit_id {
304                mb = mb.id(id);
305            }
306            mb
307        });
308    }
309    Ok(MinimalTypeObject::Struct(builder.build_minimal()))
310}
311
312fn map_extensibility(e: Extensibility) -> TypeExt {
313    match e {
314        Extensibility::Final => TypeExt::Final,
315        Extensibility::Appendable => TypeExt::Appendable,
316        Extensibility::Mutable => TypeExt::Mutable,
317    }
318}
319
320// ============================================================================
321// Enum
322// ============================================================================
323
324fn bridge_enum(e: &EnumType) -> MinimalEnumeratedType {
325    let bit_bound: u16 = e.bit_bound.unwrap_or(32).min(64) as u16;
326    // Auto-Numbering: Spec §7.3.1.2.4.1 — wenn `value` fehlt, wird
327    // `prev + 1` verwendet, beginnend bei 0.
328    let mut prev: i32 = -1;
329    let literal_seq: Vec<MinimalEnumeratedLiteral> = e
330        .enumerators
331        .iter()
332        .map(|l: &EnumLiteral| {
333            let value = l.value.unwrap_or(prev.saturating_add(1));
334            prev = value;
335            MinimalEnumeratedLiteral {
336                common: CommonEnumeratedLiteral {
337                    value,
338                    flags: EnumLiteralFlag::default(),
339                },
340                detail: zerodds_types::type_object::common::NameHash::from_name(&l.name),
341            }
342        })
343        .collect();
344    MinimalEnumeratedType {
345        enum_flags: EnumTypeFlag::default(),
346        header: MinimalEnumeratedHeader {
347            common: CommonEnumeratedHeader { bit_bound },
348        },
349        literal_seq,
350    }
351}
352
353// ============================================================================
354// Union
355// ============================================================================
356
357fn bridge_union<R: NameResolver>(u: &UnionType, res: &R) -> Result<MinimalTypeObject, BridgeError> {
358    let disc_ti = type_ref_to_identifier(&u.discriminator, res);
359    let mut next_seq: u32 = 1;
360    let mut member_seq: Vec<MinimalUnionMember> = Vec::with_capacity(u.cases.len());
361    for case in &u.cases {
362        let m = &case.member;
363        let mut labels: Vec<i32> = Vec::with_capacity(case.discriminators.len());
364        let mut is_default = false;
365        for d in &case.discriminators {
366            match d {
367                UnionDiscriminator::Default => {
368                    is_default = true;
369                }
370                UnionDiscriminator::Value(s) => {
371                    let v = parse_label(s)?;
372                    labels.push(v);
373                }
374            }
375        }
376        let id = m.id.unwrap_or_else(|| {
377            let v = next_seq;
378            next_seq += 1;
379            v
380        });
381        let ti = wrap_member_type(m, res)?;
382        let mut flags: u16 = 0;
383        if is_default {
384            flags |= UnionMemberFlag::IS_DEFAULT;
385        }
386        member_seq.push(MinimalUnionMember {
387            common: zerodds_types::type_object::common::CommonUnionMember {
388                member_id: id,
389                member_flags: UnionMemberFlag(flags),
390                type_id: ti,
391                label_seq: labels,
392            },
393            detail: zerodds_types::type_object::common::NameHash::from_name(&m.name),
394        });
395    }
396    // XML-Union hat aktuell keinen Extensibility-Marker — Default
397    // Appendable (Spec §7.2.2.4) → IS_APPENDABLE. Reuse die Struct-
398    // Flag-Bitpositionen, die der Builder fuer Union-Flags spiegelt.
399    let _ = &u.name;
400    let union_flags_bits = StructTypeFlag::IS_APPENDABLE;
401    Ok(MinimalTypeObject::Union(MinimalUnionType {
402        union_flags: UnionTypeFlag(union_flags_bits),
403        discriminator: MinimalDiscriminatorMember {
404            common: CommonDiscriminatorMember {
405                member_flags: UnionDiscriminatorFlag::default(),
406                type_id: disc_ti,
407            },
408        },
409        member_seq,
410    }))
411}
412
413fn parse_label(s: &str) -> Result<i32, BridgeError> {
414    let trimmed = s.trim();
415    if let Some(rest) = trimmed
416        .strip_prefix("0x")
417        .or_else(|| trimmed.strip_prefix("0X"))
418    {
419        return i64::from_str_radix(rest, 16)
420            .ok()
421            .and_then(|v| i32::try_from(v).ok())
422            .ok_or_else(|| BridgeError::InvalidLiteral(s.to_string()));
423    }
424    trimmed
425        .parse::<i32>()
426        .map_err(|_| BridgeError::InvalidLiteral(s.to_string()))
427}
428
429// ============================================================================
430// Typedef → Alias / PlainSequence / PlainArray
431// ============================================================================
432
433fn bridge_typedef_alias<R: NameResolver>(
434    t: &TypedefType,
435    res: &R,
436) -> Result<MinimalTypeObject, BridgeError> {
437    // Wenn arrayDimensions gesetzt ist, ist das Typedef ein
438    // MinimalArrayType. Wenn sequenceMaxLength gesetzt ist, ist es ein
439    // MinimalSequenceType. Sonst ein MinimalAliasType.
440    if !t.array_dimensions.is_empty() {
441        let element =
442            type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
443        return Ok(MinimalTypeObject::Array(MinimalArrayType {
444            collection_flag: CollectionTypeFlag::default(),
445            bound_seq: t.array_dimensions.clone(),
446            element: MinimalCollectionElement {
447                common: CommonCollectionElement {
448                    element_flags: CollectionElementFlag::default(),
449                    type_id: element,
450                },
451            },
452        }));
453    }
454    if let Some(bound) = t.sequence_max_length {
455        let element =
456            type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
457        return Ok(MinimalTypeObject::Sequence(MinimalSequenceType {
458            collection_flag: CollectionTypeFlag::default(),
459            bound,
460            element: MinimalCollectionElement {
461                common: CommonCollectionElement {
462                    element_flags: CollectionElementFlag::default(),
463                    type_id: element,
464                },
465            },
466        }));
467    }
468    let related = type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
469    let alias = TypeObjectBuilder::alias(t.name.clone(), related).build_minimal();
470    Ok(MinimalTypeObject::Alias(alias))
471}
472
473// ============================================================================
474// Bitmask
475// ============================================================================
476
477fn bridge_bitmask(b: &BitmaskType) -> zerodds_types::type_object::minimal::MinimalBitmaskType {
478    let bit_bound: u16 = b.bit_bound.unwrap_or(32).min(64) as u16;
479    let mut prev: i32 = -1;
480    let mut builder = TypeObjectBuilder::bitmask(b.name.clone()).bit_bound(bit_bound);
481    for v in &b.bit_values {
482        let pos = match v.position {
483            Some(p) => {
484                prev = p as i32;
485                p
486            }
487            None => {
488                let p = (prev.saturating_add(1)).max(0) as u32;
489                prev = p as i32;
490                p
491            }
492        };
493        let p_u16 = pos as u16;
494        let _ = v as &BitValue;
495        builder = builder.flag(v.name.clone(), p_u16);
496    }
497    builder.build_minimal()
498}
499
500// ============================================================================
501// Bitset
502// ============================================================================
503
504fn bridge_bitset(b: &BitsetType) -> Result<MinimalTypeObject, BridgeError> {
505    use zerodds_types::type_identifier::kinds::{
506        TK_INT8, TK_INT16, TK_INT32, TK_INT64, TK_UINT8, TK_UINT16, TK_UINT32, TK_UINT64,
507    };
508    // Bit-Position laeuft kumulativ: jedes Feld setzt prev + bitcount.
509    let mut next_pos: u16 = 0;
510    let mut builder = TypeObjectBuilder::bitset(b.name.clone());
511    for f in &b.bit_fields {
512        let bitcount = parse_bitset_mask_bits(&f.mask)?;
513        let holder = match &f.type_ref {
514            TypeRef::Primitive(p) => holder_kind_byte(*p).ok_or_else(|| {
515                BridgeError::UnsupportedXsdConstruct(alloc::format!(
516                    "bitset holder type: {}",
517                    p.as_xml()
518                ))
519            })?,
520            TypeRef::Named(_) => {
521                return Err(BridgeError::UnsupportedXsdConstruct(
522                    "bitset holder must be a primitive integer".to_string(),
523                ));
524            }
525        };
526        // Defensive: dass die TK_-Konstanten existieren.
527        debug_assert!(matches!(
528            holder,
529            TK_INT8 | TK_INT16 | TK_INT32 | TK_INT64 | TK_UINT8 | TK_UINT16 | TK_UINT32 | TK_UINT64
530        ));
531        let pos = next_pos;
532        next_pos = next_pos.saturating_add(u16::from(bitcount));
533        builder = builder.field(f.name.clone(), pos, bitcount, holder);
534        let _ = f as &BitField;
535    }
536    Ok(MinimalTypeObject::Bitset(builder.build_minimal()))
537}
538
539fn holder_kind_byte(p: XmlPrimitive) -> Option<u8> {
540    use zerodds_types::type_identifier::kinds::*;
541    Some(match p {
542        XmlPrimitive::Octet => TK_BYTE,
543        XmlPrimitive::Short => TK_INT16,
544        XmlPrimitive::UShort => TK_UINT16,
545        XmlPrimitive::Long => TK_INT32,
546        XmlPrimitive::ULong => TK_UINT32,
547        XmlPrimitive::LongLong => TK_INT64,
548        XmlPrimitive::ULongLong => TK_UINT64,
549        XmlPrimitive::Boolean => TK_BOOLEAN,
550        XmlPrimitive::Char => TK_CHAR8,
551        XmlPrimitive::WChar => TK_CHAR16,
552        _ => return None,
553    })
554}
555
556/// Parst eine Bitmask wie `0x0F` zu der Anzahl gesetzter Bits.
557fn parse_bitset_mask_bits(mask: &str) -> Result<u8, BridgeError> {
558    let trimmed = mask.trim();
559    let (rest, radix) = if let Some(r) = trimmed
560        .strip_prefix("0x")
561        .or_else(|| trimmed.strip_prefix("0X"))
562    {
563        (r, 16u32)
564    } else if let Some(r) = trimmed
565        .strip_prefix("0b")
566        .or_else(|| trimmed.strip_prefix("0B"))
567    {
568        (r, 2u32)
569    } else {
570        (trimmed, 10u32)
571    };
572    let v = u64::from_str_radix(rest, radix)
573        .map_err(|_| BridgeError::InvalidLiteral(alloc::format!("bitset mask {mask}")))?;
574    let bits = u8::try_from(v.count_ones())
575        .map_err(|_| BridgeError::InvalidLiteral(alloc::format!("bitset mask too wide: {mask}")))?;
576    if bits == 0 {
577        return Err(BridgeError::InvalidLiteral(alloc::format!(
578            "bitset mask must have >= 1 bit: {mask}"
579        )));
580    }
581    Ok(bits)
582}
583
584// ============================================================================
585// TypeRef → TypeIdentifier
586// ============================================================================
587
588/// Wickelt einen Struct-Member in seinen finalen TypeIdentifier — inkl.
589/// `arrayDimensions`, `sequenceMaxLength` und `stringMaxLength`.
590fn wrap_member_type<R: NameResolver>(
591    m: &StructMember,
592    res: &R,
593) -> Result<TypeIdentifier, BridgeError> {
594    let inner = type_ref_to_identifier_with_string_bound(&m.type_ref, m.string_max_length, res);
595
596    // arrayDimensions hat Vorrang vor sequenceMaxLength.
597    if !m.array_dimensions.is_empty() {
598        return Ok(wrap_array(inner, &m.array_dimensions));
599    }
600    if let Some(bound) = m.sequence_max_length {
601        return Ok(wrap_sequence(inner, bound));
602    }
603    Ok(inner)
604}
605
606fn wrap_sequence(element: TypeIdentifier, bound: u32) -> TypeIdentifier {
607    if bound <= 255 {
608        TypeIdentifier::PlainSequenceSmall {
609            header: zerodds_types::PlainCollectionHeader::default(),
610            bound: bound as u8,
611            element: alloc::boxed::Box::new(element),
612        }
613    } else {
614        TypeIdentifier::PlainSequenceLarge {
615            header: zerodds_types::PlainCollectionHeader::default(),
616            bound,
617            element: alloc::boxed::Box::new(element),
618        }
619    }
620}
621
622fn wrap_array(element: TypeIdentifier, dims: &[u32]) -> TypeIdentifier {
623    let any_large = dims.iter().any(|d| *d > 255);
624    if any_large {
625        TypeIdentifier::PlainArrayLarge {
626            header: zerodds_types::PlainCollectionHeader::default(),
627            array_bounds: dims.to_vec(),
628            element: alloc::boxed::Box::new(element),
629        }
630    } else {
631        TypeIdentifier::PlainArraySmall {
632            header: zerodds_types::PlainCollectionHeader::default(),
633            array_bounds: dims.iter().map(|d| *d as u8).collect(),
634            element: alloc::boxed::Box::new(element),
635        }
636    }
637}
638
639fn type_ref_to_identifier<R: NameResolver>(t: &TypeRef, res: &R) -> TypeIdentifier {
640    type_ref_to_identifier_with_string_bound(t, None, res)
641}
642
643fn type_ref_to_identifier_with_string_bound<R: NameResolver>(
644    t: &TypeRef,
645    string_max_length: Option<u32>,
646    res: &R,
647) -> TypeIdentifier {
648    match t {
649        TypeRef::Primitive(p) => primitive_to_identifier(*p, string_max_length),
650        TypeRef::Named(n) => res.resolve(n),
651    }
652}
653
654fn primitive_to_identifier(p: XmlPrimitive, bound: Option<u32>) -> TypeIdentifier {
655    match p {
656        XmlPrimitive::Boolean => TypeIdentifier::Primitive(PrimitiveKind::Boolean),
657        XmlPrimitive::Octet => TypeIdentifier::Primitive(PrimitiveKind::Byte),
658        XmlPrimitive::Char => TypeIdentifier::Primitive(PrimitiveKind::Char8),
659        XmlPrimitive::WChar => TypeIdentifier::Primitive(PrimitiveKind::Char16),
660        XmlPrimitive::Short => TypeIdentifier::Primitive(PrimitiveKind::Int16),
661        XmlPrimitive::UShort => TypeIdentifier::Primitive(PrimitiveKind::UInt16),
662        XmlPrimitive::Long => TypeIdentifier::Primitive(PrimitiveKind::Int32),
663        XmlPrimitive::ULong => TypeIdentifier::Primitive(PrimitiveKind::UInt32),
664        XmlPrimitive::LongLong => TypeIdentifier::Primitive(PrimitiveKind::Int64),
665        XmlPrimitive::ULongLong => TypeIdentifier::Primitive(PrimitiveKind::UInt64),
666        XmlPrimitive::Float => TypeIdentifier::Primitive(PrimitiveKind::Float32),
667        XmlPrimitive::Double => TypeIdentifier::Primitive(PrimitiveKind::Float64),
668        XmlPrimitive::LongDouble => TypeIdentifier::Primitive(PrimitiveKind::Float128),
669        XmlPrimitive::String => string_id(false, bound.unwrap_or(0)),
670        XmlPrimitive::WString => string_id(true, bound.unwrap_or(0)),
671    }
672}
673
674fn string_id(wide: bool, bound: u32) -> TypeIdentifier {
675    if wide {
676        if bound <= 255 {
677            TypeIdentifier::String16Small { bound: bound as u8 }
678        } else {
679            TypeIdentifier::String16Large { bound }
680        }
681    } else if bound <= 255 {
682        TypeIdentifier::String8Small { bound: bound as u8 }
683    } else {
684        TypeIdentifier::String8Large { bound }
685    }
686}
687
688// ============================================================================
689// Tests
690// ============================================================================
691
692#[cfg(test)]
693#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
694mod tests {
695    use super::*;
696    use crate::xtypes_def::{
697        BitField, BitValue, BitmaskType, BitsetType, EnumLiteral, EnumType, ModuleEntry,
698        StructMember, StructType, TypedefType, UnionCase, UnionType,
699    };
700
701    fn primitive(p: XmlPrimitive) -> TypeRef {
702        TypeRef::Primitive(p)
703    }
704
705    fn make_struct() -> StructType {
706        StructType {
707            name: "Position".into(),
708            extensibility: Some(Extensibility::Final),
709            base_type: None,
710            members: alloc::vec![
711                StructMember {
712                    name: "x".into(),
713                    type_ref: primitive(XmlPrimitive::Float),
714                    key: true,
715                    ..Default::default()
716                },
717                StructMember {
718                    name: "y".into(),
719                    type_ref: primitive(XmlPrimitive::Float),
720                    ..Default::default()
721                },
722            ],
723        }
724    }
725
726    // ---- Struct ----------------------------------------------------------
727
728    #[test]
729    fn struct_two_members_lower_to_minimal_struct() {
730        let s = make_struct();
731        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
732        match mto {
733            MinimalTypeObject::Struct(st) => {
734                assert_eq!(st.member_seq.len(), 2);
735                assert!(st.struct_flags.has(StructTypeFlag::IS_FINAL));
736                assert!(matches!(
737                    st.member_seq[0].common.member_type_id,
738                    TypeIdentifier::Primitive(PrimitiveKind::Float32)
739                ));
740            }
741            other => panic!("expected struct, got {other:?}"),
742        }
743    }
744
745    #[test]
746    fn struct_explicit_id_is_preserved() {
747        let s = StructType {
748            name: "X".into(),
749            extensibility: None,
750            base_type: None,
751            members: alloc::vec![StructMember {
752                name: "a".into(),
753                type_ref: primitive(XmlPrimitive::Long),
754                id: Some(42),
755                ..Default::default()
756            }],
757        };
758        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
759        if let MinimalTypeObject::Struct(st) = mto {
760            assert_eq!(st.member_seq[0].common.member_id, 42);
761        } else {
762            panic!("not a struct");
763        }
764    }
765
766    #[test]
767    fn struct_autoid_sequential_starts_at_1() {
768        let s = StructType {
769            name: "X".into(),
770            extensibility: None,
771            base_type: None,
772            members: alloc::vec![
773                StructMember {
774                    name: "a".into(),
775                    type_ref: primitive(XmlPrimitive::Long),
776                    ..Default::default()
777                },
778                StructMember {
779                    name: "b".into(),
780                    type_ref: primitive(XmlPrimitive::Long),
781                    ..Default::default()
782                },
783            ],
784        };
785        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
786        if let MinimalTypeObject::Struct(st) = mto {
787            assert_eq!(st.member_seq[0].common.member_id, 1);
788            assert_eq!(st.member_seq[1].common.member_id, 2);
789        } else {
790            panic!("not a struct");
791        }
792    }
793
794    #[test]
795    fn struct_member_flags_key_optional_must_understand() {
796        use zerodds_types::type_object::flags::StructMemberFlag;
797        let s = StructType {
798            name: "X".into(),
799            extensibility: None,
800            base_type: None,
801            members: alloc::vec![StructMember {
802                name: "a".into(),
803                type_ref: primitive(XmlPrimitive::Long),
804                key: true,
805                optional: true,
806                must_understand: true,
807                ..Default::default()
808            }],
809        };
810        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
811        if let MinimalTypeObject::Struct(st) = mto {
812            let f = st.member_seq[0].common.member_flags;
813            assert!(f.has(StructMemberFlag::IS_KEY));
814            assert!(f.has(StructMemberFlag::IS_OPTIONAL));
815            assert!(f.has(StructMemberFlag::IS_MUST_UNDERSTAND));
816        } else {
817            panic!("not a struct");
818        }
819    }
820
821    #[test]
822    fn struct_string_member_with_bound_uses_string_small() {
823        let s = StructType {
824            name: "X".into(),
825            extensibility: None,
826            base_type: None,
827            members: alloc::vec![StructMember {
828                name: "name".into(),
829                type_ref: primitive(XmlPrimitive::String),
830                string_max_length: Some(64),
831                ..Default::default()
832            }],
833        };
834        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
835        if let MinimalTypeObject::Struct(st) = mto {
836            assert_eq!(
837                st.member_seq[0].common.member_type_id,
838                TypeIdentifier::String8Small { bound: 64 }
839            );
840        } else {
841            panic!();
842        }
843    }
844
845    #[test]
846    fn struct_member_with_array_dimensions_wraps_into_plain_array() {
847        let s = StructType {
848            name: "X".into(),
849            extensibility: None,
850            base_type: None,
851            members: alloc::vec![StructMember {
852                name: "a".into(),
853                type_ref: primitive(XmlPrimitive::Long),
854                array_dimensions: alloc::vec![3, 4],
855                ..Default::default()
856            }],
857        };
858        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
859        if let MinimalTypeObject::Struct(st) = mto {
860            assert!(matches!(
861                st.member_seq[0].common.member_type_id,
862                TypeIdentifier::PlainArraySmall { .. }
863            ));
864        } else {
865            panic!();
866        }
867    }
868
869    #[test]
870    fn struct_member_with_sequence_bound_wraps_into_plain_sequence() {
871        let s = StructType {
872            name: "X".into(),
873            extensibility: None,
874            base_type: None,
875            members: alloc::vec![StructMember {
876                name: "a".into(),
877                type_ref: primitive(XmlPrimitive::Long),
878                sequence_max_length: Some(100),
879                ..Default::default()
880            }],
881        };
882        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
883        if let MinimalTypeObject::Struct(st) = mto {
884            assert!(matches!(
885                st.member_seq[0].common.member_type_id,
886                TypeIdentifier::PlainSequenceSmall { bound: 100, .. }
887            ));
888        } else {
889            panic!();
890        }
891    }
892
893    #[test]
894    fn struct_member_with_large_array_uses_plain_array_large() {
895        let s = StructType {
896            name: "X".into(),
897            extensibility: None,
898            base_type: None,
899            members: alloc::vec![StructMember {
900                name: "a".into(),
901                type_ref: primitive(XmlPrimitive::Long),
902                array_dimensions: alloc::vec![300, 4],
903                ..Default::default()
904            }],
905        };
906        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
907        if let MinimalTypeObject::Struct(st) = mto {
908            assert!(matches!(
909                st.member_seq[0].common.member_type_id,
910                TypeIdentifier::PlainArrayLarge { .. }
911            ));
912        } else {
913            panic!();
914        }
915    }
916
917    #[test]
918    fn struct_extensibility_mutable_sets_flag() {
919        let mut s = make_struct();
920        s.extensibility = Some(Extensibility::Mutable);
921        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
922        if let MinimalTypeObject::Struct(st) = mto {
923            assert!(st.struct_flags.has(StructTypeFlag::IS_MUTABLE));
924        } else {
925            panic!();
926        }
927    }
928
929    // ---- Enum ------------------------------------------------------------
930
931    #[test]
932    fn enum_with_explicit_values() {
933        let e = EnumType {
934            name: "Color".into(),
935            bit_bound: Some(16),
936            enumerators: alloc::vec![
937                EnumLiteral {
938                    name: "RED".into(),
939                    value: Some(0),
940                },
941                EnumLiteral {
942                    name: "GREEN".into(),
943                    value: Some(1),
944                },
945                EnumLiteral {
946                    name: "BLUE".into(),
947                    value: Some(2),
948                },
949            ],
950        };
951        let mto = xml_type_to_minimal_typeobject(&TypeDef::Enum(e)).unwrap();
952        if let MinimalTypeObject::Enumerated(en) = mto {
953            assert_eq!(en.header.common.bit_bound, 16);
954            assert_eq!(en.literal_seq.len(), 3);
955            assert_eq!(en.literal_seq[2].common.value, 2);
956        } else {
957            panic!();
958        }
959    }
960
961    #[test]
962    fn enum_auto_numbering_starts_at_zero() {
963        let e = EnumType {
964            name: "X".into(),
965            bit_bound: None,
966            enumerators: alloc::vec![
967                EnumLiteral {
968                    name: "A".into(),
969                    value: None,
970                },
971                EnumLiteral {
972                    name: "B".into(),
973                    value: None,
974                },
975                EnumLiteral {
976                    name: "C".into(),
977                    value: Some(10),
978                },
979                EnumLiteral {
980                    name: "D".into(),
981                    value: None,
982                },
983            ],
984        };
985        let mto = xml_type_to_minimal_typeobject(&TypeDef::Enum(e)).unwrap();
986        if let MinimalTypeObject::Enumerated(en) = mto {
987            assert_eq!(en.literal_seq[0].common.value, 0);
988            assert_eq!(en.literal_seq[1].common.value, 1);
989            assert_eq!(en.literal_seq[2].common.value, 10);
990            assert_eq!(en.literal_seq[3].common.value, 11);
991            // Default bit_bound = 32.
992            assert_eq!(en.header.common.bit_bound, 32);
993        } else {
994            panic!();
995        }
996    }
997
998    // ---- Union -----------------------------------------------------------
999
1000    #[test]
1001    fn union_with_int_disc_and_default_branch() {
1002        let u = UnionType {
1003            name: "U".into(),
1004            discriminator: primitive(XmlPrimitive::Long),
1005            cases: alloc::vec![
1006                UnionCase {
1007                    discriminators: alloc::vec![
1008                        UnionDiscriminator::Value("1".into()),
1009                        UnionDiscriminator::Value("2".into()),
1010                    ],
1011                    member: StructMember {
1012                        name: "a".into(),
1013                        type_ref: primitive(XmlPrimitive::LongLong),
1014                        ..Default::default()
1015                    },
1016                },
1017                UnionCase {
1018                    discriminators: alloc::vec![UnionDiscriminator::Default],
1019                    member: StructMember {
1020                        name: "b".into(),
1021                        type_ref: primitive(XmlPrimitive::String),
1022                        string_max_length: Some(64),
1023                        ..Default::default()
1024                    },
1025                },
1026            ],
1027        };
1028        let mto = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap();
1029        if let MinimalTypeObject::Union(un) = mto {
1030            assert_eq!(un.member_seq.len(), 2);
1031            assert_eq!(un.member_seq[0].common.label_seq, alloc::vec![1_i32, 2]);
1032            assert!(un.member_seq[1].common.member_flags.0 & UnionMemberFlag::IS_DEFAULT != 0);
1033        } else {
1034            panic!();
1035        }
1036    }
1037
1038    #[test]
1039    fn union_label_hex_parsed() {
1040        let u = UnionType {
1041            name: "U".into(),
1042            discriminator: primitive(XmlPrimitive::Long),
1043            cases: alloc::vec![UnionCase {
1044                discriminators: alloc::vec![UnionDiscriminator::Value("0x10".into())],
1045                member: StructMember {
1046                    name: "a".into(),
1047                    type_ref: primitive(XmlPrimitive::Long),
1048                    ..Default::default()
1049                },
1050            }],
1051        };
1052        let mto = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap();
1053        if let MinimalTypeObject::Union(un) = mto {
1054            assert_eq!(un.member_seq[0].common.label_seq, alloc::vec![16_i32]);
1055        } else {
1056            panic!();
1057        }
1058    }
1059
1060    #[test]
1061    fn union_label_invalid_returns_invalid_literal() {
1062        let u = UnionType {
1063            name: "U".into(),
1064            discriminator: primitive(XmlPrimitive::Long),
1065            cases: alloc::vec![UnionCase {
1066                discriminators: alloc::vec![UnionDiscriminator::Value("not-an-int".into())],
1067                member: StructMember {
1068                    name: "a".into(),
1069                    type_ref: primitive(XmlPrimitive::Long),
1070                    ..Default::default()
1071                },
1072            }],
1073        };
1074        let err = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap_err();
1075        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1076    }
1077
1078    // ---- Typedef ---------------------------------------------------------
1079
1080    #[test]
1081    fn typedef_to_alias() {
1082        let t = TypedefType {
1083            name: "Count".into(),
1084            type_ref: primitive(XmlPrimitive::ULongLong),
1085            ..Default::default()
1086        };
1087        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1088        if let MinimalTypeObject::Alias(a) = mto {
1089            assert_eq!(
1090                a.body.common.related_type,
1091                TypeIdentifier::Primitive(PrimitiveKind::UInt64)
1092            );
1093        } else {
1094            panic!("not an alias");
1095        }
1096    }
1097
1098    #[test]
1099    fn typedef_with_array_dims_lowers_to_array_type() {
1100        let t = TypedefType {
1101            name: "Vec3".into(),
1102            type_ref: primitive(XmlPrimitive::Float),
1103            array_dimensions: alloc::vec![3],
1104            ..Default::default()
1105        };
1106        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1107        match mto {
1108            MinimalTypeObject::Array(a) => {
1109                assert_eq!(a.bound_seq, alloc::vec![3]);
1110                assert_eq!(
1111                    a.element.common.type_id,
1112                    TypeIdentifier::Primitive(PrimitiveKind::Float32)
1113                );
1114            }
1115            other => panic!("expected array, got {other:?}"),
1116        }
1117    }
1118
1119    #[test]
1120    fn typedef_with_sequence_bound_lowers_to_sequence_type() {
1121        let t = TypedefType {
1122            name: "Bytes".into(),
1123            type_ref: primitive(XmlPrimitive::Octet),
1124            sequence_max_length: Some(1024),
1125            ..Default::default()
1126        };
1127        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1128        match mto {
1129            MinimalTypeObject::Sequence(s) => {
1130                assert_eq!(s.bound, 1024);
1131            }
1132            _ => panic!(),
1133        }
1134    }
1135
1136    // ---- Bitmask ---------------------------------------------------------
1137
1138    #[test]
1139    fn bitmask_three_flags() {
1140        let b = BitmaskType {
1141            name: "Perms".into(),
1142            bit_bound: Some(8),
1143            bit_values: alloc::vec![
1144                BitValue {
1145                    name: "READ".into(),
1146                    position: Some(0),
1147                },
1148                BitValue {
1149                    name: "WRITE".into(),
1150                    position: Some(1),
1151                },
1152                BitValue {
1153                    name: "EXEC".into(),
1154                    position: Some(2),
1155                },
1156            ],
1157        };
1158        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitmask(b)).unwrap();
1159        if let MinimalTypeObject::Bitmask(bm) = mto {
1160            assert_eq!(bm.bit_bound, 8);
1161            assert_eq!(bm.flag_seq.len(), 3);
1162            assert_eq!(bm.flag_seq[0].common.position, 0);
1163            assert_eq!(bm.flag_seq[2].common.position, 2);
1164        } else {
1165            panic!();
1166        }
1167    }
1168
1169    #[test]
1170    fn bitmask_auto_position() {
1171        let b = BitmaskType {
1172            name: "X".into(),
1173            bit_bound: None,
1174            bit_values: alloc::vec![
1175                BitValue {
1176                    name: "A".into(),
1177                    position: None,
1178                },
1179                BitValue {
1180                    name: "B".into(),
1181                    position: None,
1182                },
1183                BitValue {
1184                    name: "C".into(),
1185                    position: Some(10),
1186                },
1187                BitValue {
1188                    name: "D".into(),
1189                    position: None,
1190                },
1191            ],
1192        };
1193        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitmask(b)).unwrap();
1194        if let MinimalTypeObject::Bitmask(bm) = mto {
1195            assert_eq!(bm.flag_seq[0].common.position, 0);
1196            assert_eq!(bm.flag_seq[1].common.position, 1);
1197            assert_eq!(bm.flag_seq[2].common.position, 10);
1198            assert_eq!(bm.flag_seq[3].common.position, 11);
1199            assert_eq!(bm.bit_bound, 32);
1200        } else {
1201            panic!();
1202        }
1203    }
1204
1205    // ---- Bitset ----------------------------------------------------------
1206
1207    #[test]
1208    fn bitset_with_two_fields() {
1209        let b = BitsetType {
1210            name: "Packed".into(),
1211            bit_fields: alloc::vec![
1212                BitField {
1213                    name: "header".into(),
1214                    type_ref: primitive(XmlPrimitive::ULong),
1215                    mask: "0x0F".into(),
1216                },
1217                BitField {
1218                    name: "body".into(),
1219                    type_ref: primitive(XmlPrimitive::ULong),
1220                    mask: "0xFFFFFFF0".into(),
1221                },
1222            ],
1223        };
1224        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap();
1225        if let MinimalTypeObject::Bitset(bs) = mto {
1226            assert_eq!(bs.field_seq.len(), 2);
1227            assert_eq!(bs.field_seq[0].common.bitcount, 4);
1228            assert_eq!(bs.field_seq[0].common.position, 0);
1229            assert_eq!(bs.field_seq[1].common.bitcount, 28);
1230            assert_eq!(bs.field_seq[1].common.position, 4);
1231        } else {
1232            panic!();
1233        }
1234    }
1235
1236    #[test]
1237    fn bitset_invalid_mask_rejected() {
1238        let b = BitsetType {
1239            name: "X".into(),
1240            bit_fields: alloc::vec![BitField {
1241                name: "f".into(),
1242                type_ref: primitive(XmlPrimitive::ULong),
1243                mask: "garbage".into(),
1244            }],
1245        };
1246        let err = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap_err();
1247        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1248    }
1249
1250    #[test]
1251    fn bitset_zero_mask_rejected() {
1252        let b = BitsetType {
1253            name: "X".into(),
1254            bit_fields: alloc::vec![BitField {
1255                name: "f".into(),
1256                type_ref: primitive(XmlPrimitive::ULong),
1257                mask: "0x00".into(),
1258            }],
1259        };
1260        let err = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap_err();
1261        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1262    }
1263
1264    // ---- Module / Library -----------------------------------------------
1265
1266    #[test]
1267    fn module_at_top_level_is_error() {
1268        let m = TypeDef::Module(ModuleEntry {
1269            name: "M".into(),
1270            types: alloc::vec![],
1271        });
1272        let err = xml_type_to_minimal_typeobject(&m).unwrap_err();
1273        assert!(matches!(err, BridgeError::ModuleAtTopLevel(_)));
1274    }
1275
1276    #[test]
1277    fn library_resolves_named_refs_between_types() {
1278        // struct Point { float x; float y; };
1279        // struct Line { Point a; Point b; };
1280        let lib = TypeLibrary {
1281            name: "Lib".into(),
1282            types: alloc::vec![
1283                TypeDef::Struct(StructType {
1284                    name: "Point".into(),
1285                    extensibility: None,
1286                    base_type: None,
1287                    members: alloc::vec![
1288                        StructMember {
1289                            name: "x".into(),
1290                            type_ref: primitive(XmlPrimitive::Float),
1291                            ..Default::default()
1292                        },
1293                        StructMember {
1294                            name: "y".into(),
1295                            type_ref: primitive(XmlPrimitive::Float),
1296                            ..Default::default()
1297                        },
1298                    ],
1299                }),
1300                TypeDef::Struct(StructType {
1301                    name: "Line".into(),
1302                    extensibility: None,
1303                    base_type: None,
1304                    members: alloc::vec![
1305                        StructMember {
1306                            name: "a".into(),
1307                            type_ref: TypeRef::Named("Point".into()),
1308                            ..Default::default()
1309                        },
1310                        StructMember {
1311                            name: "b".into(),
1312                            type_ref: TypeRef::Named("Point".into()),
1313                            ..Default::default()
1314                        },
1315                    ],
1316                }),
1317            ],
1318        };
1319        let map = bridge_library(&lib).unwrap();
1320        let line = map.get("Line").expect("Line registered");
1321        if let MinimalTypeObject::Struct(st) = line {
1322            // Beide Member sollten auf denselben Hash zeigen.
1323            assert_eq!(
1324                st.member_seq[0].common.member_type_id,
1325                st.member_seq[1].common.member_type_id
1326            );
1327            assert!(matches!(
1328                st.member_seq[0].common.member_type_id,
1329                TypeIdentifier::EquivalenceHashMinimal(_)
1330            ));
1331        } else {
1332            panic!("Line not a struct");
1333        }
1334        // Beide Types in der Map.
1335        assert!(map.contains_key("Point"));
1336    }
1337
1338    #[test]
1339    fn library_module_flattens_to_scoped_name() {
1340        let lib = TypeLibrary {
1341            name: "L".into(),
1342            types: alloc::vec![TypeDef::Module(ModuleEntry {
1343                name: "Inner".into(),
1344                types: alloc::vec![TypeDef::Struct(StructType {
1345                    name: "S".into(),
1346                    extensibility: None,
1347                    base_type: None,
1348                    members: alloc::vec![],
1349                })],
1350            })],
1351        };
1352        let map = bridge_library(&lib).unwrap();
1353        assert!(map.contains_key("Inner::S"));
1354    }
1355
1356    #[test]
1357    fn library_unknown_named_ref_falls_back_to_zero_hash() {
1358        let lib = TypeLibrary {
1359            name: "L".into(),
1360            types: alloc::vec![TypeDef::Struct(StructType {
1361                name: "S".into(),
1362                extensibility: None,
1363                base_type: None,
1364                members: alloc::vec![StructMember {
1365                    name: "missing".into(),
1366                    type_ref: TypeRef::Named("DoesNotExist".into()),
1367                    ..Default::default()
1368                }],
1369            })],
1370        };
1371        let map = bridge_library(&lib).unwrap();
1372        let s = map.get("S").unwrap();
1373        if let MinimalTypeObject::Struct(st) = s {
1374            // Null-Hash als Fallback.
1375            assert_eq!(
1376                st.member_seq[0].common.member_type_id,
1377                TypeIdentifier::EquivalenceHashMinimal(zerodds_types::EquivalenceHash([0; 14]))
1378            );
1379        } else {
1380            panic!();
1381        }
1382    }
1383
1384    // ---- TypeIdentifier-Hash --------------------------------------------
1385
1386    #[test]
1387    fn typeobject_can_be_hashed() {
1388        let s = make_struct();
1389        let to = xml_type_to_typeobject(&TypeDef::Struct(s)).unwrap();
1390        let h = zerodds_types::compute_hash(&to).expect("hash");
1391        // Deterministisch: zweimal dasselbe Ergebnis.
1392        let h2 = zerodds_types::compute_hash(&to).unwrap();
1393        assert_eq!(h, h2);
1394    }
1395
1396    #[test]
1397    fn xml_struct_matches_idl_struct_hash() {
1398        // Cross-Check: ein XML-Struct und eine direkt im Builder
1399        // formulierte aequivalente Definition sollten denselben Hash
1400        // haben (sofern Member-IDs/Flags/Types identisch sind).
1401        let xml_struct = StructType {
1402            name: "Sensor".into(),
1403            extensibility: Some(Extensibility::Appendable),
1404            base_type: None,
1405            members: alloc::vec![
1406                StructMember {
1407                    name: "id".into(),
1408                    type_ref: primitive(XmlPrimitive::LongLong),
1409                    key: true,
1410                    ..Default::default()
1411                },
1412                StructMember {
1413                    name: "temp".into(),
1414                    type_ref: primitive(XmlPrimitive::Float),
1415                    ..Default::default()
1416                },
1417            ],
1418        };
1419        let xml_mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(xml_struct)).unwrap();
1420        let xml_hash = compute_minimal_hash(&xml_mto).unwrap();
1421
1422        // Aequivalentes via Builder direkt:
1423        let builder_struct = TypeObjectBuilder::struct_type("Sensor")
1424            .extensibility(TypeExt::Appendable)
1425            .member("id", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
1426                m.key()
1427            })
1428            .member(
1429                "temp",
1430                TypeIdentifier::Primitive(PrimitiveKind::Float32),
1431                |m| m,
1432            )
1433            .build_minimal();
1434        let builder_mto = MinimalTypeObject::Struct(builder_struct);
1435        let builder_hash = compute_minimal_hash(&builder_mto).unwrap();
1436        assert_eq!(
1437            xml_hash, builder_hash,
1438            "XML-bridge und Builder muessen byte-identische TypeObjects produzieren"
1439        );
1440    }
1441
1442    // ---- Display-Impl der BridgeError ------------------------------------
1443
1444    #[test]
1445    fn bridge_error_display() {
1446        let e = BridgeError::UnsupportedXsdConstruct("foo".into());
1447        assert!(e.to_string().contains("foo"));
1448        let e = BridgeError::UnresolvedReference("Bar".into());
1449        assert!(e.to_string().contains("Bar"));
1450        let e = BridgeError::InvalidLiteral("x".into());
1451        assert!(e.to_string().contains("x"));
1452        let e = BridgeError::HashFailed("oom".into());
1453        assert!(e.to_string().contains("oom"));
1454        let e = BridgeError::ModuleAtTopLevel("M".into());
1455        assert!(e.to_string().contains("M"));
1456    }
1457}