No description
  • Zig 98.4%
  • Nix 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 2171fbd8c9
All checks were successful
test / test (push) Successful in 7m46s
test / docs (push) Successful in 6m6s
Push CI builds to the cache, and make the fuzz driver describe this project
The workflow's jobs take an OIDC token and end by pushing what they built
to niks3, as the other projects' do.

tools/fuzz.zig came from a project fuzzing LDAP and BER, and still said
so: a binary flavor no target used, a table of BER identifier octets, and
comments about messages and paths. The two flavors are now the two kinds of
text this project has -- MIB modules, and DISPLAY-HINTs, which get an
alphabet of their own.

package.nix's description was the BER library's; it is now this one's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LzspmERZM4HGjxKUhP2Fcr
2026-10-10 00:01:17 -05:00
.forgejo/workflows Push CI builds to the cache, and make the fuzz driver describe this project 2026-10-10 00:01:17 -05:00
LICENSES A lexer for SMIv1 and SMIv2 2026-09-12 13:52:24 -05:00
src Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
tests Push CI builds to the cache, and make the fuzz driver describe this project 2026-10-10 00:01:17 -05:00
tools Push CI builds to the cache, and make the fuzz driver describe this project 2026-10-10 00:01:17 -05:00
.gitignore A lexer for SMIv1 and SMIv2 2026-09-12 13:52:24 -05:00
build.zig Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
build.zig.zon Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
flake.lock Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
flake.nix Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
package.nix Push CI builds to the cache, and make the fuzz driver describe this project 2026-10-10 00:01:17 -05:00
README.md Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-09 23:50:03 -05:00
REUSE.toml A lexer for SMIv1 and SMIv2 2026-09-12 13:52:24 -05:00

zig-smi

A reader for SNMP MIB modules: SMIv1 and SMIv2, in Zig.

The API documentation is generated from the doc comments, which carry most of the explanation, and is published at https://jeff.jcollie.page/zig-smi/.

$ git clone https://git.jcollie.dev/jeff/zig-smi.git
$ cd zig-smi
$ nix develop -c zig build test

It is written for Zig 0.17. The last release for Zig 0.16 is v0.1.0, and the zig-0.16 branch holds it.

Where this lives

Three homes, with the same history in all of them.

  • Forgejo, at https://git.jcollie.dev/jeff/zig-smi, which is where the workflow runs and where the documentation is published from.

  • Tangled, at https://tangled.org/jcollie.dev/zig-smi.

  • The Radicle network, where the repository's identifier is

    rad:z2yVrgep2QYVHEopX1SZARvBAeL7f
    

    and

    $ rad clone rad:z2yVrgep2QYVHEopX1SZARvBAeL7f
    

    fetches it from any node that seeds it, needing no account and no forge. A Radicle repository is findable by its identifier and by nothing else, so that string is the whole of the address.

What this is for

zig-snmp speaks the protocol and knows nothing about what any OID means: it can fetch 1.3.6.1.2.1.1.1.0 but not sysDescr.0, and it renders that object's value as an octet string because it has no way to learn it is a DisplayString. All of that meaning lives in MIB modules, which are written in a language of their own — ASN.1 with a set of macros bolted on — and reading them is a compiler rather than a protocol feature. Hence a separate repository.

State

The lexer, the module parser, the name resolver and the clause reader. What is left is the integration into zig-snmp itself, so that its tools take -m and -M and a symbolic name.

Measured against 2,852 published MIB files — net-snmp's 65 base modules, and Cisco's 1,648 SMIv2 and 1,139 SMIv1 modules at a pinned revision:

lex cleanly 2,849 of 2,852
parse into a module 2,847, yielding 280,240 assignments
contain no module at all 2 — one of them is nine lines of comment reading "please ignore this mib"
fail 3, all of which net-snmp also rejects, naming the identical line on two

The three failures are genuinely broken files: a stray full stop after a closing quote, a VARIATION clause left outside the macro it belonged to, and two DESCRIPTIONs whose quote was closed early so that prose spills into the grammar.

The three rules that cost the time

A comment ends at -- or at the end of the line, whichever comes first. X.208 §8.6 says so, and it matters: in

SYNTAX INTEGER -- a comment -- (0..255)

the constraint is real, and treating -- as "rest of line" drops it silently — producing a wrong SYNTAX rather than an error, so nothing notices. But real MIBs are full of banner lines made of dashes, where the pairs close and reopen the comment and the author plainly meant the whole line. net-snmp ships -Pc to switch the rule off for exactly this reason. Neither reading is right for every real file, so it is an option.

Underscores appear in identifiers, and ASN.1 says they may not. CISCO-LWAPP-TC-MIB has dot11_6ghz(6); CISCO-SWITCH-QOS-MIB has inputStats_ingressStraight(1). net-snmp rejects both with Expected "(" (_) until -Pu is passed. The default here is the opposite of net-snmp's, on the grounds that the job is to read the MIBs that exist; anything comparing the two passes -Pu on the other side.

A module ends at END, and files have things after it. CISCO-OSPF-CAPABILITY.my ends with </PRE></BODY></HTML>, having evidently been saved out of a browser. net-snmp reads it without complaint because it stops at END and never looks, and anything scanning a whole file has to do the same or it reports a footer as a lexical failure.

Two things the parser deliberately does not do

It does not parse macro bodies. SMI's macros are ASN.1 macro invocations, and parsing one properly means implementing ASN.1's macro definition language. Nobody does that, net-snmp included. What every macro shares — a name, a keyword, a body, and ::= { parent n } — is parsed here, and the body is kept as a span for a clause parser to read. That split is worth having rather than merely convenient: the OID tree can be built from a module whose clauses have not been understood, so an unfamiliar macro still contributes its names and a bug in one clause cannot lose a whole module.

It steps over macro definitions. Four modules carry them — SNMPv2-TC defines TEXTUAL-CONVENTION, RFC1155-SMI defines OBJECT-TYPE, and so on — and a reader that already knows those macros learns nothing from their definitions. Failing on them would lose the other assignments in those files, and RFC1155-SMI is where internet, mgmt and enterprises are defined, which is to say the top of the tree.

The corpus, and how to run against it

zig build lexcheck -- <files> lexes and parses a set of MIBs and reports what it cannot read. It is how the rules above were settled and how a regression would be caught:

$ nix develop -c zig build lexcheck -- /path/to/mibs/*.txt
65 files, 63220 tokens, 0 with invalid tokens; 65 parsed (3071 assignments), 0 failed, 0 with no module

Cisco's corpus is pinned by revision so that "all but three" stays a reproducible claim rather than a memory:

$ nix build --impure --expr 'let p = import <nixpkgs> {}; in p.fetchFromGitHub {
    owner = "cisco"; repo = "cisco-mibs";
    rev = "26b38ac1b25a38510b6bb308465dd068e79df64b";
    hash = "sha256-U1ptTVBkAVSPNL3cuLWDyLqUkiygWWfqJJuVks6Z0aA=";
  }'
$ nix develop -c zig build lexcheck -- ./result/v2/*.my

net-snmp as the oracle

The same discipline as the other libraries here: correctness is settled by agreement with the reference implementation rather than by reading the standard alone. For a MIB reader the comparison is unusually clean, because snmptranslate will print its whole symbol table:

$ snmptranslate -Tz          # every name and its OID, 2637 of them
$ snmptranslate -Td <oid>    # one object in full: syntax, TC, access, status

Both have an answering tool here, and the diff is the test:

$ nix develop -c zig build translate -- /path/to/mibs/*.txt
$ nix develop -c zig build describe  -- /path/to/mibs/*.txt

Over net-snmp's 65 base modules:

names placed, versus -Tz 2,619 of 2,619 agree exactly
objects described, versus -Td 2,507 of 2,607 agree on every field — syntax, textual convention, display hint, units, access, status, index, augments and OID

Neither residue is an open question, and both are enumerated in the tools' doc comments. The 18 extra -Tz lines are the second definition of a name defined twice: net-snmp dumps a tree, in which linux under both ucdSnmpAgent and netSnmpAgentOIDs is two nodes, and this is a map from name to node, in which it is one and the discard is recorded as a conflict. net-snmp cannot translate such a name either, answering snmptranslate linux with "Sub-id not found". Of the hundred -Td differences, 79 are net-snmp rendering DEFVAL — it prints { 0 } for a hex literal it declines to interpret — 18 are that same tree-versus-map difference, 2 are RFC1213-MIB's own PhysAddress ::= OCTET STRING being followed rather than SNMPv2-TC's textual convention, and 1 is net-snmp printing a module name where a size belongs.

One thing found on the way there is worth repeating, because it looked for a long while like a disagreement and was not. An object defined in two modules at the same OID — 168 of these 2,607 are, mostly an SMIv1 module and the SMIv2 one that replaced it — is described by whichever module came first, in both tools. That was the only reason they differed about STATUS, MAX-ACCESS, UNITS and DISPLAY-HINT for a few hundred objects. net-snmp loads MIBs in reverse directory order, pulling each module's imports in depth-first as it goes; give describe that same order and all four categories go to zero:

$ snmptranslate -Dparse-file -Td SNMPv2-MIB::sysDescr 2>&1 \
    | grep -oP 'Parsing file:  \K\S+(?=\.\.\.)' | awk '!seen[$0]++' > order
$ nix develop -c zig build describe -- $(tr '\n' ' ' < order)

The rule was always the same rule. It was the input order that differed.

Licence

MIT. See LICENSES/MIT.txt.