quic: add rangeset type
A rangeset is an ordered list of non-overlapping int64 ranges.
This type will be used for tracking which packet numbers need to be
acknowledged and which parts of a stream have been sent/received.
For golang/go#58547
Change-Id: Ia4ab3a47e82d0e7aea738a0f857b2129d4ea1f63
Reviewed-on: https://go-review.googlesource.com/c/net/+/478295
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/quic/rangeset.go b/internal/quic/rangeset.go
new file mode 100644
index 0000000..9d6b63a
--- /dev/null
+++ b/internal/quic/rangeset.go
@@ -0,0 +1,183 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+// A rangeset is a set of int64s, stored as an ordered list of non-overlapping,
+// non-empty ranges.
+//
+// Rangesets are efficient for small numbers of ranges,
+// which is expected to be the common case.
+//
+// Once we're willing to drop support for pre-generics versions of Go, this can
+// be made into a parameterized type to permit use with packetNumber without casts.
+type rangeset []i64range
+
+type i64range struct {
+ start, end int64 // [start, end)
+}
+
+// size returns the size of the range.
+func (r i64range) size() int64 {
+ return r.end - r.start
+}
+
+// contains reports whether v is in the range.
+func (r i64range) contains(v int64) bool {
+ return r.start <= v && v < r.end
+}
+
+// add adds [start, end) to the set, combining it with existing ranges if necessary.
+func (s *rangeset) add(start, end int64) {
+ if start == end {
+ return
+ }
+ for i := range *s {
+ r := &(*s)[i]
+ if r.start > end {
+ // The new range comes before range i.
+ s.insertrange(i, start, end)
+ return
+ }
+ if start > r.end {
+ // The new range comes after range i.
+ continue
+ }
+ // The new range is adjacent to or overlapping range i.
+ if start < r.start {
+ r.start = start
+ }
+ if end <= r.end {
+ return
+ }
+ // Possibly coalesce subsquent ranges into range i.
+ r.end = end
+ j := i + 1
+ for ; j < len(*s) && r.end >= (*s)[j].start; j++ {
+ if e := (*s)[j].end; e > r.end {
+ // Range j ends after the new range.
+ r.end = e
+ }
+ }
+ s.removeranges(i+1, j)
+ return
+ }
+ *s = append(*s, i64range{start, end})
+}
+
+// sub removes [start, end) from the set.
+func (s *rangeset) sub(start, end int64) {
+ removefrom, removeto := -1, -1
+ for i := range *s {
+ r := &(*s)[i]
+ if end < r.start {
+ break
+ }
+ if r.end < start {
+ continue
+ }
+ switch {
+ case start <= r.start && end >= r.end:
+ // Remove the entire range.
+ if removefrom == -1 {
+ removefrom = i
+ }
+ removeto = i + 1
+ case start <= r.start:
+ // Remove a prefix.
+ r.start = end
+ case end >= r.end:
+ // Remove a suffix.
+ r.end = start
+ default:
+ // Remove the middle, leaving two new ranges.
+ rend := r.end
+ r.end = start
+ s.insertrange(i+1, end, rend)
+ return
+ }
+ }
+ if removefrom != -1 {
+ s.removeranges(removefrom, removeto)
+ }
+}
+
+// contains reports whether s contains v.
+func (s rangeset) contains(v int64) bool {
+ for _, r := range s {
+ if v >= r.end {
+ continue
+ }
+ if r.start <= v {
+ return true
+ }
+ return false
+ }
+ return false
+}
+
+// rangeContaining returns the range containing v, or the range [0,0) if v is not in s.
+func (s rangeset) rangeContaining(v int64) i64range {
+ for _, r := range s {
+ if v >= r.end {
+ continue
+ }
+ if r.start <= v {
+ return r
+ }
+ break
+ }
+ return i64range{0, 0}
+}
+
+// min returns the minimum value in the set, or 0 if empty.
+func (s rangeset) min() int64 {
+ if len(s) == 0 {
+ return 0
+ }
+ return s[0].start
+}
+
+// max returns the maximum value in the set, or 0 if empty.
+func (s rangeset) max() int64 {
+ if len(s) == 0 {
+ return 0
+ }
+ return s[len(s)-1].end - 1
+}
+
+// end returns the end of the last range in the set, or 0 if empty.
+func (s rangeset) end() int64 {
+ if len(s) == 0 {
+ return 0
+ }
+ return s[len(s)-1].end
+}
+
+// isrange reports if the rangeset covers exactly the range [start, end).
+func (s rangeset) isrange(start, end int64) bool {
+ switch len(s) {
+ case 0:
+ return start == 0 && end == 0
+ case 1:
+ return s[0].start == start && s[0].end == end
+ }
+ return false
+}
+
+// removeranges removes ranges [i,j).
+func (s *rangeset) removeranges(i, j int) {
+ if i == j {
+ return
+ }
+ copy((*s)[i:], (*s)[j:])
+ *s = (*s)[:len(*s)-(j-i)]
+}
+
+// insert adds a new range at index i.
+func (s *rangeset) insertrange(i int, start, end int64) {
+ *s = append(*s, i64range{})
+ copy((*s)[i+1:], (*s)[i:])
+ (*s)[i] = i64range{start, end}
+}
diff --git a/internal/quic/rangeset_test.go b/internal/quic/rangeset_test.go
new file mode 100644
index 0000000..2922848
--- /dev/null
+++ b/internal/quic/rangeset_test.go
@@ -0,0 +1,295 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestRangeSize(t *testing.T) {
+ for _, test := range []struct {
+ r i64range
+ want int64
+ }{{
+ r: i64range{0, 100},
+ want: 100,
+ }, {
+ r: i64range{10, 20},
+ want: 10,
+ }} {
+ if got := test.r.size(); got != test.want {
+ t.Errorf("%+v.size = %v, want %v", test.r, got, test.want)
+ }
+ }
+}
+
+func TestRangeContains(t *testing.T) {
+ r := i64range{5, 10}
+ for _, i := range []int64{0, 4, 10, 15} {
+ if r.contains(i) {
+ t.Errorf("%v.contains(%v) = true, want false", r, i)
+ }
+ }
+ for _, i := range []int64{5, 6, 7, 8, 9} {
+ if !r.contains(i) {
+ t.Errorf("%v.contains(%v) = false, want true", r, i)
+ }
+ }
+}
+
+func TestRangesetAdd(t *testing.T) {
+ for _, test := range []struct {
+ desc string
+ set rangeset
+ add i64range
+ want rangeset
+ }{{
+ desc: "add to empty set",
+ set: rangeset{},
+ add: i64range{0, 100},
+ want: rangeset{{0, 100}},
+ }, {
+ desc: "add empty range",
+ set: rangeset{},
+ add: i64range{100, 100},
+ want: rangeset{},
+ }, {
+ desc: "append nonadjacent range",
+ set: rangeset{{100, 200}},
+ add: i64range{300, 400},
+ want: rangeset{{100, 200}, {300, 400}},
+ }, {
+ desc: "prepend nonadjacent range",
+ set: rangeset{{100, 200}},
+ add: i64range{0, 50},
+ want: rangeset{{0, 50}, {100, 200}},
+ }, {
+ desc: "insert nonadjacent range",
+ set: rangeset{{100, 200}, {500, 600}},
+ add: i64range{300, 400},
+ want: rangeset{{100, 200}, {300, 400}, {500, 600}},
+ }, {
+ desc: "prepend adjacent range",
+ set: rangeset{{100, 200}},
+ add: i64range{50, 100},
+ want: rangeset{{50, 200}},
+ }, {
+ desc: "append adjacent range",
+ set: rangeset{{100, 200}},
+ add: i64range{200, 250},
+ want: rangeset{{100, 250}},
+ }, {
+ desc: "prepend overlapping range",
+ set: rangeset{{100, 200}},
+ add: i64range{50, 150},
+ want: rangeset{{50, 200}},
+ }, {
+ desc: "append overlapping range",
+ set: rangeset{{100, 200}},
+ add: i64range{150, 250},
+ want: rangeset{{100, 250}},
+ }, {
+ desc: "replace range",
+ set: rangeset{{100, 200}},
+ add: i64range{50, 250},
+ want: rangeset{{50, 250}},
+ }, {
+ desc: "prepend and combine",
+ set: rangeset{{100, 200}, {300, 400}, {500, 600}},
+ add: i64range{50, 300},
+ want: rangeset{{50, 400}, {500, 600}},
+ }, {
+ desc: "combine several ranges",
+ set: rangeset{{100, 200}, {300, 400}, {500, 600}, {700, 800}, {900, 1000}},
+ add: i64range{300, 850},
+ want: rangeset{{100, 200}, {300, 850}, {900, 1000}},
+ }} {
+ test := test
+ t.Run(test.desc, func(t *testing.T) {
+ got := test.set
+ got.add(test.add.start, test.add.end)
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("add [%v,%v) to %v", test.add.start, test.add.end, test.set)
+ t.Errorf(" got: %v", got)
+ t.Errorf(" want: %v", test.want)
+ }
+ })
+ }
+}
+
+func TestRangesetSub(t *testing.T) {
+ for _, test := range []struct {
+ desc string
+ set rangeset
+ sub i64range
+ want rangeset
+ }{{
+ desc: "subtract from empty set",
+ set: rangeset{},
+ sub: i64range{0, 100},
+ want: rangeset{},
+ }, {
+ desc: "subtract empty range",
+ set: rangeset{{0, 100}},
+ sub: i64range{0, 0},
+ want: rangeset{{0, 100}},
+ }, {
+ desc: "subtract not present in set",
+ set: rangeset{{0, 100}, {200, 300}},
+ sub: i64range{100, 200},
+ want: rangeset{{0, 100}, {200, 300}},
+ }, {
+ desc: "subtract prefix",
+ set: rangeset{{100, 200}},
+ sub: i64range{0, 150},
+ want: rangeset{{150, 200}},
+ }, {
+ desc: "subtract suffix",
+ set: rangeset{{100, 200}},
+ sub: i64range{150, 300},
+ want: rangeset{{100, 150}},
+ }, {
+ desc: "subtract middle",
+ set: rangeset{{0, 100}},
+ sub: i64range{40, 60},
+ want: rangeset{{0, 40}, {60, 100}},
+ }, {
+ desc: "subtract from two ranges",
+ set: rangeset{{0, 100}, {200, 300}},
+ sub: i64range{50, 250},
+ want: rangeset{{0, 50}, {250, 300}},
+ }, {
+ desc: "subtract removes range",
+ set: rangeset{{0, 100}, {200, 300}, {400, 500}},
+ sub: i64range{200, 300},
+ want: rangeset{{0, 100}, {400, 500}},
+ }, {
+ desc: "subtract removes multiple ranges",
+ set: rangeset{{0, 100}, {200, 300}, {400, 500}, {600, 700}},
+ sub: i64range{50, 650},
+ want: rangeset{{0, 50}, {650, 700}},
+ }, {
+ desc: "subtract only range",
+ set: rangeset{{0, 100}},
+ sub: i64range{0, 100},
+ want: rangeset{},
+ }} {
+ test := test
+ t.Run(test.desc, func(t *testing.T) {
+ got := test.set
+ got.sub(test.sub.start, test.sub.end)
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("sub [%v,%v) from %v", test.sub.start, test.sub.end, test.set)
+ t.Errorf(" got: %v", got)
+ t.Errorf(" want: %v", test.want)
+ }
+ })
+ }
+}
+
+func TestRangesetContains(t *testing.T) {
+ var s rangeset
+ s.add(10, 20)
+ s.add(30, 40)
+ for i := int64(0); i < 50; i++ {
+ want := (i >= 10 && i < 20) || (i >= 30 && i < 40)
+ if got := s.contains(i); got != want {
+ t.Errorf("%v.contains(%v) = %v, want %v", s, i, got, want)
+ }
+ }
+}
+
+func TestRangesetRangeContaining(t *testing.T) {
+ var s rangeset
+ s.add(10, 20)
+ s.add(30, 40)
+ for _, test := range []struct {
+ v int64
+ want i64range
+ }{
+ {0, i64range{0, 0}},
+ {9, i64range{0, 0}},
+ {10, i64range{10, 20}},
+ {15, i64range{10, 20}},
+ {19, i64range{10, 20}},
+ {20, i64range{0, 0}},
+ {29, i64range{0, 0}},
+ {30, i64range{30, 40}},
+ {39, i64range{30, 40}},
+ {40, i64range{0, 0}},
+ } {
+ got := s.rangeContaining(test.v)
+ if got != test.want {
+ t.Errorf("%v.rangeContaining(%v) = %v, want %v", s, test.v, got, test.want)
+ }
+ }
+}
+
+func TestRangesetLimits(t *testing.T) {
+ for _, test := range []struct {
+ s rangeset
+ wantMin int64
+ wantMax int64
+ wantEnd int64
+ }{{
+ s: rangeset{},
+ wantMin: 0,
+ wantMax: 0,
+ wantEnd: 0,
+ }, {
+ s: rangeset{{10, 20}},
+ wantMin: 10,
+ wantMax: 19,
+ wantEnd: 20,
+ }, {
+ s: rangeset{{10, 20}, {30, 40}, {50, 60}},
+ wantMin: 10,
+ wantMax: 59,
+ wantEnd: 60,
+ }} {
+ if got, want := test.s.min(), test.wantMin; got != want {
+ t.Errorf("%+v.min() = %v, want %v", test.s, got, want)
+ }
+ if got, want := test.s.max(), test.wantMax; got != want {
+ t.Errorf("%+v.max() = %v, want %v", test.s, got, want)
+ }
+ if got, want := test.s.end(), test.wantEnd; got != want {
+ t.Errorf("%+v.end() = %v, want %v", test.s, got, want)
+ }
+ }
+}
+
+func TestRangesetIsRange(t *testing.T) {
+ for _, test := range []struct {
+ s rangeset
+ r i64range
+ want bool
+ }{{
+ s: rangeset{{0, 100}},
+ r: i64range{0, 100},
+ want: true,
+ }, {
+ s: rangeset{{0, 100}},
+ r: i64range{0, 101},
+ want: false,
+ }, {
+ s: rangeset{{0, 10}, {11, 100}},
+ r: i64range{0, 100},
+ want: false,
+ }, {
+ s: rangeset{},
+ r: i64range{0, 0},
+ want: true,
+ }, {
+ s: rangeset{},
+ r: i64range{0, 1},
+ want: false,
+ }} {
+ if got := test.s.isrange(test.r.start, test.r.end); got != test.want {
+ t.Errorf("%+v.isrange(%v, %v) = %v, want %v", test.s, test.r.start, test.r.end, got, test.want)
+ }
+ }
+}