From c947fd9bf57bd3452d1e7fcc88116f69879c569b Mon Sep 17 00:00:00 2001 From: johnanleitner1-Coder Date: Mon, 29 Jun 2026 16:28:57 -0700 Subject: [PATCH] fix(nntp): support open-ended OVER/XOVER ranges ("n-") parseRange treated a range of the form "low-" (low article to the highest available) as malformed and returned an empty (0,0) range, so OVER/XOVER "n-" returned no articles instead of every article >= n. RFC 3977 defines a range as a single number, "number-", or "number-number"; the open-ended form is common in real NNTP clients. Handle "low-" as (low, MaxInt64), reject specs with more than one dash (e.g. "1-2-3") as malformed, and keep existing malformed handling so a bad range still yields an empty range rather than "all articles". Adds tests for closed, open-ended, empty, and malformed specs. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/news/nntpd/range_test.go | 40 ++++++++++++++++++++++++++++++- internal/news/nntpd/server.go | 9 +++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/news/nntpd/range_test.go b/internal/news/nntpd/range_test.go index 7cd35bf..96b0ffe 100644 --- a/internal/news/nntpd/range_test.go +++ b/internal/news/nntpd/range_test.go @@ -1,6 +1,9 @@ package nntpd -import "testing" +import ( + "math" + "testing" +) func TestParseRangeSingleArticle(t *testing.T) { low, high := parseRange("5") @@ -8,3 +11,38 @@ func TestParseRangeSingleArticle(t *testing.T) { t.Fatalf(`parseRange("5") = %d, %d; want 5, 5`, low, high) } } + +func TestParseRangeClosed(t *testing.T) { + low, high := parseRange("5-10") + if low != 5 || high != 10 { + t.Fatalf(`parseRange("5-10") = %d, %d; want 5, 10`, low, high) + } +} + +// "low-" is a valid open-ended range meaning "from low to the highest +// article" (RFC 3977). It must not be treated as a malformed empty range, +// otherwise OVER/XOVER "n-" returns nothing instead of all articles >= n. +func TestParseRangeOpenEnded(t *testing.T) { + low, high := parseRange("5-") + if low != 5 || high != math.MaxInt64 { + t.Fatalf(`parseRange("5-") = %d, %d; want 5, %d`, low, high, int64(math.MaxInt64)) + } +} + +// Empty spec means "all articles". +func TestParseRangeEmptyIsAll(t *testing.T) { + low, high := parseRange("") + if low != 0 || high != math.MaxInt64 { + t.Fatalf(`parseRange("") = %d, %d; want 0, %d`, low, high, int64(math.MaxInt64)) + } +} + +// Malformed specs must yield an empty range (0,0), never "all articles". +func TestParseRangeMalformed(t *testing.T) { + for _, spec := range []string{"abc", "5-abc", "abc-5", "1-2-3", "-", "-5"} { + low, high := parseRange(spec) + if low != 0 || high != 0 { + t.Fatalf(`parseRange(%q) = %d, %d; want 0, 0`, spec, low, high) + } + } +} diff --git a/internal/news/nntpd/server.go b/internal/news/nntpd/server.go index 8ac5a50..9ce47fb 100644 --- a/internal/news/nntpd/server.go +++ b/internal/news/nntpd/server.go @@ -177,10 +177,19 @@ func parseRange(spec string) (low, high int64) { } return h, h } + if len(parts) != 2 { + return 0, 0 // malformed (e.g. "1-2-3") — empty range + } l, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return 0, 0 // malformed — empty range } + // Open-ended range "low-" means "from low to the highest article" + // (RFC 3977: a range is a single number, "number-", or + // "number-number"). An empty upper bound is unbounded, not malformed. + if parts[1] == "" { + return l, math.MaxInt64 + } h, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return 0, 0 // malformed — empty range