- Zig 92.3%
- Nix 7.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
test / test (push) Successful in 2m31s
STARTTLS support went in without being recorded anywhere a reader looks for what this library implements. It is now in the README's standards table and in root.zig's list, and the doc comments for Client.startTls, Connection.startTls and response code 580 carry section links the way the rest of the code already did. Also states in the table that RFC 8143 prefers the dedicated TLS port over STARTTLS, since the library implements both and the choice between them is the first thing a caller has to make. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkxDixvQUrBMuaMQPvQNMF |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| test | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
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 4642 | NNTP Extension for TLS | STARTTLS, upgrading an established plaintext connection in place |
| RFC 8143 | Using TLS with NNTP | Implicit TLS (NNTPS on port 563) with system-root certificate verification, which the RFC prefers over STARTTLS |
| RFC 1951 | DEFLATE Compressed Data Format | Compression via std.compress.flate; decompression via a custom resumable inflate suited to interactive streams |
Design
znntp.Clientimplements the protocol over anystd.Io.Reader/std.Io.Writerpair. Transport is the caller's business — plain TCP, TLS, a proxy, or fixed buffers in tests.znntp.Connectionis the batteries-included transport: TCP or TLS (NNTPS, port 563) with system-root certificate verification, built onstd.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
*Allocvariants. - 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.