fix(nntpd): support open-ended NNTP ranges in XOVER/OVER

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.
This commit is contained in:
Alex Nazaruk 2026-07-02 23:01:10 +02:00
parent 43dbdf0e06
commit 5fba77f295
2 changed files with 48 additions and 13 deletions

View file

@ -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)
}
}
}

View file

@ -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 {