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
359 lines
7.9 KiB
Go
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 ""
|
|
}
|