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
425 lines
11 KiB
Go
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
|
|
}
|