No description
  • Zig 97%
  • Nix 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 3a2f925299
All checks were successful
test / test (push) Successful in 1m23s
update workflow runner tag
2026-09-04 19:16:55 -05:00
.forgejo/workflows update workflow runner tag 2026-09-04 19:16:55 -05:00
LICENSES Initial commit: an NZB decoder for Zig 2026-08-30 14:19:38 -05:00
src Give every public declaration a test named after it 2026-08-30 20:09:29 -05:00
.gitignore Ignore zig-pkg/ 2026-08-30 15:57:11 -05:00
build.zig Depend on zxml instead of carrying the XML parser 2026-08-30 16:03:42 -05:00
build.zig.zon Re-pin zxml to 997c280 2026-08-30 21:54:42 -05:00
flake.lock Initial commit: an NZB decoder for Zig 2026-08-30 14:19:38 -05:00
flake.nix provide a CA bundle in the dev shell for zig's fetcher 2026-08-30 18:42:40 -05:00
README.md List the standards this library implements 2026-08-30 22:10:24 -05:00
REUSE.toml Initial commit: an NZB decoder for Zig 2026-08-30 14:19:38 -05:00

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.nzb decodes NZB 1.1 documents into a Nzb value: the <head> metadata and, per file, its poster, subject, newsgroups, and segments.
  • znzb.subject recovers 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. Nzb owns an arena holding every string it hands out, so the whole result is released with one deinit and nothing borrows from the input buffer after parse returns.
  • 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 missing number or 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.zig carries a std.testing.fuzz target that runs the decoder under the testing allocator, so zig build test replays the corpus on every run; the XML parser has its own in zxml. Note that zig build test --fuzz does 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 <[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. --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.