Files
nmon/internal/mib/parse.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

359 lines
7.9 KiB
Go

package mib
import (
"fmt"
"os"
"strconv"
"strings"
)
// ParseFile parses every module in one MIB file. OIDs are not resolved; use a
// Loader for that. A file with no MODULE DEFINITIONS is an error.
func ParseFile(path string) ([]*Module, error) {
src, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return parse(path, src, true)
}
// Parse parses MIB source text. The name is used for error messages.
func Parse(name string, src []byte) ([]*Module, error) { return parse(name, src, true) }
type parser struct {
file string
toks []token
i int
m *Module
keepDesc bool
}
func parse(file string, src []byte, keepDesc bool) ([]*Module, error) {
toks, err := lex(src)
if err != nil {
return nil, fmt.Errorf("%s: %w", file, err)
}
p := &parser{file: file, toks: toks, keepDesc: keepDesc}
var mods []*Module
for p.i < len(toks) {
if !p.isIdent(p.i, "DEFINITIONS") {
p.i++
continue
}
name, err := p.moduleName(p.i)
if err != nil {
return nil, err
}
p.i++
// optional tag defaults, then ::= BEGIN
for k := 0; k < 8 && p.i < len(toks) && !p.isIdent(p.i, "BEGIN"); k++ {
p.i++
}
if !p.isIdent(p.i, "BEGIN") {
return nil, p.errf("module %s: missing BEGIN", name)
}
p.i++
p.m = &Module{Name: name, File: file, types: map[string]*typeDef{}, byName: map[string]*Object{}}
if err := p.body(); err != nil {
return nil, err
}
mods = append(mods, p.m)
}
if len(mods) == 0 {
return nil, fmt.Errorf("%s: no MODULE DEFINITIONS found", file)
}
return mods, nil
}
func (p *parser) errf(format string, a ...any) error {
line := int32(0)
if p.i < len(p.toks) {
line = p.toks[p.i].line
} else if len(p.toks) > 0 {
line = p.toks[len(p.toks)-1].line
}
return fmt.Errorf("%s:%d: %s", p.file, line, fmt.Sprintf(format, a...))
}
func (p *parser) at(i int) token {
if i < len(p.toks) {
return p.toks[i]
}
return token{kind: tSym, s: "\x00"}
}
func (p *parser) isIdent(i int, s string) bool {
return i < len(p.toks) && p.toks[i].kind == tIdent && p.toks[i].s == s
}
func (p *parser) isSym(i int, s string) bool {
return i < len(p.toks) && p.toks[i].kind == tSym && p.toks[i].s == s
}
func (p *parser) isAssign(i int) bool { return i < len(p.toks) && p.toks[i].kind == tAssign }
// moduleName finds the module name before the DEFINITIONS token at index di.
func (p *parser) moduleName(di int) (string, error) {
j := di - 1
if j >= 0 && p.isSym(j, "}") {
for j >= 0 && !p.isSym(j, "{") {
j--
}
j--
}
if j < 0 || p.toks[j].kind != tIdent {
p.i = di
return "", p.errf("cannot find module name before DEFINITIONS")
}
return p.toks[j].s, nil
}
func (p *parser) body() error {
for p.i < len(p.toks) {
t := p.toks[p.i]
if t.kind != tIdent {
p.i++
continue
}
switch t.s {
case "END":
p.i++
return nil
case "EXPORTS":
for p.i < len(p.toks) && !p.isSym(p.i, ";") {
p.i++
}
p.i++
continue
case "IMPORTS":
p.i++
p.imports()
continue
}
if err := p.item(t); err != nil {
return err
}
}
p.m.Warnings = append(p.m.Warnings, "module has no END")
return nil
}
// item parses one top-level assignment that starts with identifier t.
func (p *parser) item(t token) error {
n1 := p.at(p.i + 1)
switch {
case n1.kind == tAssign:
return p.typeAssign()
case n1.kind == tIdent && n1.s == "MACRO":
p.skipMacro()
case n1.kind == tIdent && n1.s == "OBJECT" && p.isIdent(p.i+2, "IDENTIFIER"):
return p.objectIdentifier(t.s)
case n1.kind == tIdent:
return p.definition(t.s, n1.s)
default:
p.m.Warnings = append(p.m.Warnings, fmt.Sprintf("line %d: skipped stray token %q", t.line, t.s))
p.i++
}
return nil
}
// objectIdentifier parses "name OBJECT IDENTIFIER ::= { ... }".
func (p *parser) objectIdentifier(name string) error {
p.i += 3
if !p.isAssign(p.i) {
return p.errf("%s: expected ::=", name)
}
p.i++
def, err := p.oidValue()
if err != nil {
return err
}
p.add(&Object{Name: name, Kind: KindNode, def: def})
return nil
}
func (p *parser) add(o *Object) {
o.Module = p.m.Name
o.mod = p.m
if _, dup := p.m.byName[o.Name]; !dup {
p.m.byName[o.Name] = o
}
p.m.Objects = append(p.m.Objects, o)
}
func (p *parser) imports() {
for p.i < len(p.toks) && p.importGroup() {
}
}
// importNames collects the identifiers before FROM. It reports true when the
// IMPORTS clause ended (";") first.
func (p *parser) importNames() (names []string, ended bool) {
for p.i < len(p.toks) && !p.isIdent(p.i, "FROM") {
if p.toks[p.i].kind == tIdent {
names = append(names, p.toks[p.i].s)
}
p.i++
if p.isSym(p.i, ";") {
p.i++
return names, true
}
}
return names, false
}
// importGroup parses "a, b FROM Module" and reports whether another group follows.
func (p *parser) importGroup() bool {
if p.isSym(p.i, ";") {
p.i++
return false
}
names, ended := p.importNames()
if ended {
return false
}
p.i++ // FROM
if p.i >= len(p.toks) || p.toks[p.i].kind != tIdent {
return false
}
mod := p.toks[p.i].s
p.i++
if p.isSym(p.i, "{") { // ASN.1 style module OID
p.skipBraces()
}
p.m.Imports = append(p.m.Imports, Import{Module: mod, Names: names})
if p.isSym(p.i, ";") {
p.i++
return false
}
// Another group follows only if the next ident is followed by "," or FROM.
return p.at(p.i).kind == tIdent && (p.isSym(p.i+1, ",") || p.isIdent(p.i+1, "FROM"))
}
// skipBraces skips a balanced { ... } group starting at p.i.
func (p *parser) skipBraces() {
depth := 0
for p.i < len(p.toks) {
switch {
case p.isSym(p.i, "{"):
depth++
case p.isSym(p.i, "}"):
depth--
if depth <= 0 {
p.i++
return
}
}
p.i++
}
}
func (p *parser) skipMacro() {
p.i += 2 // name MACRO
depth := 0
for p.i < len(p.toks) {
switch {
case p.isIdent(p.i, "BEGIN"):
depth++
case p.isIdent(p.i, "END"):
depth--
if depth <= 0 {
p.i++
return
}
}
p.i++
}
}
// oidValue parses { a b(1) 2 mod.name }.
func (p *parser) oidValue() ([]oidElem, error) {
if !p.isSym(p.i, "{") {
return nil, p.errf("expected { in OBJECT IDENTIFIER value, got %q", p.at(p.i).s)
}
p.i++
var out []oidElem
for p.i < len(p.toks) && !p.isSym(p.i, "}") {
e, ok, err := p.oidElement(p.toks[p.i])
if err != nil {
return nil, err
}
if ok {
out = append(out, e)
}
}
if !p.isSym(p.i, "}") {
return nil, p.errf("unterminated OBJECT IDENTIFIER value")
}
p.i++
if len(out) == 0 {
return nil, p.errf("empty OBJECT IDENTIFIER value")
}
return out, nil
}
// oidElement parses the sub-identifier starting at token t. ok is false for a
// separator that yields no element.
func (p *parser) oidElement(t token) (e oidElem, ok bool, err error) {
switch t.kind {
case tNumber:
n, err := strconv.Atoi(t.s)
if err != nil {
return e, false, p.errf("bad sub-identifier %q", t.s)
}
p.i++
return oidElem{num: n, hasNum: true}, true, nil
case tIdent:
e, err := p.oidName(t)
return e, err == nil, err
case tSym:
if t.s == "." {
p.i++
return e, false, nil
}
}
return e, false, p.errf("unexpected %q in OBJECT IDENTIFIER value", t.s)
}
// oidName parses name, Module.name or name(n).
func (p *parser) oidName(t token) (oidElem, error) {
e := oidElem{name: t.s}
p.i++
if p.isSym(p.i, ".") && p.at(p.i+1).kind == tIdent { // Module.name
e.name += "." + p.toks[p.i+1].s
p.i += 2
}
if p.isSym(p.i, "(") && p.at(p.i+1).kind == tNumber && p.isSym(p.i+2, ")") {
n, err := strconv.Atoi(p.toks[p.i+1].s)
if err != nil {
return e, p.errf("bad sub-identifier %q", p.toks[p.i+1].s)
}
e.num, e.hasNum = n, true
p.i += 3
}
return e, nil
}
func (p *parser) text(s string) string {
if !p.keepDesc {
return ""
}
return strings.Join(strings.Fields(s), " ")
}
// str consumes a string token if present.
func (p *parser) str() string {
if p.i < len(p.toks) && p.toks[p.i].kind == tString {
s := p.toks[p.i].s
p.i++
return s
}
return ""
}
func (p *parser) ident() string {
if p.i < len(p.toks) && p.toks[p.i].kind == tIdent {
s := p.toks[p.i].s
p.i++
return s
}
return ""
}