From 5fba77f295f248304bfc09b137b7f5dff960a0a6 Mon Sep 17 00:00:00 2001 From: Alex Nazaruk Date: Thu, 2 Jul 2026 23:01:10 +0200 Subject: [PATCH] fix(nntpd): support open-ended NNTP ranges in XOVER/OVER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseRange split the spec on '-' and required BOTH bounds to parse as integers. For a valid open-ended range like '5-' (RFC 3977 6.2.3 / RFC 2980 XOVER: article 5 through the last) strconv.ParseInt('') fails, so parseRange returned (0, 0) and handleOver asked the backend for articles in [0, 0] — zero rows. '-10' (first..10) was likewise broken. Treat an empty bound as the extreme (low 0 / high MaxInt64) while still returning an empty range for genuinely malformed specs. Add table-driven tests for the open-ended and malformed forms. --- internal/news/nntpd/range_test.go | 29 +++++++++++++++++++++++++++- internal/news/nntpd/server.go | 32 +++++++++++++++++++------------ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/internal/news/nntpd/range_test.go b/internal/news/nntpd/range_test.go index 7cd35bf..c9eddef 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,27 @@ func TestParseRangeSingleArticle(t *testing.T) { t.Fatalf(`parseRange("5") = %d, %d; want 5, 5`, low, high) } } + +func TestParseRange(t *testing.T) { + // spec -> (low, high). Empty bounds mean the extreme; malformed -> (0, 0). + cases := []struct { + spec string + low int64 + high int64 + }{ + {"5", 5, 5}, + {"5-10", 5, 10}, + {"5-", 5, math.MaxInt64}, + {"-10", 0, 10}, + {"", 0, math.MaxInt64}, + {"abc", 0, 0}, + {"5-abc", 0, 0}, + {"abc-10", 0, 0}, + } + for _, c := range cases { + low, high := parseRange(c.spec) + if low != c.low || high != c.high { + t.Errorf("parseRange(%q) = %d, %d; want %d, %d", c.spec, low, high, c.low, c.high) + } + } +} diff --git a/internal/news/nntpd/server.go b/internal/news/nntpd/server.go index 8ac5a50..4a7998d 100644 --- a/internal/news/nntpd/server.go +++ b/internal/news/nntpd/server.go @@ -165,27 +165,35 @@ func (s *Server) Process(nc net.Conn) { } } +// parseRange parses an NNTP article range spec (RFC 3977 §6.2.3 / RFC 2980). +// An empty bound means "the extreme": "5-" is article 5 through the last +// article, "-10" is the first article through 10, "5-10" is 5..10, "5" is the +// single article 5, and "" is all articles. A genuinely malformed bound yields +// an empty range (0, 0). func parseRange(spec string) (low, high int64) { if spec == "" { return 0, math.MaxInt64 } - parts := strings.Split(spec, "-") - if len(parts) == 1 { - h, err := strconv.ParseInt(parts[0], 10, 64) + parts := strings.SplitN(spec, "-", 2) + low, high = 0, math.MaxInt64 + if parts[0] != "" { + v, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { - return 0, 0 // malformed — empty range instead of all articles + return 0, 0 // malformed low bound — empty range } - return h, h + low = v } - l, err := strconv.ParseInt(parts[0], 10, 64) - if err != nil { - return 0, 0 // malformed — empty range + if len(parts) == 1 { // single article, e.g. "5" + return low, low } - h, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return 0, 0 // malformed — empty range + if parts[1] != "" { + v, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return 0, 0 // malformed high bound — empty range + } + high = v } - return l, h + return low, high } func handleOver(args []string, s *session, c *textproto.Conn) error {