- Zig 97%
- Nix 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
znzb
A decoder for NZB files, for Zig 0.16.
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.
Installation
$ zig fetch --save git+https://git.ocjtech.us/jeff/znzb.git
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. Note thatzig build test --fuzzdoes not currently work with Zig 0.16.0: fuzz mode fails to build the compiler's own test runner, and a trivial one-test file reproduces it. The targets are there for the toolchain that fixes it.
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 <bloggs@nowhere.example>
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. --fuzz for continuous
fuzzing is blocked by the Zig 0.16.0 issue noted above.
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.
License
MIT.