Files
nmon/internal/mib/loader.go
jleung 287fbb98bd Maintainability cleanup (core libraries): 223 lint findings to 0, oversized files split; fix apc-ups label columns and never poll text metrics
Migration files restructured with shared helpers; verified byte-identical SQLite schema vs previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 19:11:15 +00:00

425 lines
11 KiB
Go

package mib
import (
"errors"
"fmt"
"os"
"sort"
"strconv"
"strings"
"sync"
)
// Loader finds MIB modules on a search path (earlier directories override later
// ones when several files define the same module), parses them on demand together with
// the modules they import, and resolves names to numeric OIDs. It is safe for
// concurrent use.
type Loader struct {
paths []string
mu sync.Mutex
indexed bool
index map[string][]string // module name -> candidate files, best first
files map[string][]*Module
mods map[string]*Module
loading map[string]bool
byOID map[string]*Object
byName map[string][]*Object
types map[string][]*typeDef // global fallback for type names
typeMods map[*typeDef]*Module
noDesc bool
}
// NewLoader returns a Loader that searches the given directories recursively.
// Nothing is read until the first call that needs a module.
func NewLoader(searchPath ...string) *Loader {
return &Loader{
paths: searchPath,
files: map[string][]*Module{},
mods: map[string]*Module{},
loading: map[string]bool{},
byOID: map[string]*Object{},
byName: map[string][]*Object{},
types: map[string][]*typeDef{},
typeMods: map[*typeDef]*Module{},
}
}
// DropDescriptions makes the Loader discard DESCRIPTION text, which saves a lot
// of memory when loading thousands of modules. Call it before the first Load.
func (l *Loader) DropDescriptions() { l.noDesc = true }
// Modules returns the names of every module found on the search path, sorted.
func (l *Loader) Modules() ([]string, error) {
l.mu.Lock()
defer l.mu.Unlock()
if err := l.buildIndex(); err != nil {
return nil, err
}
out := make([]string, 0, len(l.index))
for n := range l.index {
out = append(out, n)
}
sort.Strings(out)
return out, nil
}
// File returns the file chosen for a module, or "" when it is not on the path.
func (l *Loader) File(module string) string {
l.mu.Lock()
defer l.mu.Unlock()
if l.buildIndex() != nil {
return ""
}
if c := l.index[module]; len(c) > 0 {
return c[0]
}
return ""
}
// ErrNotFound is returned (wrapped) when a module or object does not exist.
var ErrNotFound = errors.New("mib: not found")
// Load parses a module and everything it imports. Imported modules that are not
// on the search path are recorded in Module.Missing rather than failing the
// load; names that depend on them stay unresolved.
func (l *Loader) Load(module string) (*Module, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.load(module)
}
func (l *Loader) load(name string) (*Module, error) {
if m, ok := l.mods[name]; ok {
return m, nil
}
if err := l.buildIndex(); err != nil {
return nil, err
}
m, err := l.parseCandidates(name, l.index[name])
if err != nil {
return nil, err
}
l.register(m)
l.loading[name] = true
err = l.loadImports(m)
delete(l.loading, name)
if err != nil {
delete(l.mods, name)
return nil, err
}
l.finalize(m)
return m, nil
}
// parseCandidates returns the first parsable candidate file's definition of module name.
func (l *Loader) parseCandidates(name string, cands []string) (*Module, error) {
var firstErr error
for _, f := range cands {
mods, err := l.parseFile(f)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
for _, x := range mods {
if x.Name == name {
return x, nil
}
}
}
if firstErr == nil {
firstErr = fmt.Errorf("%w: module %s", ErrNotFound, name)
}
return nil, firstErr
}
// loadImports loads what m imports. A missing import is recorded in m.Missing;
// one that exists but does not parse is an error.
func (l *Loader) loadImports(m *Module) error {
for _, imp := range m.Imports {
if l.loading[imp.Module] {
continue
}
if _, err := l.load(imp.Module); err != nil {
if errors.Is(err, ErrNotFound) {
m.Missing = appendUnique(m.Missing, imp.Module)
continue
}
return fmt.Errorf("mib: %s imports %s: %w", m.Name, imp.Module, err)
}
}
return nil
}
func appendUnique(s []string, v string) []string {
for _, x := range s {
if x == v {
return s
}
}
return append(s, v)
}
func (l *Loader) parseFile(path string) ([]*Module, error) {
if m, ok := l.files[path]; ok {
return m, nil
}
src, err := os.ReadFile(path)
if err != nil {
return nil, err
}
mods, err := parse(path, src, !l.noDesc)
if err != nil {
return nil, err
}
l.files[path] = mods
return mods, nil
}
// register makes a module's names visible for global fallback lookups.
func (l *Loader) register(m *Module) {
l.mods[m.Name] = m
for _, o := range m.Objects {
l.byName[o.Name] = append(l.byName[o.Name], o)
}
for _, t := range m.types {
l.types[t.name] = append(l.types[t.name], t)
l.typeMods[t] = m
}
}
var roots = map[string]int{"iso": 1, "ccitt": 0, "joint-iso-ccitt": 2}
// wellKnown covers the SMI roots for modules whose imports are missing.
var wellKnown = map[string]string{
"iso": "1", "org": "1.3", "dod": "1.3.6", "internet": "1.3.6.1", "directory": "1.3.6.1.1",
"mgmt": "1.3.6.1.2", "mib-2": "1.3.6.1.2.1", "transmission": "1.3.6.1.2.1.10", "experimental": "1.3.6.1.3",
"private": "1.3.6.1.4", "enterprises": "1.3.6.1.4.1", "security": "1.3.6.1.5", "snmpV2": "1.3.6.1.6",
"snmpModules": "1.3.6.1.6.3", "snmpDomains": "1.3.6.1.6.1", "snmpProxys": "1.3.6.1.6.2",
"zeroDotZero": "0.0",
}
// lookupName finds a definition visible from module m: its own, then its
// imports (transitively), then the well-known SMI names, then any loaded module.
func (l *Loader) lookupName(m *Module, name string) *Object {
if mod, n, ok := strings.Cut(name, "."); ok {
if x, err := l.load(mod); err == nil {
if o := l.lookupIn(x, n, map[*Module]bool{}); o != nil {
return o
}
}
name = n
}
if o := l.lookupIn(m, name, map[*Module]bool{}); o != nil {
return o
}
if s, ok := wellKnown[name]; ok {
return &Object{Name: name, Module: "SNMPv2-SMI", OID: s, state: 2, Kind: KindNode}
}
if c := l.byName[name]; len(c) > 0 {
return c[0]
}
return nil
}
func (l *Loader) lookupIn(m *Module, name string, seen map[*Module]bool) *Object {
if seen[m] {
return nil
}
seen[m] = true
if o := m.byName[name]; o != nil {
return o
}
// prefer the module named in the import list
for _, imp := range m.Imports {
for _, n := range imp.Names {
if n == name {
if x := l.mods[imp.Module]; x != nil {
if o := l.lookupIn(x, name, seen); o != nil {
return o
}
}
}
}
}
return nil
}
// oidOf computes (and memoises) an object's numeric OID.
func (l *Loader) oidOf(o *Object) (string, error) {
switch o.state {
case 2:
return o.OID, o.rerr
case 1:
return "", fmt.Errorf("circular definition of %s", o.Name)
}
o.state = 1
oid, err := l.compute(o)
o.state = 2
o.OID, o.rerr = oid, err
return oid, err
}
func (l *Loader) compute(o *Object) (string, error) {
var parts []string
for i, e := range o.def {
var part string
var err error
switch {
case e.name == "" || (i > 0 && e.hasNum):
part = strconv.Itoa(e.num)
case i == 0:
part, err = l.firstArc(o, e)
default: // a bare name after the first: its last arc
part, err = l.laterArc(o, e)
}
if err != nil {
return "", err
}
parts = append(parts, part)
}
return strings.Join(parts, "."), nil
}
// firstArc resolves the leading named element of an OID: an SMI root or the full OID of its parent.
func (l *Loader) firstArc(o *Object, e oidElem) (string, error) {
if r, ok := roots[e.name]; ok {
return strconv.Itoa(r), nil
}
t := l.lookupName(o.mod, e.name)
if t == nil {
return "", fmt.Errorf("%s: unknown parent %q", o.Name, e.name)
}
s, err := l.oidOf(t)
if err != nil {
return "", err
}
if s == "" {
return "", fmt.Errorf("%s: parent %q has no OID", o.Name, e.name)
}
return s, nil
}
// laterArc resolves a bare name after the first element to its last arc.
func (l *Loader) laterArc(o *Object, e oidElem) (string, error) {
t := l.lookupName(o.mod, e.name)
if t == nil {
return "", fmt.Errorf("%s: unknown component %q", o.Name, e.name)
}
s, err := l.oidOf(t)
if err != nil || s == "" {
return "", fmt.Errorf("%s: component %q unresolved", o.Name, e.name)
}
return s[strings.LastIndexByte(s, '.')+1:], nil
}
// finalize resolves every object's OID and type information and classifies rows and columns.
func (l *Loader) finalize(m *Module) {
local := l.resolveObjects(m)
parent := func(oid string) *Object {
i := strings.LastIndexByte(oid, '.')
if i < 0 {
return nil
}
return local[oid[:i]]
}
linkRows(m, parent)
linkColumns(m, parent)
// A scalar-looking object with children is just a node (rare, e.g. aggregate without SYNTAX).
for _, o := range m.Objects {
if o.Kind == KindScalar && o.syn == nil {
o.Kind = KindNode
}
}
}
// linkRows names each row's table; it must run before linkColumns, which copies that name.
func linkRows(m *Module, parent func(string) *Object) {
for _, o := range m.Objects {
if o.OID == "" || o.Kind != KindRow {
continue
}
if t := parent(o.OID); t != nil && t.Kind == KindTable {
o.Table = t.Name
}
}
}
// linkColumns turns a scalar whose parent is a row into a column of that row's table.
func linkColumns(m *Module, parent func(string) *Object) {
for _, o := range m.Objects {
if o.OID == "" || o.Kind != KindScalar {
continue
}
if r := parent(o.OID); r != nil && r.Kind == KindRow {
o.Kind, o.Table = KindColumn, r.Table
}
}
}
// resolveObjects computes every object's OID and type info; it returns the module's objects keyed by OID.
func (l *Loader) resolveObjects(m *Module) map[string]*Object {
local := map[string]*Object{}
for _, o := range m.Objects {
oid, err := l.oidOf(o)
if err != nil || oid == "" {
m.Unresolved = append(m.Unresolved, fmt.Sprintf("%s: %v", o.Name, err))
continue
}
local[oid] = o
if _, dup := l.byOID[oid]; !dup {
l.byOID[oid] = o
}
if o.syn != nil {
rt := l.resolveSyntax(m, o.syn)
o.BaseType, o.TC, o.DisplayHint, o.Enum, o.Range, o.Size = rt.base, rt.tc, rt.hint, rt.enum, rt.rng, rt.size
if o.BaseType == "SEQUENCE" || o.BaseType == "SEQUENCE OF" || o.BaseType == "CHOICE" {
o.BaseType, o.TC = "", ""
}
}
}
return local
}
// Object resolves "MODULE::name" (loading the module) or a bare name (searched in
// the modules loaded so far).
func (l *Loader) Object(name string) (*Object, error) {
l.mu.Lock()
defer l.mu.Unlock()
if mod, n, ok := strings.Cut(name, "::"); ok {
m, err := l.load(mod)
if err != nil {
return nil, err
}
o := m.byName[n]
if o == nil {
return nil, fmt.Errorf("%w: %s::%s", ErrNotFound, mod, n)
}
if o.OID == "" {
return nil, fmt.Errorf("mib: %s::%s: OID unresolved: %w", mod, n, o.rerr)
}
return o, nil
}
if c := l.byName[name]; len(c) > 0 {
for _, o := range c {
if o.OID != "" {
return o, nil
}
}
}
return nil, fmt.Errorf("%w: %s (not in a loaded module; use MODULE::name)", ErrNotFound, name)
}
// ByOID returns the loaded object with exactly this numeric OID (a leading dot is ignored).
func (l *Loader) ByOID(oid string) (*Object, error) {
l.mu.Lock()
defer l.mu.Unlock()
o, ok := l.byOID[strings.TrimPrefix(oid, ".")]
if !ok {
return nil, fmt.Errorf("%w: OID %s (no loaded module defines it)", ErrNotFound, oid)
}
return o, nil
}