- Zig 96.3%
- Nix 3.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Take Zig 0.17.0 from nixpkgs and drop the patched 0.16 test runner, which 0.17 no longer needs. Compile both test binaries with LLVM so that Zig's own fuzzer gets coverage; `zig build fuzz --fuzz` now works. Port what 0.17 changed: passthrough args, the docs server reading the generated directory, `**` replaced by @splat, the enum builtins renamed, and `zig build` no longer taking --global-cache-dir. Element.enumerated read is_exhaustive and fields, which 0.17's Type.Enum no longer has; no test instantiated it with an exhaustive enum, so one does now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGnbbt9Gi9YZeo6qnXQGWH |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-asn1
The Basic Encoding Rules for Zig: an encoder, a decoder, and the OBJECT IDENTIFIER type, for protocols that are defined in ASN.1 and carried in BER on a wire.
The API documentation is generated from the doc comments, which carry most of the explanation, and is published at https://jeff.jcollie.page/zig-asn1/.
It builds with Zig 0.17. For Zig 0.16, use the zig-0.16 branch or the
v0.1.0 tag.
$ git clone https://git.jcollie.dev/jeff/zig-asn1.git
$ cd zig-asn1
$ nix develop -c zig build test
Where this lives
Two homes, with the same history in both.
- Forgejo, at https://git.jcollie.dev/jeff/zig-asn1, which is where the workflow runs and where the documentation is published from.
- Tangled, at https://tangled.org/jcollie.dev/zig-asn1.
What is here
Not the whole of X.690, and deliberately so. What is implemented is the subset that real protocols use and that can be implemented without ambiguity: definite lengths only, the primitive form for simple types, and minimal encodings throughout. LDAP narrows BER to very nearly this in RFC 4511 §5.1 and SNMP does the same in RFC 3417, so the restriction costs nothing and buys a decoder with exactly one reading of every input.
Tag, Class, Universal |
An identifier: its class, whether it is constructed, and its number. Tag.universal, Tag.application and Tag.context are the three constructors, and application- and context-class tags are first-class rather than an afterthought — they are how most protocols spell their own types. |
Encoder |
Builds an encoding in memory, backpatching lengths. A definite length has to be written before the content that determines it, so begin leaves it out and end fills it in: callers write a message in reading order instead of measuring everything twice. |
Decoder, Element |
Walks a buffer. Allocates nothing and copies nothing — every Element.content is a subslice of the bytes you handed in, which is what makes decoding a large message free. |
ObjectId |
An OBJECT IDENTIFIER, in both of its encodings: base-128 sub-identifiers on the wire and dotted decimal as text. A fixed-capacity value rather than an allocation, with the lexicographic ordering and prefix test that walking a MIB needs. |
readElementAlloc |
Reads exactly one element off an Io.Reader, for a stream protocol that has to know where a message ends. This is framing, not validation — see below. |
Using it as a library
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-asn1.git
const asn1 = @import("asn1");
// Encoding: SEQUENCE { OBJECT IDENTIFIER, NULL }, which is an SNMP varbind.
var e: asn1.Encoder = .init(gpa);
defer e.deinit();
try e.begin(.sequence);
try e.objectId(try asn1.ObjectId.parse("1.3.6.1.2.1.1.1.0"));
try e.@"null"();
try e.end();
// Decoding, out of the bytes as they arrived.
var d: asn1.Decoder = .init(e.buffered());
var varbind = (try d.expect(.sequence)).decoder();
const oid = try varbind.objectId();
std.debug.print("{f}\n", .{oid}); // .1.3.6.1.2.1.1.1.0
What is minimal, and why it matters
The decoder rejects every encoding that has a shorter spelling of the same
value: a length written in the long form when the short form would do, an
integer with redundant leading 00 or ff octets, a sub-identifier or a high
tag number with a leading 0x80, and — the one a fuzzer had to find — the high
tag number form used for a number below 31, so that 1f 05 is refused as a
second spelling of 05.
That is not pedantry about the standard. A protocol that accepts two encodings
of one message is a protocol where "the bytes of the message" is not a
well-defined thing, and SNMPv3 computes an HMAC over exactly that. Canonicality
is a security property, so the decoder enforces it and the encoder only ever
emits the one form. decodeTag composed with encodeTag is the identity on
everything the decoder accepts, and that is a fuzz target rather than a hope.
Framing is not validation
readElementAlloc answers "where does this element end", so that a caller
reading from a stream knows how many bytes to hold before it holds any of
them. It does not promise the bytes are a legal element — it will happily
frame one whose tag is non-minimally encoded, and Decoder is what then
refuses it. The two are separate on purpose, and the doc comment says so,
because a fuzz property that assumed otherwise is what surfaced the
distinction.
Tests
$ nix develop -c zig build test --summary all
$ nix develop -c zig build fuzz --fuzz=1M
$ nix develop -c zig build fuzz-run -- --seconds 60
Unit tests live beside what they cover. tests/fuzz.zig holds properties
rather than examples — that the decoder terminates on any input, that every
Element.content really is a subslice of the input, and that anything which
decodes re-encodes to the identical bytes. zig build fuzz --fuzz runs them
under Zig's own coverage-guided fuzzer; both test binaries are compiled with
LLVM even in Debug, because the self-hosted backend emits no coverage
instrumentation. tools/fuzz.zig drives the same properties from a loop of
our own, seeded and bounded by a count, which is what the workflow runs.
Both defects found so far were found by those properties and are described above: the non-minimal high tag number, and the framing/validation confusion.
The API documentation
$ nix develop -c zig build docs # into zig-out/docs
$ nix develop -c zig build docs-serve # http://127.0.0.1:8000
It has to be served rather than opened: the viewer Zig emits is WebAssembly
that fetches sources.tar at runtime, which a browser refuses from a file://
page.
Licence
MIT. See LICENSES/MIT.txt.