- Zig 97.5%
- Nix 2.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XR6wrQGmTR746eD8sDDNTa |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
znzb
A decoder for NZB files, for Zig 0.17.
The decoder fetches nothing. An NZB names articles by Message-ID and newsgroup;
retrieving them is a job for an NNTP client such as
znntp, and Segment formats itself with
the angle brackets those commands expect.
The API documentation is generated from the doc comments, which is where most of the explanation lives.
Installation
$ zig fetch --save git+https://git.jcollie.dev/jeff/znzb.git
For Zig 0.16, use the zig-0.16 branch, which holds the last of znzb to build
with it (version 0.1.0):
$ zig fetch --save git+https://git.jcollie.dev/jeff/znzb.git#zig-0.16
Its one dependency is zxml, a small XML
pull parser with no dependencies of its own — Zig's standard library has none.
zig fetch resolves it over git+https; git+ssh is not a scheme Zig
understands, so the dependency URL is the https form even where the push remote
is ssh.
Modules
znzb.nzbdecodes NZB 1.1 documents into aNzbvalue: the<head>metadata and, per file, its poster, subject, newsgroups, and segments.znzb.subjectrecovers the filename and part numbering that an NZB records nowhere except inside the article subject.
The XML parsing is zxml's. Depend on that package directly if what you want is the parser rather than the NZB decoder built on it.
Design
- One arena per document.
Nzbowns an arena holding every string it hands out, so the whole result is released with onedeinitand nothing borrows from the input buffer afterparsereturns. - Liberal about what it accepts. Unknown elements, namespace prefixes, and
extension attributes are skipped rather than rejected. Fields with no
structural weight —
bytes,date,poster— may be missing or malformed and come back zero, null, or empty. Only something that would leave a segment unfetchable, a missingnumberor Message-ID, is an error. - Encoding is handled. Documents declaring ISO-8859-1, as the NZB specification's own example does, are transcoded to UTF-8; the C1 range is decoded as Windows-1252, which is what mislabelled files actually contain. That, and the refusal to resolve external entities, come from zxml.
- Fuzz target included.
src/nzb.zigcarries astd.testing.fuzztarget that runs the decoder under the testing allocator, sozig build testreplays the corpus on every run; the XML parser has its own in zxml.zig build test --fuzzfuzzes it with coverage feedback.
Usage
const std = @import("std");
const znzb = @import("znzb");
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
var doc = try znzb.nzb.parse(gpa, bytes, .{});
defer doc.deinit();
std.debug.print("{?s}: {d} files, {d} bytes\n", .{
doc.title(), doc.files.len, doc.totalBytes(),
});
for (doc.files) |file| {
const subject = file.parsedSubject();
std.debug.print("{s}\n", .{subject.displayName()});
for (file.segments) |segment| {
// Prints as <message-id>, ready for an NNTP ARTICLE command.
std.debug.print(" {f} from {s}\n", .{ segment, file.groups[0] });
}
}
}
Reading straight from a stream instead of a slice:
var doc = try znzb.nzb.parseReader(gpa, reader, .limited(64 * 1024 * 1024), .{});
defer doc.deinit();
Filenames
An NZB <file> has no filename field. The name of the file its segments
reassemble into exists only in the article subject, which posting software
formats by convention rather than by rule. znzb.subject handles the shapes
that dominate in practice:
const s = znzb.subject.parse("[1/8] - \"Some.Release.part1.rar\" yEnc (1/120)");
// s.name == "Some.Release.part1.rar"
// s.part == .{ .index = 1, .total = 120 } // which segment
// s.file_index == .{ .index = 1, .total = 8 } // which file
// s.yenc == true
The segment counter is what makes File.isComplete possible: an NZB records no
count of its own, so a posting missing half its articles is otherwise
indistinguishable from a whole one.
Command line
The package also builds a znzb binary, which decodes an NZB and summarizes
it. It is a worked example of the API as much as a tool.
$ zig build run -- --list example.nzb
example.nzb: NZB
title: Your File!
tag: Example
2 files, 4 segments, 1.1 MiB
[1] abc-mr2a.r01
2 segments of 2, 104.4 KiB
poster: Joe Bloggs <[email protected]>
date: 1071674882
groups: alt.binaries.newzbin, alt.binaries.mojo
A path of - reads standard input, so a downloaded NZB can be piped straight
in:
$ curl -s "$INDEXER/getnzb/abc123.nzb" | zig build run -- -l -
Testing
$ zig build test
The fuzz targets run their corpus as part of that. To fuzz them:
$ zig build test --fuzz # until interrupted, with a web interface
$ zig build test --fuzz=1M # a bounded run, then a report
The test binaries are always compiled with LLVM, because the self-hosted backend Debug otherwise uses emits no coverage instrumentation, and the fuzzer needs it.
Documentation
$ zig build docs # into zig-out/docs
$ zig build docs-serve # and read it at http://127.0.0.1:8000
It has to be served rather than opened: the viewer Zig generates fetches
sources.tar and main.wasm at runtime, and a browser refuses both from a
file:// page. -Ddocs-port=N moves the port. Pushing to main publishes the
same output to https://jeff.jcollie.page/znzb/.
Standards
| Standard | Title | Support in znzb |
|---|---|---|
| NZB 1.1 | newzBin NZB | The whole format: <head> metadata, and per file its poster, date, subject, groups and segments. Namespace http://www.newzbin.com/DTD/2003/nzb |
| XML 1.0 (5th ed.) | Extensible Markup Language | Parsing is zxml's; this library only walks the result |
| ISO/IEC 8859-1 | Latin alphabet No. 1 | Transcoded to UTF-8 when declared, as the NZB specification's own example does |
| RFC 5322 | Internet Message Format | The poster attribute is a From value, kept as written rather than parsed |
| RFC 5536 | Netnews Article Format | Message-IDs, which NZB stores without angle brackets and NNTP wants with them |
The <file> subject is not covered by any standard. What znzb.subject
recovers from it is convention among posting software, described in
src/subject.zig.
Repository
The repository's home is my Forgejo instance at git.jcollie.dev/jeff/znzb, which is where CI runs.
$ git clone https://git.jcollie.dev/jeff/znzb.git
It is mirrored on Tangled at https://tangled.org/jcollie.dev/znzb, and it is also published on Radicle, a peer-to-peer forge built on git, where the copy needs no account and no server anyone has to keep running. The repository's identifier there is
rad:z23b7qppaT6qaFaj7vVYkiJUUMB3L
and this fetches it:
$ rad clone rad:z23b7qppaT6qaFaj7vVYkiJUUMB3L
Any of the three is the whole project, on the main branch, with the same
history.
rad clone finds seeds through your local node's routing table rather than
through a known host, so the node has to be running before it can find
anything:
$ rad node start
If you already have the repository and only want to help host it, seeding it tells your node to carry a copy for others:
$ rad seed rad:z23b7qppaT6qaFaj7vVYkiJUUMB3L
License
MIT.