Skip to main content

zerodds_types/
resolve.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Type-Resolution + Recursion-Guards (XTypes §7.3.4.5, §7.3.4.9).
4//!
5//! Cross-references between TypeObjects happen via `EquivalenceHash`
6//! TypeIdentifiers (EK_MINIMAL / EK_COMPLETE). A TypeRegistry map caches
7//! known objects; resolver functions follow alias chains and
8//! detect recursion/cycles/DoS attempts via a depth cap.
9
10use alloc::collections::{BTreeMap, BTreeSet};
11use alloc::vec::Vec;
12
13use crate::type_identifier::{EquivalenceHash, TypeIdentifier};
14use crate::type_object::minimal::MinimalTypeObject;
15use crate::type_object::{CompleteTypeObject, TypeObject};
16
17/// Maximum depth for recursively resolving alias chains and
18/// TypeIdentifier references. Prevents DoS via malicious
19/// type graphs with cycles.
20pub const DEFAULT_MAX_RESOLVE_DEPTH: usize = 64;
21
22/// Maximum number of nodes during [`collect_referenced_hashes`]. In addition
23/// to the depth cap this also bounds wide/fan-out-heavy graphs
24/// (a struct with 10,000 member entries that all reference hashes).
25pub const DEFAULT_MAX_RESOLVE_NODES: usize = 4_096;
26
27/// Error during type resolution.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum ResolveError {
31    /// Maximum depth exceeded.
32    DepthExceeded {
33        /// Allowed depth.
34        limit: usize,
35    },
36    /// Maximum node count exceeded.
37    NodeLimitExceeded {
38        /// Allowed node count.
39        limit: usize,
40    },
41    /// Referenced type not in the registry.
42    Unknown {
43        /// Hash that was looked up.
44        hash: EquivalenceHash,
45    },
46    /// Cycle without progress detected.
47    Cycle,
48}
49
50/// In-memory registry of known TypeObjects, indexed by
51/// `EquivalenceHash`. Typically populated by TypeLookup replies.
52#[derive(Debug, Clone, Default)]
53pub struct TypeRegistry {
54    minimals: BTreeMap<EquivalenceHash, MinimalTypeObject>,
55    completes: BTreeMap<EquivalenceHash, CompleteTypeObject>,
56}
57
58impl TypeRegistry {
59    /// Leere Registry.
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Inserts a minimal TypeObject, indexed by its hash.
66    pub fn insert_minimal(&mut self, hash: EquivalenceHash, t: MinimalTypeObject) {
67        self.minimals.insert(hash, t);
68    }
69
70    /// Inserts a complete TypeObject.
71    pub fn insert_complete(&mut self, hash: EquivalenceHash, t: CompleteTypeObject) {
72        self.completes.insert(hash, t);
73    }
74
75    /// Lookup.
76    #[must_use]
77    pub fn get_minimal(&self, hash: &EquivalenceHash) -> Option<&MinimalTypeObject> {
78        self.minimals.get(hash)
79    }
80
81    /// Lookup.
82    #[must_use]
83    pub fn get_complete(&self, hash: &EquivalenceHash) -> Option<&CompleteTypeObject> {
84        self.completes.get(hash)
85    }
86
87    /// Number of entries.
88    #[must_use]
89    pub fn len(&self) -> (usize, usize) {
90        (self.minimals.len(), self.completes.len())
91    }
92
93    /// Empty?
94    #[must_use]
95    pub fn is_empty(&self) -> bool {
96        self.minimals.is_empty() && self.completes.is_empty()
97    }
98
99    /// Iterates over all minimal entries.
100    pub fn iter_minimals(
101        &self,
102    ) -> alloc::collections::btree_map::Iter<'_, EquivalenceHash, MinimalTypeObject> {
103        self.minimals.iter()
104    }
105
106    /// Iterates over all complete entries.
107    pub fn iter_completes(
108        &self,
109    ) -> alloc::collections::btree_map::Iter<'_, EquivalenceHash, CompleteTypeObject> {
110        self.completes.iter()
111    }
112
113    /// Returns all direct dependencies of a hash (transitively via
114    /// [`collect_referenced_hashes`]). If the type is not in the
115    /// registry, empty is returned.
116    ///
117    /// Prefers the Minimal variant, falls back to Complete.
118    #[must_use]
119    pub fn dependencies_of(&self, hash: &EquivalenceHash) -> Vec<EquivalenceHash> {
120        let to = if let Some(m) = self.minimals.get(hash) {
121            TypeObject::Minimal(m.clone())
122        } else if let Some(c) = self.completes.get(hash) {
123            TypeObject::Complete(c.clone())
124        } else {
125            return Vec::new();
126        };
127        collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap_or_default()
128    }
129
130    /// Collects **transitively** all dependent hashes — follows each
131    /// found hash recursively through the registry. The incoming hash
132    /// itself is NOT included. Bounded by `max_nodes`.
133    #[must_use]
134    pub fn transitive_dependencies(
135        &self,
136        hash: &EquivalenceHash,
137        max_nodes: usize,
138    ) -> Vec<EquivalenceHash> {
139        let mut out: Vec<EquivalenceHash> = Vec::new();
140        let mut seen: BTreeSet<EquivalenceHash> = BTreeSet::new();
141        let mut queue: Vec<EquivalenceHash> = self.dependencies_of(hash);
142        while let Some(h) = queue.pop() {
143            if !seen.insert(h) {
144                continue;
145            }
146            if out.len() >= max_nodes {
147                break;
148            }
149            out.push(h);
150            for child in self.dependencies_of(&h) {
151                if !seen.contains(&child) {
152                    queue.push(child);
153                }
154            }
155        }
156        out
157    }
158}
159
160/// Follows alias chains: if `ti` references an alias (and the
161/// alias is known in the registry), resolve to the related_type.
162/// Primitive / plain / hash-direct are returned unchanged.
163///
164/// # Errors
165/// - `DepthExceeded` on deeper alias nesting than `max_depth`.
166/// - `Unknown` if a hash is not in the registry.
167pub fn resolve_alias_chain(
168    start: &TypeIdentifier,
169    registry: &TypeRegistry,
170    max_depth: usize,
171) -> Result<TypeIdentifier, ResolveError> {
172    let mut current = start.clone();
173    let mut visited: BTreeSet<EquivalenceHash> = BTreeSet::new();
174    for _ in 0..max_depth {
175        let hash = match &current {
176            TypeIdentifier::EquivalenceHashMinimal(h) => *h,
177            TypeIdentifier::EquivalenceHashComplete(h) => *h,
178            _ => return Ok(current), // not hash-referenced — done
179        };
180        if !visited.insert(hash) {
181            return Err(ResolveError::Cycle);
182        }
183
184        let next_ti = if matches!(current, TypeIdentifier::EquivalenceHashMinimal(_)) {
185            match registry.get_minimal(&hash) {
186                Some(MinimalTypeObject::Alias(a)) => a.body.common.related_type.clone(),
187                Some(_) => return Ok(current), // struct/enum/... — not an alias
188                None => return Err(ResolveError::Unknown { hash }),
189            }
190        } else {
191            match registry.get_complete(&hash) {
192                Some(CompleteTypeObject::Alias(a)) => a.body.related_type.clone(),
193                Some(_) => return Ok(current),
194                None => return Err(ResolveError::Unknown { hash }),
195            }
196        };
197        current = next_ti;
198    }
199    Err(ResolveError::DepthExceeded { limit: max_depth })
200}
201
202/// Collects transitively all `EquivalenceHash` TypeIdentifiers that are
203/// referenced by `root` directly or indirectly (through collections, struct
204/// members, union cases, alias targets). Useful for
205/// TypeLookup dependency resolution (T14).
206///
207/// Uses a depth cap against malicious type graphs.
208///
209/// # Errors
210/// `DepthExceeded` if the type graph is deeper than `max_depth`.
211pub fn collect_referenced_hashes(
212    root: &TypeObject,
213    max_depth: usize,
214) -> Result<Vec<EquivalenceHash>, ResolveError> {
215    let mut out = Vec::new();
216    let mut seen = Vec::new();
217    collect_from_type_object(root, max_depth, 0, &mut out, &mut seen)?;
218    Ok(out)
219}
220
221fn collect_from_type_object(
222    to: &TypeObject,
223    max_depth: usize,
224    depth: usize,
225    out: &mut Vec<EquivalenceHash>,
226    seen: &mut Vec<EquivalenceHash>,
227) -> Result<(), ResolveError> {
228    if depth >= max_depth {
229        return Err(ResolveError::DepthExceeded { limit: max_depth });
230    }
231    match to {
232        TypeObject::Minimal(m) => collect_from_minimal(m, max_depth, depth + 1, out, seen),
233        TypeObject::Complete(c) => collect_from_complete(c, max_depth, depth + 1, out, seen),
234    }
235}
236
237fn collect_from_minimal(
238    m: &MinimalTypeObject,
239    max_depth: usize,
240    depth: usize,
241    out: &mut Vec<EquivalenceHash>,
242    seen: &mut Vec<EquivalenceHash>,
243) -> Result<(), ResolveError> {
244    match m {
245        MinimalTypeObject::Struct(s) => {
246            collect_from_ti(&s.header.base_type, max_depth, depth, out, seen)?;
247            for mem in &s.member_seq {
248                collect_from_ti(&mem.common.member_type_id, max_depth, depth, out, seen)?;
249            }
250        }
251        MinimalTypeObject::Union(u) => {
252            collect_from_ti(&u.discriminator.common.type_id, max_depth, depth, out, seen)?;
253            for mem in &u.member_seq {
254                collect_from_ti(&mem.common.type_id, max_depth, depth, out, seen)?;
255            }
256        }
257        MinimalTypeObject::Alias(a) => {
258            collect_from_ti(&a.body.common.related_type, max_depth, depth, out, seen)?;
259        }
260        MinimalTypeObject::Sequence(s) => {
261            collect_from_ti(&s.element.common.type_id, max_depth, depth, out, seen)?;
262        }
263        MinimalTypeObject::Array(a) => {
264            collect_from_ti(&a.element.common.type_id, max_depth, depth, out, seen)?;
265        }
266        MinimalTypeObject::Map(m) => {
267            collect_from_ti(&m.key.common.type_id, max_depth, depth, out, seen)?;
268            collect_from_ti(&m.element.common.type_id, max_depth, depth, out, seen)?;
269        }
270        _ => {} // Enum/Bitmask/Bitset/Annotation — no nested TIs
271    }
272    Ok(())
273}
274
275fn collect_from_complete(
276    c: &CompleteTypeObject,
277    max_depth: usize,
278    depth: usize,
279    out: &mut Vec<EquivalenceHash>,
280    seen: &mut Vec<EquivalenceHash>,
281) -> Result<(), ResolveError> {
282    match c {
283        CompleteTypeObject::Struct(s) => {
284            collect_from_ti(&s.header.base_type, max_depth, depth, out, seen)?;
285            for mem in &s.member_seq {
286                collect_from_ti(&mem.common.member_type_id, max_depth, depth, out, seen)?;
287            }
288        }
289        CompleteTypeObject::Union(u) => {
290            collect_from_ti(&u.discriminator.common.type_id, max_depth, depth, out, seen)?;
291            for mem in &u.member_seq {
292                collect_from_ti(&mem.common.type_id, max_depth, depth, out, seen)?;
293            }
294        }
295        CompleteTypeObject::Alias(a) => {
296            collect_from_ti(&a.body.related_type, max_depth, depth, out, seen)?;
297        }
298        CompleteTypeObject::Sequence(s) => {
299            collect_from_ti(&s.element.common.type_id, max_depth, depth, out, seen)?;
300        }
301        CompleteTypeObject::Array(a) => {
302            collect_from_ti(&a.element.common.type_id, max_depth, depth, out, seen)?;
303        }
304        CompleteTypeObject::Map(m) => {
305            collect_from_ti(&m.key.common.type_id, max_depth, depth, out, seen)?;
306            collect_from_ti(&m.element.common.type_id, max_depth, depth, out, seen)?;
307        }
308        _ => {}
309    }
310    Ok(())
311}
312
313/// zerodds-lint: recursion-depth 16
314///
315/// Walks plain collection elements recursively (Sequence→Map→Array nesting).
316/// The depth is effectively bounded by `TypeIdentifier::MAX_DECODE_DEPTH` (=16),
317/// because the input comes from the wire decoder.
318fn collect_from_ti(
319    ti: &TypeIdentifier,
320    _max_depth: usize,
321    _depth: usize,
322    out: &mut Vec<EquivalenceHash>,
323    seen: &mut Vec<EquivalenceHash>,
324) -> Result<(), ResolveError> {
325    match ti {
326        TypeIdentifier::EquivalenceHashMinimal(h) | TypeIdentifier::EquivalenceHashComplete(h) => {
327            if !seen.contains(h) {
328                if seen.len() >= DEFAULT_MAX_RESOLVE_NODES {
329                    return Err(ResolveError::NodeLimitExceeded {
330                        limit: DEFAULT_MAX_RESOLVE_NODES,
331                    });
332                }
333                seen.push(*h);
334                out.push(*h);
335            }
336        }
337        TypeIdentifier::PlainSequenceSmall { element, .. }
338        | TypeIdentifier::PlainSequenceLarge { element, .. }
339        | TypeIdentifier::PlainArraySmall { element, .. }
340        | TypeIdentifier::PlainArrayLarge { element, .. } => {
341            collect_from_ti(element, _max_depth, _depth, out, seen)?;
342        }
343        TypeIdentifier::PlainMapSmall { element, key, .. }
344        | TypeIdentifier::PlainMapLarge { element, key, .. } => {
345            collect_from_ti(element, _max_depth, _depth, out, seen)?;
346            collect_from_ti(key, _max_depth, _depth, out, seen)?;
347        }
348        _ => {}
349    }
350    Ok(())
351}
352
353// ============================================================================
354// Tests
355// ============================================================================
356
357#[cfg(test)]
358#[allow(clippy::unwrap_used, clippy::panic)]
359mod tests {
360    use super::*;
361    use crate::builder::TypeObjectBuilder;
362    use crate::hash::compute_minimal_hash;
363    use crate::type_identifier::PrimitiveKind;
364    use crate::type_object::minimal::MinimalAliasType;
365
366    fn make_alias(related: TypeIdentifier) -> MinimalAliasType {
367        use crate::type_object::flags::{AliasMemberFlag, AliasTypeFlag};
368        use crate::type_object::minimal::{CommonAliasBody, MinimalAliasBody};
369        MinimalAliasType {
370            alias_flags: AliasTypeFlag::default(),
371            body: MinimalAliasBody {
372                common: CommonAliasBody {
373                    related_flags: AliasMemberFlag::default(),
374                    related_type: related,
375                },
376            },
377        }
378    }
379
380    #[test]
381    fn resolve_returns_primitive_unchanged() {
382        let reg = TypeRegistry::new();
383        let p = TypeIdentifier::Primitive(PrimitiveKind::Int64);
384        let resolved = resolve_alias_chain(&p, &reg, 8).unwrap();
385        assert_eq!(resolved, p);
386    }
387
388    #[test]
389    fn resolve_follows_single_alias_to_target() {
390        let mut reg = TypeRegistry::new();
391        let target = TypeIdentifier::Primitive(PrimitiveKind::UInt64);
392        let alias = MinimalTypeObject::Alias(make_alias(target.clone()));
393        let alias_hash = compute_minimal_hash(&alias).unwrap();
394        reg.insert_minimal(alias_hash, alias);
395
396        let start = TypeIdentifier::EquivalenceHashMinimal(alias_hash);
397        let resolved = resolve_alias_chain(&start, &reg, 8).unwrap();
398        assert_eq!(resolved, target);
399    }
400
401    #[test]
402    fn resolve_chained_aliases_two_deep() {
403        let mut reg = TypeRegistry::new();
404        let final_target = TypeIdentifier::Primitive(PrimitiveKind::Int8);
405        let inner = MinimalTypeObject::Alias(make_alias(final_target.clone()));
406        let inner_hash = compute_minimal_hash(&inner).unwrap();
407        reg.insert_minimal(inner_hash, inner);
408
409        let outer = MinimalTypeObject::Alias(make_alias(TypeIdentifier::EquivalenceHashMinimal(
410            inner_hash,
411        )));
412        let outer_hash = compute_minimal_hash(&outer).unwrap();
413        reg.insert_minimal(outer_hash, outer);
414
415        let resolved =
416            resolve_alias_chain(&TypeIdentifier::EquivalenceHashMinimal(outer_hash), &reg, 8)
417                .unwrap();
418        assert_eq!(resolved, final_target);
419    }
420
421    #[test]
422    fn resolve_cycle_detected() {
423        let mut reg = TypeRegistry::new();
424        // Two aliases pointing at each other. To get consistent hashes
425        // we would have to insert them simultaneously — we simulate
426        // a cycle by registering the same alias with related_type = its
427        // own hash.
428        let self_hash = EquivalenceHash([0x99; 14]);
429        let self_alias = MinimalTypeObject::Alias(make_alias(
430            TypeIdentifier::EquivalenceHashMinimal(self_hash),
431        ));
432        reg.insert_minimal(self_hash, self_alias);
433
434        let err = resolve_alias_chain(&TypeIdentifier::EquivalenceHashMinimal(self_hash), &reg, 8)
435            .unwrap_err();
436        assert_eq!(err, ResolveError::Cycle);
437    }
438
439    #[test]
440    fn resolve_unknown_hash_fails() {
441        let reg = TypeRegistry::new();
442        let err = resolve_alias_chain(
443            &TypeIdentifier::EquivalenceHashMinimal(EquivalenceHash([0xAA; 14])),
444            &reg,
445            8,
446        )
447        .unwrap_err();
448        assert!(matches!(err, ResolveError::Unknown { .. }));
449    }
450
451    #[test]
452    fn collect_referenced_hashes_picks_up_nested_struct_members() {
453        let inner_hash = EquivalenceHash([0x01; 14]);
454        let st = TypeObjectBuilder::struct_type("::X")
455            .member(
456                "ref_a",
457                TypeIdentifier::EquivalenceHashMinimal(inner_hash),
458                |m| m,
459            )
460            .member("p", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
461            .build_minimal();
462        let to = TypeObject::Minimal(MinimalTypeObject::Struct(st));
463        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
464        assert_eq!(refs, alloc::vec![inner_hash]);
465    }
466
467    #[test]
468    fn collect_referenced_hashes_from_union_cases() {
469        use crate::type_object::common::CommonUnionMember;
470        use crate::type_object::flags::{UnionDiscriminatorFlag, UnionMemberFlag, UnionTypeFlag};
471        use crate::type_object::minimal::{
472            CommonDiscriminatorMember, MinimalDiscriminatorMember, MinimalUnionMember,
473            MinimalUnionType,
474        };
475        let h_disc = EquivalenceHash([0x01; 14]);
476        let h_case = EquivalenceHash([0x02; 14]);
477        let u = MinimalUnionType {
478            union_flags: UnionTypeFlag::default(),
479            discriminator: MinimalDiscriminatorMember {
480                common: CommonDiscriminatorMember {
481                    member_flags: UnionDiscriminatorFlag::default(),
482                    type_id: TypeIdentifier::EquivalenceHashMinimal(h_disc),
483                },
484            },
485            member_seq: alloc::vec![MinimalUnionMember {
486                common: CommonUnionMember {
487                    member_id: 1,
488                    member_flags: UnionMemberFlag::default(),
489                    type_id: TypeIdentifier::EquivalenceHashMinimal(h_case),
490                    label_seq: alloc::vec![1],
491                },
492                detail: crate::type_object::common::NameHash([0; 4]),
493            }],
494        };
495        let to = TypeObject::Minimal(MinimalTypeObject::Union(u));
496        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
497        assert!(refs.contains(&h_disc));
498        assert!(refs.contains(&h_case));
499    }
500
501    #[test]
502    fn collect_referenced_hashes_from_sequence_and_array_and_map() {
503        use crate::builder::TypeObjectBuilder;
504        let h_elem = EquivalenceHash([0x10; 14]);
505
506        let seq = TypeObjectBuilder::sequence(TypeIdentifier::EquivalenceHashMinimal(h_elem), 50)
507            .build_minimal();
508        let refs = collect_referenced_hashes(
509            &TypeObject::Minimal(MinimalTypeObject::Sequence(seq)),
510            DEFAULT_MAX_RESOLVE_DEPTH,
511        )
512        .unwrap();
513        assert_eq!(refs, alloc::vec![h_elem]);
514
515        let arr = TypeObjectBuilder::array(
516            TypeIdentifier::EquivalenceHashMinimal(h_elem),
517            alloc::vec![2, 3],
518        )
519        .build_minimal();
520        let refs = collect_referenced_hashes(
521            &TypeObject::Minimal(MinimalTypeObject::Array(arr)),
522            DEFAULT_MAX_RESOLVE_DEPTH,
523        )
524        .unwrap();
525        assert_eq!(refs, alloc::vec![h_elem]);
526
527        let h_key = EquivalenceHash([0x20; 14]);
528        let map = TypeObjectBuilder::map(
529            TypeIdentifier::EquivalenceHashMinimal(h_key),
530            TypeIdentifier::EquivalenceHashMinimal(h_elem),
531            10,
532        )
533        .build_minimal();
534        let refs = collect_referenced_hashes(
535            &TypeObject::Minimal(MinimalTypeObject::Map(map)),
536            DEFAULT_MAX_RESOLVE_DEPTH,
537        )
538        .unwrap();
539        assert!(refs.contains(&h_key));
540        assert!(refs.contains(&h_elem));
541    }
542
543    #[test]
544    fn collect_referenced_hashes_from_complete_typeobject() {
545        use crate::builder::TypeObjectBuilder;
546        let h = EquivalenceHash([0x44; 14]);
547        let st = TypeObjectBuilder::struct_type("::X")
548            .member("f", TypeIdentifier::EquivalenceHashMinimal(h), |m| m)
549            .build_complete();
550        let to = TypeObject::Complete(CompleteTypeObject::Struct(st));
551        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
552        assert_eq!(refs, alloc::vec![h]);
553    }
554
555    #[test]
556    fn collect_referenced_hashes_from_alias() {
557        use crate::builder::TypeObjectBuilder;
558        let h = EquivalenceHash([0x55; 14]);
559        let a = TypeObjectBuilder::alias("::A", TypeIdentifier::EquivalenceHashMinimal(h))
560            .build_minimal();
561        let to = TypeObject::Minimal(MinimalTypeObject::Alias(a));
562        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
563        assert_eq!(refs, alloc::vec![h]);
564    }
565
566    #[test]
567    fn collect_referenced_hashes_nested_plain_sequence_in_plain_map() {
568        // Plain TypeIdentifier im Member-Type: nested Sequence<of Map<of Hash>>
569        let h = EquivalenceHash([0x66; 14]);
570        let inner_map = TypeIdentifier::PlainMapSmall {
571            header: crate::type_identifier::PlainCollectionHeader::default(),
572            bound: 1,
573            element: alloc::boxed::Box::new(TypeIdentifier::EquivalenceHashMinimal(h)),
574            key_flags: crate::type_identifier::CollectionElementFlag(0),
575            key: alloc::boxed::Box::new(TypeIdentifier::Primitive(
576                crate::type_identifier::PrimitiveKind::Int32,
577            )),
578        };
579        let outer_seq = TypeIdentifier::PlainSequenceSmall {
580            header: crate::type_identifier::PlainCollectionHeader::default(),
581            bound: 3,
582            element: alloc::boxed::Box::new(inner_map),
583        };
584        use crate::builder::TypeObjectBuilder;
585        let st = TypeObjectBuilder::struct_type("::Nested")
586            .member("data", outer_seq, |m| m)
587            .build_minimal();
588        let to = TypeObject::Minimal(MinimalTypeObject::Struct(st));
589        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
590        assert_eq!(refs, alloc::vec![h]);
591    }
592
593    #[test]
594    fn resolve_depth_exceeded_with_long_alias_chain() {
595        // Build chain depth > max_depth.
596        let mut reg = TypeRegistry::new();
597        let mut last_hash = EquivalenceHash([0x00; 14]);
598        for i in 0..10u8 {
599            let target = if i == 0 {
600                TypeIdentifier::Primitive(crate::type_identifier::PrimitiveKind::Int32)
601            } else {
602                TypeIdentifier::EquivalenceHashMinimal(last_hash)
603            };
604            let alias = MinimalTypeObject::Alias(make_alias(target));
605            let h = crate::hash::compute_minimal_hash(&alias).unwrap();
606            reg.insert_minimal(h, alias);
607            last_hash = h;
608        }
609        let err = resolve_alias_chain(
610            &TypeIdentifier::EquivalenceHashMinimal(last_hash),
611            &reg,
612            3, // depth too low
613        )
614        .unwrap_err();
615        assert_eq!(err, ResolveError::DepthExceeded { limit: 3 });
616    }
617
618    #[test]
619    fn collect_referenced_hashes_skips_duplicates() {
620        let h = EquivalenceHash([0x22; 14]);
621        let st = TypeObjectBuilder::struct_type("::Y")
622            .member("a", TypeIdentifier::EquivalenceHashMinimal(h), |m| m)
623            .member("b", TypeIdentifier::EquivalenceHashMinimal(h), |m| m)
624            .build_minimal();
625        let to = TypeObject::Minimal(MinimalTypeObject::Struct(st));
626        let refs = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap();
627        assert_eq!(refs.len(), 1);
628    }
629
630    #[test]
631    fn collect_referenced_hashes_node_limit_exceeded_on_wide_struct() {
632        // A struct with DEFAULT_MAX_RESOLVE_NODES + 1 distinct hash refs
633        // triggers the node-limit brake.
634        let mut builder = TypeObjectBuilder::struct_type("::Wide");
635        for i in 0..(DEFAULT_MAX_RESOLVE_NODES as u32 + 1) {
636            let mut h_bytes = [0u8; 14];
637            h_bytes[..4].copy_from_slice(&i.to_le_bytes());
638            let h = EquivalenceHash(h_bytes);
639            builder = builder.member(
640                alloc::format!("m_{i}").as_str(),
641                TypeIdentifier::EquivalenceHashMinimal(h),
642                |m| m,
643            );
644        }
645        let st = builder.build_minimal();
646        let to = TypeObject::Minimal(MinimalTypeObject::Struct(st));
647        let err = collect_referenced_hashes(&to, DEFAULT_MAX_RESOLVE_DEPTH).unwrap_err();
648        assert_eq!(
649            err,
650            ResolveError::NodeLimitExceeded {
651                limit: DEFAULT_MAX_RESOLVE_NODES,
652            }
653        );
654    }
655
656    // ====================================================================
657    // SCC / Mutual-Dependency (XTypes 1.3 §7.3.4.8 + §7.3.4.9)
658    // ====================================================================
659    //
660    // When three or more structs reference each other, they form
661    // a strongly connected component (SCC). The TypeRegistry must
662    // perform reference resolution correctly despite the cycle — the seen-set-
663    // based walker prevents infinite loops, and transitive_dependencies
664    // deterministically returns all 3/4/N nodes of the SCC.
665
666    fn struct_with_member_ref(name: &str, ref_hash: EquivalenceHash) -> MinimalTypeObject {
667        MinimalTypeObject::Struct(
668            TypeObjectBuilder::struct_type(name)
669                .member(
670                    "next",
671                    TypeIdentifier::EquivalenceHashMinimal(ref_hash),
672                    |m| m,
673                )
674                .build_minimal(),
675        )
676    }
677
678    #[test]
679    fn scc_three_element_cycle_resolves_correctly() {
680        // A -> B -> C -> A
681        let h_a = EquivalenceHash([0x0A; 14]);
682        let h_b = EquivalenceHash([0x0B; 14]);
683        let h_c = EquivalenceHash([0x0C; 14]);
684        let mut reg = TypeRegistry::new();
685        reg.insert_minimal(h_a, struct_with_member_ref("A", h_b));
686        reg.insert_minimal(h_b, struct_with_member_ref("B", h_c));
687        reg.insert_minimal(h_c, struct_with_member_ref("C", h_a));
688
689        let deps = reg.transitive_dependencies(&h_a, DEFAULT_MAX_RESOLVE_NODES);
690        assert!(deps.contains(&h_a));
691        assert!(deps.contains(&h_b));
692        assert!(deps.contains(&h_c));
693        // No infinite loop: all 3 + a-self-ref onto known hashes
694        // → exactly 3 unique entries.
695        let mut sorted = deps.clone();
696        sorted.sort();
697        sorted.dedup();
698        assert_eq!(sorted.len(), 3);
699    }
700
701    #[test]
702    fn scc_four_element_cycle_resolves_correctly() {
703        // A -> B -> C -> D -> A (4-node cycle)
704        let h_a = EquivalenceHash([0x1A; 14]);
705        let h_b = EquivalenceHash([0x1B; 14]);
706        let h_c = EquivalenceHash([0x1C; 14]);
707        let h_d = EquivalenceHash([0x1D; 14]);
708        let mut reg = TypeRegistry::new();
709        reg.insert_minimal(h_a, struct_with_member_ref("A", h_b));
710        reg.insert_minimal(h_b, struct_with_member_ref("B", h_c));
711        reg.insert_minimal(h_c, struct_with_member_ref("C", h_d));
712        reg.insert_minimal(h_d, struct_with_member_ref("D", h_a));
713
714        let deps = reg.transitive_dependencies(&h_a, DEFAULT_MAX_RESOLVE_NODES);
715        assert!(deps.contains(&h_a));
716        assert!(deps.contains(&h_b));
717        assert!(deps.contains(&h_c));
718        assert!(deps.contains(&h_d));
719        let mut sorted = deps.clone();
720        sorted.sort();
721        sorted.dedup();
722        assert_eq!(sorted.len(), 4);
723    }
724
725    #[test]
726    fn scc_diamond_with_cycle_resolves_finitely() {
727        //   A -> B -> D
728        //   |    |
729        //   v    v
730        //   C -> A   (diamond with a back-edge to A)
731        let h_a = EquivalenceHash([0x2A; 14]);
732        let h_b = EquivalenceHash([0x2B; 14]);
733        let h_c = EquivalenceHash([0x2C; 14]);
734        let h_d = EquivalenceHash([0x2D; 14]);
735        let mut reg = TypeRegistry::new();
736        // A has two members: -> B and -> C
737        let st_a = MinimalTypeObject::Struct(
738            TypeObjectBuilder::struct_type("A")
739                .member("to_b", TypeIdentifier::EquivalenceHashMinimal(h_b), |m| m)
740                .member("to_c", TypeIdentifier::EquivalenceHashMinimal(h_c), |m| m)
741                .build_minimal(),
742        );
743        reg.insert_minimal(h_a, st_a);
744        reg.insert_minimal(h_b, struct_with_member_ref("B", h_d));
745        reg.insert_minimal(h_c, struct_with_member_ref("C", h_a));
746        // D: terminal (no hash member)
747        let st_d = MinimalTypeObject::Struct(
748            TypeObjectBuilder::struct_type("D")
749                .member("x", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
750                .build_minimal(),
751        );
752        reg.insert_minimal(h_d, st_d);
753
754        let deps = reg.transitive_dependencies(&h_a, DEFAULT_MAX_RESOLVE_NODES);
755        let mut sorted = deps.clone();
756        sorted.sort();
757        sorted.dedup();
758        assert!(sorted.contains(&h_a));
759        assert!(sorted.contains(&h_b));
760        assert!(sorted.contains(&h_c));
761        assert!(sorted.contains(&h_d));
762        assert_eq!(sorted.len(), 4);
763    }
764
765    // ---- §7.2.2.4.9 @external Forward-Decls + Diamond ----
766
767    #[test]
768    fn external_forward_decl_roundtrip() {
769        // Struct A has an `@external A* next` (self-forward).
770        // The TypeObject's wire encode/decode preserves the EXTERNAL flag.
771        use crate::type_identifier::PrimitiveKind;
772        use crate::type_object::TypeObject;
773        use crate::type_object::flags::StructMemberFlag;
774        use crate::type_object::minimal::MinimalTypeObject;
775        let h_self = EquivalenceHash([0x4A; 14]);
776        let mut a = TypeObjectBuilder::struct_type("::A")
777            .member(
778                "next",
779                TypeIdentifier::EquivalenceHashMinimal(h_self),
780                |m| m.external().id(1),
781            )
782            .member(
783                "value",
784                TypeIdentifier::Primitive(PrimitiveKind::Int32),
785                |m| m.id(2),
786            )
787            .build_minimal();
788        // Pin the hash to the wire form (only for the test).
789        a.member_seq[0].common.member_flags = StructMemberFlag(StructMemberFlag::IS_EXTERNAL);
790
791        let mut reg = TypeRegistry::new();
792        reg.insert_minimal(h_self, MinimalTypeObject::Struct(a.clone()));
793
794        // Wire-Roundtrip via TypeObject Encode/Decode.
795        let to = TypeObject::Minimal(MinimalTypeObject::Struct(a.clone()));
796        let bytes = to.to_bytes_le().unwrap();
797        let decoded = TypeObject::from_bytes_le(&bytes).unwrap();
798        if let TypeObject::Minimal(MinimalTypeObject::Struct(b)) = decoded {
799            assert!(
800                b.member_seq[0]
801                    .common
802                    .member_flags
803                    .has(StructMemberFlag::IS_EXTERNAL)
804            );
805            assert_eq!(b.member_seq[0].common.member_id, 1);
806        } else {
807            panic!("expected MinimalStruct");
808        }
809
810        // Forward resolution via the registry: dependencies_of returns h_self.
811        let deps = reg.transitive_dependencies(&h_self, DEFAULT_MAX_RESOLVE_NODES);
812        assert!(deps.contains(&h_self));
813    }
814
815    #[test]
816    fn external_diamond_resolves_through_both_paths() {
817        // Diamond:
818        //   A -> @external B -> @external D
819        //   A -> @external C -> @external D
820        //
821        // Expected: transitive_dependencies(A) = {B, C, D}, each unique.
822        use crate::type_identifier::PrimitiveKind;
823        use crate::type_object::flags::StructMemberFlag;
824        use crate::type_object::minimal::MinimalTypeObject;
825        let h_a = EquivalenceHash([0x5A; 14]);
826        let h_b = EquivalenceHash([0x5B; 14]);
827        let h_c = EquivalenceHash([0x5C; 14]);
828        let h_d = EquivalenceHash([0x5D; 14]);
829
830        let mut a = TypeObjectBuilder::struct_type("A")
831            .member("to_b", TypeIdentifier::EquivalenceHashMinimal(h_b), |m| {
832                m.external().id(1)
833            })
834            .member("to_c", TypeIdentifier::EquivalenceHashMinimal(h_c), |m| {
835                m.external().id(2)
836            })
837            .build_minimal();
838        // Ensure that the EXTERNAL flags are actually set.
839        for m in &mut a.member_seq {
840            m.common.member_flags = StructMemberFlag(StructMemberFlag::IS_EXTERNAL);
841        }
842
843        let b = TypeObjectBuilder::struct_type("B")
844            .member("to_d", TypeIdentifier::EquivalenceHashMinimal(h_d), |m| {
845                m.external().id(1)
846            })
847            .build_minimal();
848        let c = TypeObjectBuilder::struct_type("C")
849            .member("to_d", TypeIdentifier::EquivalenceHashMinimal(h_d), |m| {
850                m.external().id(1)
851            })
852            .build_minimal();
853        let d = TypeObjectBuilder::struct_type("D")
854            .member("v", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
855                m.id(1)
856            })
857            .build_minimal();
858
859        let mut reg = TypeRegistry::new();
860        reg.insert_minimal(h_a, MinimalTypeObject::Struct(a));
861        reg.insert_minimal(h_b, MinimalTypeObject::Struct(b));
862        reg.insert_minimal(h_c, MinimalTypeObject::Struct(c));
863        reg.insert_minimal(h_d, MinimalTypeObject::Struct(d));
864
865        let deps = reg.transitive_dependencies(&h_a, DEFAULT_MAX_RESOLVE_NODES);
866        let mut sorted = deps.clone();
867        sorted.sort();
868        sorted.dedup();
869        assert!(sorted.contains(&h_b));
870        assert!(sorted.contains(&h_c));
871        assert!(sorted.contains(&h_d));
872        // 3 unique hashes (B, C, D); A itself is the root.
873        assert_eq!(sorted.len(), 3);
874    }
875
876    #[test]
877    fn scc_self_loop_does_not_explode_node_count() {
878        // A -> A (1-node cycle, only self-loop).
879        let h_a = EquivalenceHash([0x3A; 14]);
880        let mut reg = TypeRegistry::new();
881        reg.insert_minimal(h_a, struct_with_member_ref("A", h_a));
882
883        let deps = reg.transitive_dependencies(&h_a, DEFAULT_MAX_RESOLVE_NODES);
884        // dependencies_of(h_a) = [h_a], queue.pop() → seen.insert(h_a) push,
885        // then children of h_a = [h_a] already in seen → done. Exactly 1 entry.
886        assert_eq!(deps, alloc::vec![h_a]);
887    }
888}