Skip to main content

mib_rs/mib/
mib.rs

1//! Top-level MIB container and query methods.
2//!
3//! [`Mib`] is the central type in the resolved model. It owns all arena
4//! storage, the OID tree, lookup indices, and diagnostics. After resolution
5//! it is immutable and safe for concurrent reads behind `&Mib`.
6
7use std::collections::HashMap;
8use std::fmt;
9
10use crate::mib::{Oid, ParseOidError};
11use crate::types::{BaseType, Diagnostic, Kind, Severity};
12
13use super::capability::CapabilityData;
14use super::compliance::ComplianceData;
15use super::group::GroupData;
16use super::handle::{
17    Capability, Compliance, Group, HandleIter, Module, Node, Notification, Object, Type,
18};
19use super::module::ModuleData;
20use super::node::{NodeData, OidTree};
21use super::notification::NotificationData;
22use super::object::ObjectData;
23use super::raw::RawMib;
24use super::symbol::Symbol;
25use super::typedef::TypeData;
26use super::types::*;
27
28/// Top-level container for all resolved MIB data.
29///
30/// Holds the OID tree, all entity arenas, lookup indices, and diagnostics
31/// produced during resolution. Built once and then immutable, so it is safe
32/// for concurrent reads behind `&Mib`.
33///
34/// Use the handle-oriented methods ([`Mib::node`], [`Mib::object`],
35/// [`Mib::module`], etc.) for most queries. For lower-level arena access,
36/// see [`Mib::raw`].
37pub struct Mib {
38    pub(crate) tree: OidTree,
39
40    // Entity arenas
41    pub(crate) objects: Vec<ObjectData>,
42    pub(crate) types: Vec<TypeData>,
43    pub(crate) notifications: Vec<NotificationData>,
44    pub(crate) groups: Vec<GroupData>,
45    pub(crate) compliances: Vec<ComplianceData>,
46    pub(crate) capabilities: Vec<CapabilityData>,
47    pub(crate) modules: Vec<ModuleData>,
48
49    // Lookup indices
50    pub(crate) module_by_name: HashMap<String, ModuleId>,
51    pub(crate) name_to_nodes: HashMap<String, Vec<NodeId>>,
52    pub(crate) type_by_name: HashMap<String, TypeId>,
53
54    pub(crate) node_count: usize,
55    pub(crate) diagnostics: Vec<Diagnostic>,
56    pub(crate) unresolved: Vec<UnresolvedRef>,
57}
58
59impl Mib {
60    pub(crate) fn new() -> Self {
61        Self {
62            tree: OidTree::new(),
63            objects: Vec::new(),
64            types: Vec::new(),
65            notifications: Vec::new(),
66            groups: Vec::new(),
67            compliances: Vec::new(),
68            capabilities: Vec::new(),
69            modules: Vec::new(),
70            module_by_name: HashMap::new(),
71            name_to_nodes: HashMap::new(),
72            type_by_name: HashMap::new(),
73            node_count: 0,
74            diagnostics: Vec::new(),
75            unresolved: Vec::new(),
76        }
77    }
78
79    // --- Tree access ---
80
81    pub(crate) fn tree(&self) -> &OidTree {
82        &self.tree
83    }
84
85    /// Return a low-level view of this MIB.
86    ///
87    /// Exposes arena-backed ids and data structures. Most callers should
88    /// prefer the high-level borrowed handles instead.
89    pub fn raw(&self) -> RawMib<'_> {
90        RawMib::new(self)
91    }
92
93    pub(crate) fn node_data(&self, id: NodeId) -> &NodeData {
94        self.tree.get(id)
95    }
96
97    /// Return a handle to the synthetic root of the OID tree.
98    ///
99    /// The root has no name and no OID arcs. All top-level OID branches
100    /// (`iso`, etc.) are children of this node.
101    #[must_use]
102    pub fn root_node(&self) -> Node<'_> {
103        Node::new(self, self.tree.root())
104    }
105
106    // --- Entity accessors ---
107
108    pub(crate) fn object_data(&self, id: ObjectId) -> &ObjectData {
109        &self.objects[id.0 as usize]
110    }
111
112    #[must_use]
113    pub(crate) fn type_data(&self, id: TypeId) -> &TypeData {
114        &self.types[id.0 as usize]
115    }
116
117    pub(crate) fn notification_data(&self, id: NotificationId) -> &NotificationData {
118        &self.notifications[id.0 as usize]
119    }
120
121    pub(crate) fn group_data(&self, id: GroupId) -> &GroupData {
122        &self.groups[id.0 as usize]
123    }
124
125    pub(crate) fn compliance_data(&self, id: ComplianceId) -> &ComplianceData {
126        &self.compliances[id.0 as usize]
127    }
128
129    pub(crate) fn capability_data(&self, id: CapabilityId) -> &CapabilityData {
130        &self.capabilities[id.0 as usize]
131    }
132
133    pub(crate) fn module_data(&self, id: ModuleId) -> &ModuleData {
134        &self.modules[id.0 as usize]
135    }
136
137    // --- Mutable entity access (for resolver) ---
138
139    pub(crate) fn object_mut(&mut self, id: ObjectId) -> &mut ObjectData {
140        &mut self.objects[id.0 as usize]
141    }
142
143    pub(crate) fn type_mut(&mut self, id: TypeId) -> &mut TypeData {
144        &mut self.types[id.0 as usize]
145    }
146
147    pub(crate) fn module_mut(&mut self, id: ModuleId) -> &mut ModuleData {
148        &mut self.modules[id.0 as usize]
149    }
150
151    // --- Name lookups ---
152
153    /// Search nodes associated with `name` for the first one where `get`
154    /// returns Some.
155    fn find_in_nodes<T>(&self, name: &str, get: impl Fn(&NodeData) -> Option<T>) -> Option<T> {
156        for &id in self.name_to_nodes.get(name)? {
157            if let Some(val) = get(self.tree.get(id)) {
158                return Some(val);
159            }
160        }
161        None
162    }
163
164    /// Return all nodes registered under the given name, or an empty slice.
165    ///
166    /// Unlike [`Mib::node_by_name`], which returns a single preferred node,
167    /// this returns every node across all loaded modules that shares the name.
168    #[must_use]
169    pub fn nodes_by_name(&self, name: &str) -> &[NodeId] {
170        self.name_to_nodes
171            .get(name)
172            .map(|v| v.as_slice())
173            .unwrap_or(&[])
174    }
175
176    /// Look up a node by name, returning the [`NodeId`].
177    ///
178    /// When multiple nodes share the same name (which can happen when
179    /// different modules define overlapping OID assignments), prefers the
180    /// one with an attached object, then notifications, then any remaining node.
181    #[must_use]
182    pub fn node_by_name(&self, name: &str) -> Option<NodeId> {
183        let nodes = self.name_to_nodes.get(name)?;
184        for &id in nodes {
185            if self.tree.get(id).object.is_some() {
186                return Some(id);
187            }
188        }
189        for &id in nodes {
190            if self.tree.get(id).notification.is_some() {
191                return Some(id);
192            }
193        }
194        nodes.first().copied()
195    }
196
197    /// Look up an object by name, returning the [`ObjectId`].
198    #[must_use]
199    pub fn object_by_name(&self, name: &str) -> Option<ObjectId> {
200        self.find_in_nodes(name, |n| n.object)
201    }
202
203    /// Look up an object by name and return an [`Object`] handle.
204    #[must_use]
205    pub fn object(&self, name: &str) -> Option<Object<'_>> {
206        self.object_by_name(name).map(|id| Object::new(self, id))
207    }
208
209    /// Look up a type by name, returning the [`TypeId`].
210    #[must_use]
211    pub fn type_by_name(&self, name: &str) -> Option<TypeId> {
212        self.type_by_name.get(name).copied()
213    }
214
215    /// Look up a type by name and return a [`Type`] handle.
216    #[must_use]
217    pub fn r#type(&self, name: &str) -> Option<Type<'_>> {
218        self.type_by_name(name).map(|id| Type::new(self, id))
219    }
220
221    /// Look up a notification by name, returning the [`NotificationId`].
222    #[must_use]
223    pub fn notification_by_name(&self, name: &str) -> Option<NotificationId> {
224        self.find_in_nodes(name, |n| n.notification)
225    }
226
227    /// Look up a notification by name and return a [`Notification`] handle.
228    #[must_use]
229    pub fn notification(&self, name: &str) -> Option<Notification<'_>> {
230        self.notification_by_name(name)
231            .map(|id| Notification::new(self, id))
232    }
233
234    /// Look up a group by name, returning the [`GroupId`].
235    #[must_use]
236    pub fn group_by_name(&self, name: &str) -> Option<GroupId> {
237        self.find_in_nodes(name, |n| n.group)
238    }
239
240    /// Look up a group by name and return a [`Group`] handle.
241    #[must_use]
242    pub fn group(&self, name: &str) -> Option<Group<'_>> {
243        self.group_by_name(name).map(|id| Group::new(self, id))
244    }
245
246    /// Look up a compliance statement by name, returning the [`ComplianceId`].
247    #[must_use]
248    pub fn compliance_by_name(&self, name: &str) -> Option<ComplianceId> {
249        self.find_in_nodes(name, |n| n.compliance)
250    }
251
252    /// Look up a compliance statement by name and return a [`Compliance`] handle.
253    #[must_use]
254    pub fn compliance(&self, name: &str) -> Option<Compliance<'_>> {
255        self.compliance_by_name(name)
256            .map(|id| Compliance::new(self, id))
257    }
258
259    /// Look up a capability statement by name, returning the [`CapabilityId`].
260    #[must_use]
261    pub fn capability_by_name(&self, name: &str) -> Option<CapabilityId> {
262        self.find_in_nodes(name, |n| n.capability)
263    }
264
265    /// Look up a capability statement by name and return a [`Capability`] handle.
266    #[must_use]
267    pub fn capability(&self, name: &str) -> Option<Capability<'_>> {
268        self.capability_by_name(name)
269            .map(|id| Capability::new(self, id))
270    }
271
272    /// Look up a module by name, returning the [`ModuleId`].
273    #[must_use]
274    pub fn module_by_name(&self, name: &str) -> Option<ModuleId> {
275        self.module_by_name.get(name).copied()
276    }
277
278    /// Look up a module by name and return a [`Module`] handle.
279    #[must_use]
280    pub fn module(&self, name: &str) -> Option<Module<'_>> {
281        self.module_by_name(name).map(|id| Module::new(self, id))
282    }
283
284    /// Look up a symbol by name, returning a [`Symbol`] variant.
285    ///
286    /// Priority: objects, notifications, groups, compliances, capabilities,
287    /// plain nodes, then types.
288    #[must_use]
289    pub fn symbol_by_name(&self, name: &str) -> Option<Symbol> {
290        if let Some(nodes) = self.name_to_nodes.get(name) {
291            let mut notification = None;
292            let mut group = None;
293            let mut compliance = None;
294            let mut capability = None;
295            let mut node = None;
296
297            for &id in nodes {
298                let entry = self.tree.get(id);
299                node.get_or_insert(id);
300                if let Some(object) = entry.object {
301                    return Some(Symbol::Object(object));
302                }
303                notification = notification.or(entry.notification);
304                group = group.or(entry.group);
305                compliance = compliance.or(entry.compliance);
306                capability = capability.or(entry.capability);
307            }
308
309            if let Some(id) = notification {
310                return Some(Symbol::Notification(id));
311            }
312            if let Some(id) = group {
313                return Some(Symbol::Group(id));
314            }
315            if let Some(id) = compliance {
316                return Some(Symbol::Compliance(id));
317            }
318            if let Some(id) = capability {
319                return Some(Symbol::Capability(id));
320            }
321            if let Some(id) = node {
322                return Some(Symbol::Node(id));
323            }
324        }
325        if let Some(&id) = self.type_by_name.get(name) {
326            return Some(Symbol::Type(id));
327        }
328        None
329    }
330
331    // --- OID lookups ---
332
333    /// Look up a node at an exact numeric [`Oid`], returning the [`NodeId`].
334    ///
335    /// Returns `None` if no tree node exists at that exact OID.
336    #[must_use]
337    pub fn node_by_oid(&self, oid: &Oid) -> Option<NodeId> {
338        let (id, exact) = self.tree.walk_oid(self.tree.root(), oid);
339        if exact { Some(id) } else { None }
340    }
341
342    /// Look up a node by name and return a [`Node`] handle.
343    #[must_use]
344    pub fn node(&self, name: &str) -> Option<Node<'_>> {
345        self.node_by_name(name).map(|id| Node::new(self, id))
346    }
347
348    /// Look up a node only when the numeric OID exactly matches a tree node.
349    ///
350    /// For instance OIDs such as `ifIndex.5`, use [`Mib::lookup_oid`] or
351    /// [`Mib::resolve_node`] instead.
352    #[must_use]
353    pub fn exact_node_by_oid(&self, oid: &Oid) -> Option<Node<'_>> {
354        self.node_by_oid(oid).map(|id| Node::new(self, id))
355    }
356
357    /// Find the deepest node matching a prefix of the OID, starting from root.
358    #[must_use]
359    pub fn longest_prefix_by_oid(&self, oid: &Oid) -> NodeId {
360        self.tree.longest_prefix(oid)
361    }
362
363    /// Look up the deepest node matching a numeric OID prefix as a [`Node`] handle.
364    ///
365    /// This is the handle-oriented entry point for instance OIDs such as
366    /// `ifIndex.5`. Always returns a node (at minimum the root).
367    ///
368    /// When you also need the instance suffix (the arcs after the matched
369    /// node), use [`lookup_instance`](Mib::lookup_instance) instead.
370    #[must_use]
371    pub fn lookup_oid(&self, oid: &Oid) -> Node<'_> {
372        Node::new(self, self.longest_prefix_by_oid(oid))
373    }
374
375    /// Look up a numeric OID and return both the matched node and the
376    /// instance suffix.
377    ///
378    /// This is the standard pattern for processing SNMP varbinds, where
379    /// you need the base node (for metadata, type info, formatting) and
380    /// the trailing arcs (the instance index that identifies which row
381    /// or scalar instance the value belongs to).
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// # fn example_mib() -> mib_rs::Mib {
387    /// #     let source = mib_rs::source::memory(
388    /// #         "DOC-EXAMPLE-MIB",
389    /// #         include_bytes!("../../tests/data/doc-example-mib.txt").as_slice(),
390    /// #     );
391    /// #     mib_rs::Loader::new()
392    /// #         .source(source)
393    /// #         .modules(["DOC-EXAMPLE-MIB"])
394    /// #         .load()
395    /// #         .expect("example MIB should load")
396    /// # }
397    /// let mib = example_mib();
398    /// let instance_oid = mib.resolve_oid("docDescr.7").unwrap();
399    /// let lookup = mib.lookup_instance(&instance_oid);
400    /// assert_eq!(lookup.node().name(), "docDescr");
401    /// assert_eq!(lookup.suffix(), &[7]);
402    /// ```
403    #[must_use]
404    pub fn lookup_instance(&self, oid: &Oid) -> OidLookup<'_> {
405        let id = self.longest_prefix_by_oid(oid);
406        let node_oid = self.tree.oid_of(id);
407        let suffix = oid[node_oid.len()..].to_vec();
408        OidLookup {
409            node: Node::new(self, id),
410            suffix,
411        }
412    }
413
414    /// Depth-first iterator over a subtree rooted at `id`, yielding [`NodeId`]s.
415    pub fn subtree(&self, id: NodeId) -> super::node::SubtreeIter<'_> {
416        self.tree.subtree(id)
417    }
418
419    /// Find the deepest descendant of `start` matching a prefix of `oid`.
420    #[must_use]
421    pub fn longest_prefix_from(&self, start: NodeId, oid: &Oid) -> NodeId {
422        self.tree.longest_prefix_from(start, oid)
423    }
424
425    pub(crate) fn effective_module(&self, id: NodeId) -> Option<ModuleId> {
426        self.tree.get(id).module
427    }
428
429    /// Format a numeric [`Oid`] as `MODULE::name.suffix`.
430    ///
431    /// Uses longest-prefix matching to find the deepest named node, then
432    /// appends any remaining arcs as a dotted numeric suffix. Returns the
433    /// raw numeric string if no named node matches.
434    pub fn format_oid(&self, oid: &Oid) -> String {
435        if oid.is_empty() {
436            return String::new();
437        }
438        let lookup = self.lookup_instance(oid);
439        let matched = self.tree.get(lookup.node.id);
440        if matched.name.is_empty() {
441            return oid.to_string();
442        }
443
444        let mut result = String::new();
445        if let Some(mod_id) = self.effective_module(lookup.node.id) {
446            result.push_str(&self.modules[mod_id.0 as usize].name);
447            result.push_str("::");
448        }
449        result.push_str(&matched.name);
450        for arc in lookup.suffix() {
451            result.push('.');
452            result.push_str(&arc.to_string());
453        }
454        result
455    }
456
457    /// Look up a node by name, qualified name (`MODULE::name`), or OID query,
458    /// returning a [`Node`] handle.
459    ///
460    /// Symbolic and numeric instance OIDs resolve to the deepest matching node
461    /// rather than requiring an exact tree match. See [`Mib::resolve_oid`] for the
462    /// accepted query forms.
463    pub fn resolve_node(&self, query: &str) -> Option<Node<'_>> {
464        self.resolve(query).map(|id| Node::new(self, id))
465    }
466
467    pub(crate) fn resolve(&self, query: &str) -> Option<NodeId> {
468        // Numeric OID
469        let q = query.strip_prefix('.').unwrap_or(query);
470        if q.starts_with(|c: char| c.is_ascii_digit()) {
471            let oid: Oid = q.parse().ok()?;
472            return Some(self.longest_prefix_by_oid(&oid));
473        }
474
475        self.resolve_oid(query)
476            .ok()
477            .map(|oid| self.longest_prefix_by_oid(&oid))
478    }
479
480    /// Convert a symbolic or numeric OID query to a numeric [`Oid`].
481    ///
482    /// Accepted forms include plain names, qualified names, symbolic suffixes,
483    /// and numeric OIDs.
484    ///
485    /// # Errors
486    ///
487    /// Returns [`ResolveOidError`] if the query is empty, the referenced module
488    /// or node name is not found, or a numeric suffix fails to parse.
489    pub fn resolve_oid(&self, query: &str) -> Result<Oid, ResolveOidError> {
490        if query.is_empty() {
491            return Err(ResolveOidError::EmptyQuery);
492        }
493
494        let q = query.strip_prefix('.').unwrap_or(query);
495        if q.starts_with(|c: char| c.is_ascii_digit()) {
496            return q.parse::<Oid>().map_err(ResolveOidError::InvalidOid);
497        }
498
499        // Qualified name: MODULE::name[.suffix]
500        if let Some((mod_name, rest)) = query.split_once("::") {
501            let (name, suffix) = split_name_suffix(rest);
502            let mod_id = self
503                .module_by_name
504                .get(mod_name)
505                .ok_or_else(|| ResolveOidError::ModuleNotFound(mod_name.to_string()))?;
506            let node_id = self.modules[mod_id.0 as usize]
507                .node_by_name(name)
508                .ok_or_else(|| ResolveOidError::QualifiedNodeNotFound {
509                    module: mod_name.to_string(),
510                    name: name.to_string(),
511                })?;
512            let base = self.tree.oid_of(node_id).clone();
513            return append_suffix(base, suffix);
514        }
515
516        // Plain name[.suffix]
517        let (name, suffix) = split_name_suffix(query);
518        let node_id = self
519            .node_by_name(name)
520            .ok_or_else(|| ResolveOidError::NodeNotFound(name.to_string()))?;
521        let base = self.tree.oid_of(node_id).clone();
522        append_suffix(base, suffix)
523    }
524
525    /// Return all symbols defined across all modules.
526    ///
527    /// Iterates modules in load order, yielding each module's definitions.
528    pub fn all_symbols(&self) -> Vec<Symbol> {
529        let mut result = Vec::new();
530        for module in &self.modules {
531            result.extend(module.definitions());
532        }
533        result
534    }
535
536    /// Return all symbols available in a module's scope.
537    ///
538    /// Own definitions come first, then imported symbols resolved from their
539    /// source modules. A name that is both defined and imported is yielded
540    /// only once (the own definition wins).
541    pub fn available_symbols(&self, mod_id: ModuleId) -> Vec<Symbol> {
542        let module = self.module_data(mod_id);
543        let mut result = Vec::new();
544        let mut seen = std::collections::HashSet::new();
545
546        // Own definitions first.
547        for sym in module.definitions() {
548            seen.insert(sym.name(self).to_string());
549            result.push(sym);
550        }
551
552        // Imported symbols in IMPORTS declaration order.
553        for imp in &module.imports {
554            for is in &imp.symbols {
555                if seen.contains(&is.name) {
556                    continue;
557                }
558                seen.insert(is.name.clone());
559                let Some(&source_mod_id) = module.resolved_imports.get(&is.name) else {
560                    continue;
561                };
562                let source = self.module_data(source_mod_id);
563                if let Some(sym) = source.symbol(&is.name) {
564                    result.push(sym);
565                }
566            }
567        }
568
569        result
570    }
571
572    // --- Collection accessors ---
573
574    /// Iterate all resolved modules as [`Module`] handles.
575    ///
576    /// This includes the seven synthetic base modules (SNMPv2-SMI, etc.)
577    /// which are always present. Use [`Module::is_base`] to filter them out
578    /// when you only want user-supplied modules.
579    pub fn modules(&self) -> HandleIter<'_, Module<'_>, impl Iterator<Item = ModuleId>> {
580        HandleIter::new(
581            self,
582            (0..self.modules.len()).map(|i| ModuleId::new(i as u32)),
583        )
584    }
585
586    /// Iterate user-supplied (non-base) modules as [`Module`] handles.
587    ///
588    /// Excludes the seven synthetic base modules. Equivalent to
589    /// `self.modules().filter(|m| !m.is_base())` but more convenient.
590    pub fn user_modules(&self) -> impl Iterator<Item = Module<'_>> + '_ {
591        self.modules
592            .iter()
593            .enumerate()
594            .filter(|(_, m)| !m.is_base())
595            .map(|(i, _)| Module::new(self, ModuleId::new(i as u32)))
596    }
597
598    /// Iterate all resolved objects as [`Object`] handles.
599    ///
600    /// Includes objects from base modules. Use [`Object::module`] and
601    /// [`Module::is_base`] to filter if needed.
602    pub fn objects(&self) -> HandleIter<'_, Object<'_>, impl Iterator<Item = ObjectId>> {
603        HandleIter::new(
604            self,
605            (0..self.objects.len()).map(|i| ObjectId::new(i as u32)),
606        )
607    }
608
609    /// Iterate all resolved types as [`Type`] handles.
610    ///
611    /// Includes types from base modules (e.g. `Counter32`, `DisplayString`).
612    /// Use [`Type::module`] and [`Module::is_base`] to filter if needed.
613    pub fn types(&self) -> HandleIter<'_, Type<'_>, impl Iterator<Item = TypeId>> {
614        HandleIter::new(self, (0..self.types.len()).map(|i| TypeId::new(i as u32)))
615    }
616
617    /// Iterate all OID tree nodes (excluding root) as [`Node`] handles.
618    ///
619    /// Includes nodes from base modules (e.g. `iso`, `internet`, `enterprises`).
620    /// Use [`Node::module`] and [`Module::is_base`] to filter if needed.
621    pub fn nodes(&self) -> HandleIter<'_, Node<'_>, impl Iterator<Item = NodeId>> {
622        HandleIter::new(self, self.tree.all_nodes())
623    }
624
625    /// Iterate all resolved notifications as [`Notification`] handles.
626    pub fn notifications(
627        &self,
628    ) -> HandleIter<'_, Notification<'_>, impl Iterator<Item = NotificationId>> {
629        HandleIter::new(
630            self,
631            (0..self.notifications.len()).map(|i| NotificationId::new(i as u32)),
632        )
633    }
634
635    /// Iterate all resolved groups as [`Group`] handles.
636    pub fn groups(&self) -> HandleIter<'_, Group<'_>, impl Iterator<Item = GroupId>> {
637        HandleIter::new(self, (0..self.groups.len()).map(|i| GroupId::new(i as u32)))
638    }
639
640    /// Iterate all resolved compliance statements as [`Compliance`] handles.
641    pub fn compliances(
642        &self,
643    ) -> HandleIter<'_, Compliance<'_>, impl Iterator<Item = ComplianceId>> {
644        HandleIter::new(
645            self,
646            (0..self.compliances.len()).map(|i| ComplianceId::new(i as u32)),
647        )
648    }
649
650    /// Iterate all resolved capability statements as [`Capability`] handles.
651    pub fn capabilities(
652        &self,
653    ) -> HandleIter<'_, Capability<'_>, impl Iterator<Item = CapabilityId>> {
654        HandleIter::new(
655            self,
656            (0..self.capabilities.len()).map(|i| CapabilityId::new(i as u32)),
657        )
658    }
659
660    /// Return a [`Node`] handle for the given [`NodeId`].
661    pub fn node_by_id(&self, id: NodeId) -> Node<'_> {
662        Node::new(self, id)
663    }
664
665    /// Return an [`Object`] handle for the given [`ObjectId`].
666    pub fn object_by_id(&self, id: ObjectId) -> Object<'_> {
667        Object::new(self, id)
668    }
669
670    /// Return a [`Type`] handle for the given [`TypeId`].
671    pub fn type_by_id(&self, id: TypeId) -> Type<'_> {
672        Type::new(self, id)
673    }
674
675    /// Return a [`Module`] handle for the given [`ModuleId`].
676    pub fn module_by_id(&self, id: ModuleId) -> Module<'_> {
677        Module::new(self, id)
678    }
679
680    /// Return a [`Notification`] handle for the given [`NotificationId`].
681    pub fn notification_by_id(&self, id: NotificationId) -> Notification<'_> {
682        Notification::new(self, id)
683    }
684
685    /// Return a [`Group`] handle for the given [`GroupId`].
686    pub fn group_by_id(&self, id: GroupId) -> Group<'_> {
687        Group::new(self, id)
688    }
689
690    /// Return a [`Compliance`] handle for the given [`ComplianceId`].
691    pub fn compliance_by_id(&self, id: ComplianceId) -> Compliance<'_> {
692        Compliance::new(self, id)
693    }
694
695    /// Return a [`Capability`] handle for the given [`CapabilityId`].
696    pub fn capability_by_id(&self, id: CapabilityId) -> Capability<'_> {
697        Capability::new(self, id)
698    }
699
700    pub(crate) fn modules_slice(&self) -> &[ModuleData] {
701        &self.modules
702    }
703
704    pub(crate) fn objects_slice(&self) -> &[ObjectData] {
705        &self.objects
706    }
707
708    pub(crate) fn types_slice(&self) -> &[TypeData] {
709        &self.types
710    }
711
712    pub(crate) fn notifications_slice(&self) -> &[NotificationData] {
713        &self.notifications
714    }
715
716    pub(crate) fn groups_slice(&self) -> &[GroupData] {
717        &self.groups
718    }
719
720    pub(crate) fn compliances_slice(&self) -> &[ComplianceData] {
721        &self.compliances
722    }
723
724    pub(crate) fn capabilities_slice(&self) -> &[CapabilityData] {
725        &self.capabilities
726    }
727
728    /// Return all non-base modules that define a symbol with the given name.
729    ///
730    /// Synthetic base modules (SNMPv2-SMI, etc.) are excluded from the results.
731    pub fn modules_defining(&self, name: &str) -> Vec<ModuleId> {
732        self.modules
733            .iter()
734            .enumerate()
735            .filter(|(_, m)| !m.is_base && m.defines_symbol(name))
736            .map(|(i, _)| ModuleId::new(i as u32))
737            .collect()
738    }
739
740    /// Return all non-base modules that import a symbol with the given name.
741    ///
742    /// Synthetic base modules (SNMPv2-SMI, etc.) are excluded from the results.
743    pub fn modules_importing(&self, name: &str) -> Vec<ModuleId> {
744        self.modules
745            .iter()
746            .enumerate()
747            .filter(|(_, m)| !m.is_base && m.imports_symbol(name))
748            .map(|(i, _)| ModuleId::new(i as u32))
749            .collect()
750    }
751
752    /// Return all objects whose OID tree node has the given [`Kind`].
753    pub fn objects_by_kind(&self, kind: Kind) -> Vec<ObjectId> {
754        self.objects
755            .iter()
756            .enumerate()
757            .filter(|(_, obj)| {
758                obj.entity
759                    .node
760                    .is_some_and(|id| self.tree.get(id).kind == kind)
761            })
762            .map(|(i, _)| ObjectId::new(i as u32))
763            .collect()
764    }
765
766    /// Iterate all table objects as [`Object`] handles.
767    pub fn tables(&self) -> impl Iterator<Item = Object<'_>> + '_ {
768        self.objects_with_kind(Kind::Table)
769    }
770
771    /// Iterate all scalar objects as [`Object`] handles.
772    pub fn scalars(&self) -> impl Iterator<Item = Object<'_>> + '_ {
773        self.objects_with_kind(Kind::Scalar)
774    }
775
776    /// Iterate all column objects as [`Object`] handles.
777    pub fn columns(&self) -> impl Iterator<Item = Object<'_>> + '_ {
778        self.objects_with_kind(Kind::Column)
779    }
780
781    /// Iterate all row (entry) objects as [`Object`] handles.
782    pub fn rows(&self) -> impl Iterator<Item = Object<'_>> + '_ {
783        self.objects_with_kind(Kind::Row)
784    }
785
786    /// Return all objects whose resolved type has the given name.
787    pub fn objects_by_type_name(&self, type_name: &str) -> Vec<ObjectId> {
788        self.objects
789            .iter()
790            .enumerate()
791            .filter(|(_, obj)| {
792                obj.typ
793                    .is_some_and(|id| self.types[id.0 as usize].name == type_name)
794            })
795            .map(|(i, _)| ObjectId::new(i as u32))
796            .collect()
797    }
798
799    /// Return all objects whose effective [`BaseType`] matches.
800    pub fn objects_by_base_type(&self, base: BaseType) -> Vec<ObjectId> {
801        self.objects
802            .iter()
803            .enumerate()
804            .filter(|(_, obj)| {
805                obj.typ
806                    .is_some_and(|id| self.types[id.0 as usize].effective_base(&self.types) == base)
807            })
808            .map(|(i, _)| ObjectId::new(i as u32))
809            .collect()
810    }
811
812    // --- Object table navigation ---
813
814    /// Return the containing table object for a table, row, or column.
815    ///
816    /// Tables return themselves. Scalars and non-tabular objects return `None`.
817    pub fn object_table(&self, id: ObjectId) -> Option<ObjectId> {
818        let node_id = self.object_data(id).node()?;
819        let node = self.tree.get(node_id);
820        match node.kind {
821            Kind::Table => Some(id),
822            Kind::Row => {
823                let parent = self.tree.get(node.parent?);
824                parent.object
825            }
826            Kind::Column => {
827                let parent = self.tree.get(node.parent?);
828                let grandparent = self.tree.get(parent.parent?);
829                grandparent.object
830            }
831            _ => None,
832        }
833    }
834
835    /// Return the associated row object for a table, row, or column.
836    ///
837    /// Tables return their child row entry. Rows return themselves. Columns
838    /// return their parent row. Scalars and non-tabular objects return `None`.
839    pub fn object_row(&self, id: ObjectId) -> Option<ObjectId> {
840        let node_id = self.object_data(id).node()?;
841        let node = self.tree.get(node_id);
842        match node.kind {
843            Kind::Table => {
844                for &child_id in node.children.values() {
845                    let child = self.tree.get(child_id);
846                    if child.kind == Kind::Row {
847                        return child.object;
848                    }
849                }
850                None
851            }
852            Kind::Row => Some(id),
853            Kind::Column => {
854                let parent = self.tree.get(node.parent?);
855                parent.object
856            }
857            _ => None,
858        }
859    }
860
861    /// Return column objects for a table or row in arc order.
862    ///
863    /// Non-tabular objects return an empty vector.
864    pub fn object_columns(&self, id: ObjectId) -> Vec<ObjectId> {
865        let Some(node_id) = self.object_data(id).node() else {
866            return Vec::new();
867        };
868        let node = self.tree.get(node_id);
869        let row_node = match node.kind {
870            Kind::Table => {
871                let mut found = None;
872                for &child_id in node.children.values() {
873                    if self.tree.get(child_id).kind == Kind::Row {
874                        found = Some(child_id);
875                        break;
876                    }
877                }
878                match found {
879                    Some(id) => self.tree.get(id),
880                    None => return Vec::new(),
881                }
882            }
883            Kind::Row => node,
884            _ => return Vec::new(),
885        };
886        let mut cols = Vec::new();
887        for &child_id in row_node.children.values() {
888            let child = self.tree.get(child_id);
889            if let Some(obj_id) = child.object.filter(|_| child.kind == Kind::Column) {
890                cols.push(obj_id);
891            }
892        }
893        cols
894    }
895
896    /// Return [`IndexEntry`] items for a row, following the AUGMENTS chain if needed.
897    ///
898    /// Returns an empty vector for non-row objects. For handle-level access,
899    /// see [`Object::effective_indexes`](super::handle::Object::effective_indexes).
900    pub fn effective_indexes(&self, id: ObjectId) -> Vec<IndexEntry> {
901        let mut visited = Vec::new();
902        self.effective_indexes_inner(id, &mut visited)
903    }
904
905    pub(crate) fn effective_indexes_source(&self, id: ObjectId) -> Option<ObjectId> {
906        let mut visited = Vec::new();
907        self.effective_indexes_source_inner(id, &mut visited)
908    }
909
910    fn effective_indexes_inner(
911        &self,
912        id: ObjectId,
913        visited: &mut Vec<ObjectId>,
914    ) -> Vec<IndexEntry> {
915        let obj = self.object_data(id);
916        let Some(node_id) = obj.node() else {
917            return Vec::new();
918        };
919        let kind = self.tree.get(node_id).kind;
920        if kind == Kind::Column {
921            if let Some(row_id) = self.object_row(id) {
922                return self.effective_indexes_inner(row_id, visited);
923            }
924            return Vec::new();
925        }
926        if kind != Kind::Row {
927            return Vec::new();
928        }
929        if !obj.index.is_empty() {
930            return obj.index.clone();
931        }
932        if let Some(aug_id) = obj.augments {
933            if visited.contains(&id) {
934                return Vec::new();
935            }
936            visited.push(id);
937            return self.effective_indexes_inner(aug_id, visited);
938        }
939        Vec::new()
940    }
941
942    fn effective_indexes_source_inner(
943        &self,
944        id: ObjectId,
945        visited: &mut Vec<ObjectId>,
946    ) -> Option<ObjectId> {
947        let obj = self.object_data(id);
948        let node_id = obj.node()?;
949        let kind = self.tree.get(node_id).kind;
950        if kind == Kind::Column {
951            let row_id = self.object_row(id)?;
952            return self.effective_indexes_source_inner(row_id, visited);
953        }
954        if kind != Kind::Row {
955            return None;
956        }
957        if !obj.index.is_empty() {
958            return Some(id);
959        }
960        let aug_id = obj.augments?;
961        if visited.contains(&id) {
962            return None;
963        }
964        visited.push(id);
965        self.effective_indexes_source_inner(aug_id, visited)
966    }
967
968    // --- Object kind predicates ---
969
970    /// Return `true` if the object is a table.
971    pub fn is_table(&self, id: ObjectId) -> bool {
972        self.object_kind(id) == Kind::Table
973    }
974
975    /// Return `true` if the object is a table row (entry).
976    pub fn is_row(&self, id: ObjectId) -> bool {
977        self.object_kind(id) == Kind::Row
978    }
979
980    /// Return `true` if the object is a table column.
981    pub fn is_column(&self, id: ObjectId) -> bool {
982        self.object_kind(id) == Kind::Column
983    }
984
985    /// Return `true` if the object is a scalar.
986    pub fn is_scalar(&self, id: ObjectId) -> bool {
987        self.object_kind(id) == Kind::Scalar
988    }
989
990    /// Return `true` if a column appears in its parent row's effective indexes.
991    pub fn is_index(&self, id: ObjectId) -> bool {
992        if self.object_kind(id) != Kind::Column {
993            return false;
994        }
995        self.effective_indexes(id)
996            .iter()
997            .any(|idx| idx.object == Some(id))
998    }
999
1000    fn objects_with_kind(&self, kind: Kind) -> impl Iterator<Item = Object<'_>> + '_ {
1001        self.objects.iter().enumerate().filter_map(move |(i, obj)| {
1002            let node_id = obj.entity.node?;
1003            if self.tree.get(node_id).kind == kind {
1004                Some(Object::new(self, ObjectId::new(i as u32)))
1005            } else {
1006                None
1007            }
1008        })
1009    }
1010
1011    fn object_kind(&self, id: ObjectId) -> Kind {
1012        match self.object_data(id).node() {
1013            Some(node_id) => self.tree.get(node_id).kind,
1014            None => Kind::Unknown,
1015        }
1016    }
1017
1018    // --- Diagnostics ---
1019
1020    /// Return the total number of OID tree nodes created during resolution.
1021    pub fn node_count(&self) -> usize {
1022        self.node_count
1023    }
1024
1025    /// Return all diagnostics collected during loading and resolution.
1026    pub fn diagnostics(&self) -> &[Diagnostic] {
1027        &self.diagnostics
1028    }
1029
1030    /// Return all unresolved symbol references collected during resolution.
1031    pub fn unresolved(&self) -> &[UnresolvedRef] {
1032        &self.unresolved
1033    }
1034
1035    /// Return `true` if any diagnostic has [`Severity::Error`] or higher.
1036    pub fn has_errors(&self) -> bool {
1037        self.diagnostics
1038            .iter()
1039            .any(|d| d.severity.at_least(Severity::Error))
1040    }
1041
1042    // --- Builder methods (used by resolver) ---
1043
1044    pub(crate) fn set_node_count(&mut self, n: usize) {
1045        self.node_count = n;
1046    }
1047
1048    pub(crate) fn add_module(&mut self, data: ModuleData) -> ModuleId {
1049        let id = ModuleId::new(self.modules.len() as u32);
1050        if !data.name.is_empty() {
1051            self.module_by_name.insert(data.name.clone(), id);
1052        }
1053        self.modules.push(data);
1054        id
1055    }
1056
1057    pub(crate) fn add_object(&mut self, data: ObjectData) -> ObjectId {
1058        let id = ObjectId::new(self.objects.len() as u32);
1059        self.objects.push(data);
1060        id
1061    }
1062
1063    pub(crate) fn add_type(&mut self, data: TypeData) -> TypeId {
1064        let id = TypeId::new(self.types.len() as u32);
1065        if !data.name.is_empty() && !self.type_by_name.contains_key(&data.name) {
1066            self.type_by_name.insert(data.name.clone(), id);
1067        }
1068        self.types.push(data);
1069        id
1070    }
1071
1072    pub(crate) fn add_notification(&mut self, data: NotificationData) -> NotificationId {
1073        let id = NotificationId::new(self.notifications.len() as u32);
1074        self.notifications.push(data);
1075        id
1076    }
1077
1078    pub(crate) fn add_group(&mut self, data: GroupData) -> GroupId {
1079        let id = GroupId::new(self.groups.len() as u32);
1080        self.groups.push(data);
1081        id
1082    }
1083
1084    pub(crate) fn add_compliance(&mut self, data: ComplianceData) -> ComplianceId {
1085        let id = ComplianceId::new(self.compliances.len() as u32);
1086        self.compliances.push(data);
1087        id
1088    }
1089
1090    pub(crate) fn add_capability(&mut self, data: CapabilityData) -> CapabilityId {
1091        let id = CapabilityId::new(self.capabilities.len() as u32);
1092        self.capabilities.push(data);
1093        id
1094    }
1095
1096    pub(crate) fn register_node(&mut self, name: &str, id: NodeId) {
1097        if !name.is_empty() {
1098            self.name_to_nodes
1099                .entry(name.to_string())
1100                .or_default()
1101                .push(id);
1102        }
1103    }
1104
1105    pub(crate) fn add_diagnostic(&mut self, d: Diagnostic) {
1106        self.diagnostics.push(d);
1107    }
1108
1109    pub(crate) fn add_unresolved(&mut self, r: UnresolvedRef) {
1110        self.unresolved.push(r);
1111    }
1112}
1113
1114impl Default for Mib {
1115    fn default() -> Self {
1116        Self::new()
1117    }
1118}
1119
1120impl fmt::Debug for Mib {
1121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1122        f.debug_struct("Mib")
1123            .field("modules", &self.modules.len())
1124            .field("objects", &self.objects.len())
1125            .field("types", &self.types.len())
1126            .field("notifications", &self.notifications.len())
1127            .field("node_count", &self.node_count)
1128            .finish()
1129    }
1130}
1131
1132fn split_name_suffix(s: &str) -> (&str, &str) {
1133    match s.find('.') {
1134        Some(i) => (&s[..i], &s[i..]),
1135        None => (s, ""),
1136    }
1137}
1138
1139fn append_suffix(base: Oid, suffix: &str) -> Result<Oid, ResolveOidError> {
1140    if suffix.is_empty() {
1141        return Ok(base);
1142    }
1143    let extra: Oid = suffix
1144        .parse()
1145        .map_err(|source| ResolveOidError::InvalidSuffix {
1146            suffix: suffix.to_string(),
1147            source,
1148        })?;
1149    Ok(base.child_oid(&extra))
1150}
1151
1152/// Result of [`Mib::lookup_instance`], containing the matched node and
1153/// any remaining instance suffix arcs.
1154pub struct OidLookup<'a> {
1155    node: Node<'a>,
1156    suffix: Vec<u32>,
1157}
1158
1159impl<'a> OidLookup<'a> {
1160    /// The deepest tree node matching the OID prefix.
1161    #[must_use]
1162    pub fn node(&self) -> Node<'a> {
1163        self.node
1164    }
1165
1166    /// The instance suffix: arcs from the input OID that follow the
1167    /// matched node's OID.
1168    ///
1169    /// Empty when the input OID exactly matches a tree node.
1170    #[must_use]
1171    pub fn suffix(&self) -> &[u32] {
1172        &self.suffix
1173    }
1174
1175    /// Decode the instance suffix into typed index values.
1176    ///
1177    /// Uses the matched node's row INDEX clause to interpret the suffix
1178    /// arcs per RFC 2578 section 7.7. Returns an empty Vec if the node
1179    /// has no associated object, is not part of a table, or the row has
1180    /// no index definitions.
1181    ///
1182    /// See [`index::decode_suffix`](super::index::decode_suffix) for
1183    /// the standalone function and encoding details.
1184    #[must_use]
1185    pub fn decode_indexes(&self) -> Vec<super::index::DecodedIndex> {
1186        let Some(obj) = self.node.object() else {
1187            return Vec::new();
1188        };
1189        let mut indexes = obj.effective_indexes().peekable();
1190        if indexes.peek().is_none() {
1191            return Vec::new();
1192        }
1193        super::index::decode_suffix(indexes, &self.suffix)
1194    }
1195}
1196
1197/// Error returned by [`Mib::resolve_oid`] when a query cannot be resolved.
1198#[derive(Debug, Clone, thiserror::Error)]
1199pub enum ResolveOidError {
1200    /// The query string was empty.
1201    #[error("empty query")]
1202    EmptyQuery,
1203    /// The query looked numeric but could not be parsed as a valid OID.
1204    #[error("invalid OID: {0}")]
1205    InvalidOid(#[source] ParseOidError),
1206    /// The module part of a qualified name was not found.
1207    #[error("module not found: {0}")]
1208    ModuleNotFound(String),
1209    /// The plain name was not found in any loaded module.
1210    #[error("node not found: {0}")]
1211    NodeNotFound(String),
1212    /// The name was not found within the specified module.
1213    #[error("node not found: {module}::{name}")]
1214    QualifiedNodeNotFound {
1215        /// Module name from the query.
1216        module: String,
1217        /// Node name from the query.
1218        name: String,
1219    },
1220    /// The trailing instance suffix could not be parsed as numeric arcs.
1221    #[error("invalid instance suffix {suffix:?}: {source}")]
1222    InvalidSuffix {
1223        /// The suffix string that failed to parse.
1224        suffix: String,
1225        #[source]
1226        source: ParseOidError,
1227    },
1228}
1229
1230// Extension method for building child OIDs with multiple arcs.
1231impl Oid {
1232    fn child_oid(&self, suffix: &Oid) -> Oid {
1233        let mut arcs = Vec::with_capacity(self.len() + suffix.len());
1234        arcs.extend_from_slice(self);
1235        arcs.extend_from_slice(suffix);
1236        Oid::from(arcs)
1237    }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::*;
1243    use crate::mib::module::ModuleData;
1244    use crate::mib::object::ObjectData;
1245    use crate::mib::typedef::TypeData;
1246
1247    fn make_mib_with_two_modules() -> Mib {
1248        let mut mib = Mib::new();
1249
1250        // Module A with an object and a type.
1251        let mut mod_a = ModuleData::new("MOD-A".to_string());
1252        let obj_data = ObjectData::new("objA".to_string());
1253        let obj_id = mib.add_object(obj_data);
1254        mod_a.add_object("objA", obj_id);
1255
1256        let type_data = TypeData::new("TypeA".to_string());
1257        let type_id = mib.add_type(type_data);
1258        mod_a.add_type("TypeA", type_id);
1259        let _mod_a_id = mib.add_module(mod_a);
1260
1261        // Module B with an object.
1262        let mut mod_b = ModuleData::new("MOD-B".to_string());
1263        let obj_data2 = ObjectData::new("objB".to_string());
1264        let obj_id2 = mib.add_object(obj_data2);
1265        mod_b.add_object("objB", obj_id2);
1266        let _mod_b_id = mib.add_module(mod_b);
1267
1268        mib
1269    }
1270
1271    #[test]
1272    fn all_symbols_across_modules() {
1273        let mib = make_mib_with_two_modules();
1274        let syms = mib.all_symbols();
1275        let names: Vec<&str> = syms.iter().map(|s| s.name(&mib)).collect();
1276
1277        assert!(names.contains(&"objA"));
1278        assert!(names.contains(&"TypeA"));
1279        assert!(names.contains(&"objB"));
1280        assert_eq!(names.len(), 3);
1281    }
1282
1283    #[test]
1284    fn available_symbols_own_only() {
1285        let mib = make_mib_with_two_modules();
1286        let mod_a_id = *mib.module_by_name.get("MOD-A").unwrap();
1287
1288        let syms = mib.available_symbols(mod_a_id);
1289        let names: Vec<&str> = syms.iter().map(|s| s.name(&mib)).collect();
1290
1291        assert!(names.contains(&"objA"));
1292        assert!(names.contains(&"TypeA"));
1293        assert!(!names.contains(&"objB"));
1294    }
1295
1296    #[test]
1297    fn available_symbols_with_imports() {
1298        let mut mib = Mib::new();
1299
1300        // Source module with an object.
1301        let mut source_mod = ModuleData::new("SOURCE-MIB".to_string());
1302        let obj_data = ObjectData::new("srcObj".to_string());
1303        let obj_id = mib.add_object(obj_data);
1304        source_mod.add_object("srcObj", obj_id);
1305        let source_mod_id = mib.add_module(source_mod);
1306
1307        // Consumer module that imports srcObj.
1308        let mut consumer = ModuleData::new("CONSUMER-MIB".to_string());
1309        let own_type = TypeData::new("OwnType".to_string());
1310        let own_type_id = mib.add_type(own_type);
1311        consumer.add_type("OwnType", own_type_id);
1312        consumer.imports.push(crate::mib::types::Import {
1313            module: "SOURCE-MIB".to_string(),
1314            symbols: vec![crate::mib::types::ImportSymbol {
1315                name: "srcObj".to_string(),
1316                span: crate::types::Span::SYNTHETIC,
1317            }],
1318        });
1319        consumer
1320            .resolved_imports
1321            .insert("srcObj".to_string(), source_mod_id);
1322        let consumer_id = mib.add_module(consumer);
1323
1324        let syms = mib.available_symbols(consumer_id);
1325        let names: Vec<&str> = syms.iter().map(|s| s.name(&mib)).collect();
1326
1327        assert_eq!(names, vec!["OwnType", "srcObj"]);
1328    }
1329
1330    #[test]
1331    fn available_symbols_dedup_own_over_import() {
1332        let mut mib = Mib::new();
1333
1334        // Source module defines "shared".
1335        let mut source_mod = ModuleData::new("SOURCE-MIB".to_string());
1336        let src_type = TypeData::new("shared".to_string());
1337        let src_type_id = mib.add_type(src_type);
1338        source_mod.add_type("shared", src_type_id);
1339        let source_mod_id = mib.add_module(source_mod);
1340
1341        // Consumer also defines "shared" and imports it.
1342        let mut consumer = ModuleData::new("CONSUMER-MIB".to_string());
1343        let own_type = TypeData::new("shared".to_string());
1344        let own_type_id = mib.add_type(own_type);
1345        consumer.add_type("shared", own_type_id);
1346        consumer.imports.push(crate::mib::types::Import {
1347            module: "SOURCE-MIB".to_string(),
1348            symbols: vec![crate::mib::types::ImportSymbol {
1349                name: "shared".to_string(),
1350                span: crate::types::Span::SYNTHETIC,
1351            }],
1352        });
1353        consumer
1354            .resolved_imports
1355            .insert("shared".to_string(), source_mod_id);
1356        let consumer_id = mib.add_module(consumer);
1357
1358        let syms = mib.available_symbols(consumer_id);
1359        let names: Vec<&str> = syms.iter().map(|s| s.name(&mib)).collect();
1360
1361        // "shared" appears only once (own definition wins).
1362        assert_eq!(names, vec!["shared"]);
1363    }
1364}