Skip to content

feat(nzb-nntp): bounded NNTP observation API (ported from fork) - #146

Merged
thedancingdeveloper merged 12 commits into
mainfrom
feat/nntp-observation-api
Sep 27, 2026
Merged

thedancingdeveloper merged 12 commits into
mainfrom
feat/nntp-observation-api

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Collaborator

Summary

Ports the bounded NNTP observation feature series from the MrVampy/rustnzb fork (7 commits) back onto main. It adds read-only, resource-bounded HTTP endpoints for inspecting Usenet groups/articles, backed by new fail-closed NNTP primitives. Every operation enforces hard caps (byte limits, article counts, deadlines) and retires the connection rather than exceeding a bound.

Main had no equivalent of any of this; the fork's own review flagged it as a clean, additive pull.

New HTTP endpoints (apps/rustnzb/src/group_observation/)

  • POST /groups/overview-range — byte-exact lossless XOVER over a bounded range
  • POST /groups/article-head — bounded HEAD fetch with SHA-256 digest
  • POST /groups/article-body-prefix — bounded body prefix (yEnc-aware)
  • POST /groups/article-availability — STAT-based availability, fail-closed
  • POST /groups/header-pattern / same-connection clear search — XPAT-accelerated with lossless XOVER fallback

All return a uniform {status: complete|blocked, ..., failure_code} envelope with typed nntp_* failure codes.

New NNTP primitives (crates/nzb-nntp/)

fetch_head_number, xover_lossless/xover_lossless_bounded, overview_format, xpat_bounded, fetch_body_prefix, BoundedResponse<T>, BodyPrefixResponse, a new overview.rs module (OverviewFormat, LosslessOverviewRow, DefectiveOverviewRow), two new NntpError variants (UnsupportedCommand, ResponseTooLarge), and STAT-pipeline fail-closed hardening.

Port notes

  • Cherry-picked the 7 cluster commits in chronological order. The only conflicts were internal crate-version bumps (resolved to main's current 1.5.0 versions) and one server.rs import line that referenced the fork's unrelated admissions module (dropped — that belongs to a separate PR).
  • The cluster was authored against sha2 0.10; main is on sha2 0.11, whose digest output no longer implements LowerHex. Final commit adapts the 6 digest sites to hex::encode(...) (the idiom already used in nzb-web::auth), which yields identical lowercase hex.

Tests

  • cargo test -p rustnzb --lib group_observation → 11 passed (bounded ranges, typed blockers, deadline receipts, digest-bound body prefix, XPAT-unsupported fallback, per-range receipts).
  • cargo test -p nzb-nntp → all passed (168 + suites; overview/lossless parsing, bounded fetch).
  • cargo check --workspace --all-targets, cargo clippy --workspace --all-targets, cargo fmt --check → all clean.

Original author: @MrVampy (commits preserved via cherry-pick -x).

🤖 Generated with Claude Code

MrVampy and others added 10 commits September 27, 2026 09:27
(cherry picked from commit 9e63738)
(cherry picked from commit baafa09)
The bounded NNTP observation cluster was authored against sha2 0.10 on the
fork. main is on sha2 0.11, whose digest output no longer implements
LowerHex, so `format!("{:x}", ...)` fails to compile.

- Add sha2 0.11 + hex to apps/rustnzb (matching main's crate versions)
- Replace `format!("{:x}", <digest>)` with `hex::encode(<digest>)` (the
  idiom already used in nzb-web auth), producing identical lowercase hex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop app depends on `rustnzb` by path, so the new `sha2`/`hex`
dependencies added for the ported NNTP observation API must be reflected in
desktop/src-tauri/Cargo.lock. The `desktop` CI job builds with `--locked` and
rejected the stale lock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/rustnzb/src/group_observation/body_prefix.rs Fixed
Comment thread apps/rustnzb/src/group_observation/clear_search.rs Fixed
Comment thread apps/rustnzb/src/group_observation/clear_search.rs Fixed
CodeQL flagged three "uncontrolled allocation size" sites in the bounded NNTP
observation endpoints where a Vec capacity was reserved from a request-derived
value. The values are already validated against hard constants in
group_observation::contract (max_payload_bytes <= MAX_PAYLOAD_PREFIX_BYTES;
ranges.len() <= MAX_CLEAR_SEARCH_RANGES), so these were false positives — but
clamp the capacity at each allocation with the authoritative constant so the
reservation is provably bounded regardless of upstream validation, and the
static analysis is satisfied.

- body_prefix::decode_payload_prefix: clamp capacity to MAX_PAYLOAD_PREFIX_BYTES
- clear_search range_rows / failed_response receipts: clamp to MAX_CLEAR_SEARCH_RANGES
- expose MAX_CLEAR_SEARCH_RANGES as pub(super)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/rustnzb/src/group_observation/body_prefix.rs Fixed
Comment thread apps/rustnzb/src/group_observation/clear_search.rs Fixed
Comment thread apps/rustnzb/src/group_observation/clear_search.rs Fixed
…oints

The previous `.min(CONST)` clamp did not satisfy CodeQL's Rust allocation-size
dataflow (it does not treat `min` with a constant as an upper bound). Since the
capacity was only a micro-optimization on already-bounded buffers, grow the
Vecs on demand instead:

- body_prefix::decode_payload_prefix: `Vec::new()` (the decode loop still caps
  output at the validated `maximum`).
- clear_search range_rows / failed_response receipts: `Vec::new()` (at most
  MAX_CLEAR_SEARCH_RANGES == 8 elements, enforced by validation).

Reverts the now-unused constant import/visibility from the prior attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thedancingdeveloper
thedancingdeveloper merged commit c6cf830 into main Sep 27, 2026
11 checks passed
@thedancingdeveloper
thedancingdeveloper deleted the feat/nntp-observation-api branch September 27, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants