- Zig 98.4%
- Nix 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
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.
-
The Radicle network, where the repository's identifier is
rad:z2yVrgep2QYVHEopX1SZARvBAeL7fand
$ rad clone rad:z2yVrgep2QYVHEopX1SZARvBAeL7ffetches 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.