No description
  • Zig 99.1%
  • Nix 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie d6554c0d87
Make dynamic pipeline enqueueing thread-safe
The enqueue* methods may now be called from any thread while next()
runs on the consumer thread: the pending queue is guarded by a
std.Io.Mutex, so initDynamic/pipelineDynamic take an Io. next() and
deinit remain single-consumer by contract (NNTP responses are strictly
ordered on one connection), and static pipelines never touch the mutex,
staying allocation- and lock-free.

Adds a stress test: four producer threads enqueueing numbers and
message-ids concurrently with the consuming next() loop.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013VcyQiGUcfHVsK2mpKvBdR
2026-08-30 14:06:28 -05:00
LICENSES Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
src Make dynamic pipeline enqueueing thread-safe 2026-08-30 14:06:28 -05:00
.gitignore Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
build.zig Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
build.zig.zon Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
flake.lock Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
flake.nix Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
README.md Make dynamic pipeline enqueueing thread-safe 2026-08-30 14:06:28 -05:00
REUSE.toml Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00

znntp

An NNTP client library for Zig (0.16).

Supported standards

Standard Title Support in znntp
RFC 3977 Network News Transfer Protocol (NNTP) Core protocol: session administration, group and article selection, retrieval, posting, LIST, OVER/HDR, and pipelining
RFC 4643 NNTP Extension for Authentication AUTHINFO USER/PASS
RFC 8054 NNTP Extension for Compression COMPRESS DEFLATE, both directions; interoperates with sync-flush and partial-flush servers
RFC 2980 Common NNTP Extensions XOVER, for servers that predate RFC 3977
RFC 8143 Using TLS with NNTP Implicit TLS (NNTPS on port 563) with system-root certificate verification, as the RFC recommends
RFC 1951 DEFLATE Compressed Data Format Compression via std.compress.flate; decompression via a custom resumable inflate suited to interactive streams

Design

  • znntp.Client implements the protocol over any std.Io.Reader/std.Io.Writer pair. Transport is the caller's business — plain TCP, TLS, a proxy, or fixed buffers in tests.
  • znntp.Connection is the batteries-included transport: TCP or TLS (NNTPS, port 563) with system-root certificate verification, built on std.Io.
  • Zero-copy reads: response messages and multiline data lines borrow from the reader's buffer and are invalidated by the next read. Copy what you keep, or use the *Alloc variants.
  • Dot-stuffing/unstuffing and CRLF handling are dealt with in both directions; command arguments are checked against CRLF injection.

Usage

const std = @import("std");
const znntp = @import("znntp");

pub fn main() !void {
    var gpa_state: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa_state.deinit();
    const gpa = gpa_state.allocator();

    var threaded: std.Io.Threaded = .init(gpa, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const conn = try znntp.Connection.connect(gpa, io, "news.example.com", .{
        .tls = .on, // NNTPS on port 563; use .off for plain NNTP on 119
    });
    defer conn.destroy();
    var client = conn.client();

    const greeting = try client.readGreeting();
    std.debug.print("{s}\n", .{greeting.message});

    // try client.authenticate("user", "password");

    // Compress the rest of the session (COMPRESS DEFLATE, RFC 8054).
    // Works with both sync-flush and partial-flush servers (INN included):
    // the read side uses a resumable inflate that never blocks while
    // decodable data is pending.
    // try conn.enableDeflate(&client);

    const info = try client.group("misc.test");

    // Overview of the last few articles (lines borrow the read buffer).
    var it = try client.over(.{ .span = .{ .low = info.low, .high = info.high } });
    while (try it.next()) |line| {
        const entry = try znntp.OverviewEntry.parse(line);
        std.debug.print("{d} {s}\n", .{ entry.number, entry.subject });
    }

    // Fetch an article into owned memory (capped at 1 MiB).
    var art = try client.articleAlloc(gpa, .{ .number = info.high }, .limited(1024 * 1024));
    defer art.deinit(gpa);
    std.debug.print("{s}\n", .{art.data});

    try client.quit();
}

Pipelined bulk fetches

For downloading many articles, Pipeline keeps a bounded window of commands in flight (RFC 3977 pipelining) so the server overlaps its responses with your requests — without the unbounded-burst deadlock:

var pipe = client.pipeline(.body, .{ .range = .{ .low = info.low, .high = info.high } }, .{ .window = 16 });
while (try pipe.next()) |item| {
    switch (item.result) {
        .info => |art| {
            var data = item.data.?;
            while (try data.next()) |line| { ... }
        },
        .missing => |code| { ... }, // article expired or cancelled; run continues
    }
}

Sources can be a number range, a slice of numbers, or a slice of message-ids; verbs are .article, .head, .body, and .stat. Unconsumed article data is discarded automatically on the next iteration. Don't interleave other commands while a pipeline is running.

A pipeline can also start empty and be fed while it runs — enqueue new work between iterations as you discover it (following References headers, walking an NZB, ...), from any thread. Message-ids are copied, so it's fine to pass slices parsed out of the borrowed read buffer:

var pipe = client.pipelineDynamic(gpa, io, .article, .{ .window = 16 });
defer pipe.deinit();
try pipe.enqueueMessageId(root_id);
while (try pipe.next()) |item| {
    // ...parse the article, discover more...
    try pipe.enqueueMessageId(referenced_id);
    try pipe.enqueueRange(low, high);
}
// null means idle, not finished: enqueue more and call next() again.

Commands not wrapped by a helper can be sent through the escape hatch:

const r = try client.command("XFEATURE COMPRESS GZIP", .{});
if (!r.isSuccess()) return error.Unsupported;

Multiline responses must be consumed fully (iterate to null, or call discard()) before sending the next command.

Demo CLI

zig build run -- news.example.com misc.test --tls --compress

Prints the greeting, capabilities, and the last ten overview lines of the group.

Testing

zig build test

Protocol tests run against in-memory streams; no network access needed.