diff --git a/README.md b/README.md index db6693f..ac5772b 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## !!! ATTENTION, API HAS CHANGED +## !!! ATTENTION -The API has changed from v0.3.0 to v0.4.0. +API is currently not stable! ## Overview @@ -16,8 +16,7 @@ The API has changed from v0.3.0 to v0.4.0. The implementation is based on treaps, which have been augmented here for CIDRs. Treaps are randomized, self-balancing binary search trees. Due to the nature of treaps the lookups (readers) and the update (writer) can be easily decoupled. This is the perfect fit for a software router or firewall. -This package is a specialization of the more generic [interval package] of the same author, -but explicit for CIDRs. It has a narrow focus with a specialized API for IP routing tables. +This package is a specialization of the more generic [interval package] of the same author, but explicit for CIDRs. It has a narrow focus with a specialized API for IP routing tables. [interval package]: https://github.com/gaissmai/interval @@ -25,23 +24,23 @@ but explicit for CIDRs. It has a narrow focus with a specialized API for IP rout ```go import "github.com/gaissmai/cidrtree" - type Table struct { // Has unexported fields. } + type Table[V any] struct { // Has unexported fields. } Table is an IPv4 and IPv6 routing table. The zero value is ready to use. - func (t Table) Lookup(ip netip.Addr) (lpm netip.Prefix, value any, ok bool) - func (t Table) LookupPrefix(pfx netip.Prefix) (lpm netip.Prefix, value any, ok bool) + func (t Table[V]) Lookup(ip netip.Addr) (lpm netip.Prefix, value V, ok bool) + func (t Table[V]) LookupPrefix(pfx netip.Prefix) (lpm netip.Prefix, value V, ok bool) - func (t *Table) Insert(pfx netip.Prefix, val any) - func (t *Table) Delete(pfx netip.Prefix) bool - func (t *Table) Union(other Table) + func (t *Table[V]) Insert(pfx netip.Prefix, value V) + func (t *Table[V]) Delete(pfx netip.Prefix) bool + func (t *Table[V]) Union(other Table[V]) - func (t Table) InsertImmutable(pfx netip.Prefix, val any) *Table - func (t Table) DeleteImmutable(pfx netip.Prefix) (*Table, bool) - func (t Table) UnionImmutable(other Table) *Table - func (t Table) Clone() *Table + func (t Table[V]) InsertImmutable(pfx netip.Prefix, value V) *Table[V] + func (t Table[V]) DeleteImmutable(pfx netip.Prefix) (*Table[V], bool) + func (t Table[V]) UnionImmutable(other Table[V]) *Table[V] + func (t Table[V]) Clone() *Table[V] - func (t Table) String() string - func (t Table) Fprint(w io.Writer) error + func (t Table[V]) String() string + func (t Table[V]) Fprint(w io.Writer) error - func (t Table) Walk(cb func(pfx netip.Prefix, val any) bool) + func (t Table[V]) Walk(cb func(pfx netip.Prefix, value V) bool) ``` diff --git a/debug.go b/debug.go index 252f37d..9432252 100644 --- a/debug.go +++ b/debug.go @@ -10,7 +10,7 @@ import ( // fprintBST writes a horizontal tree diagram of the binary search tree (BST) to w. // // Note: This is for debugging purposes only. -func (t Table[T]) fprintBST(w io.Writer) error { +func (t Table[V]) fprintBST(w io.Writer) error { if t.root4 != nil { if _, err := fmt.Fprint(w, "R "); err != nil { return err @@ -33,7 +33,7 @@ func (t Table[T]) fprintBST(w io.Writer) error { } // fprintBST recursive helper. -func (n *node[T]) fprintBST(w io.Writer, pad string) error { +func (n *node[V]) fprintBST(w io.Writer, pad string) error { // stringify this node _, err := fmt.Fprintf(w, "%v [prio:%.4g] [subtree maxUpper: %v]\n", n.cidr, float64(n.prio)/math.MaxUint64, n.maxUpper.cidr) if err != nil { @@ -80,7 +80,7 @@ func (n *node[T]) fprintBST(w io.Writer, pad string) error { // If the skip function is not nil, a true return value defines which nodes must be skipped in the statistics. // // Note: This is for debugging and testing purposes only during development. -func (t Table[T]) statistics(skip func(netip.Prefix, any, int) bool) (size int, maxDepth int, average, deviation float64) { +func (t Table[V]) statistics(skip func(netip.Prefix, any, int) bool) (size int, maxDepth int, average, deviation float64) { // key is depth, value is the sum of nodes with this depth depths := make(map[int]int) @@ -120,7 +120,7 @@ func (t Table[T]) statistics(skip func(netip.Prefix, any, int) bool) (size int, } // walkWithDepth in ascending prefix order. -func (n *node[T]) walkWithDepth(cb func(netip.Prefix, any, int) bool, depth int) bool { +func (n *node[V]) walkWithDepth(cb func(netip.Prefix, any, int) bool, depth int) bool { if n == nil { return true } diff --git a/stringify.go b/stringify.go index 9c52576..e3cd394 100644 --- a/stringify.go +++ b/stringify.go @@ -7,7 +7,7 @@ import ( ) // String returns a hierarchical tree diagram of the ordered CIDRs as string, just a wrapper for [Tree.Fprint]. -func (t Table[T]) String() string { +func (t Table[V]) String() string { w := new(strings.Builder) _ = t.Fprint(w) return w.String() @@ -17,7 +17,7 @@ func (t Table[T]) String() string { // // The order from top to bottom is in ascending order of the start address // and the subtree structure is determined by the CIDRs coverage. -func (t Table[T]) Fprint(w io.Writer) error { +func (t Table[V]) Fprint(w io.Writer) error { if err := t.root4.fprint(w); err != nil { return err } @@ -27,16 +27,16 @@ func (t Table[T]) Fprint(w io.Writer) error { return nil } -func (n *node[T]) fprint(w io.Writer) error { +func (n *node[V]) fprint(w io.Writer) error { if n == nil { return nil } // pcm = parent-child-mapping - var pcm parentChildsMap[T] + var pcm parentChildsMap[V] // init map - pcm.pcMap = make(map[*node[T]][]*node[T]) + pcm.pcMap = make(map[*node[V]][]*node[V]) pcm = n.buildParentChildsMap(pcm) @@ -50,11 +50,11 @@ func (n *node[T]) fprint(w io.Writer) error { } // start recursion with root and empty padding - var root *node[T] + var root *node[V] return root.walkAndStringify(w, pcm, "") } -func (n *node[T]) walkAndStringify(w io.Writer, pcm parentChildsMap[T], pad string) error { +func (n *node[V]) walkAndStringify(w io.Writer, pcm parentChildsMap[V], pad string) error { // the prefix (pad + glyphe) is already printed on the line on upper level if n != nil { if _, err := fmt.Fprintf(w, "%v (%v)\n", n.cidr, n.value); err != nil { @@ -98,7 +98,7 @@ type parentChildsMap[T any] struct { } // buildParentChildsMap, in-order traversal -func (n *node[T]) buildParentChildsMap(pcm parentChildsMap[T]) parentChildsMap[T] { +func (n *node[V]) buildParentChildsMap(pcm parentChildsMap[V]) parentChildsMap[V] { if n == nil { return pcm } @@ -114,7 +114,7 @@ func (n *node[T]) buildParentChildsMap(pcm parentChildsMap[T]) parentChildsMap[T } // pcmForNode, find parent in stack, remove cidrs from stack, put this cidr on stack. -func (n *node[T]) pcmForNode(pcm parentChildsMap[T]) parentChildsMap[T] { +func (n *node[V]) pcmForNode(pcm parentChildsMap[V]) parentChildsMap[V] { // if this cidr is covered by a prev cidr on stack for j := len(pcm.stack) - 1; j >= 0; j-- { that := pcm.stack[j] diff --git a/treap.go b/treap.go index 4c96ac4..4f6a0bc 100644 --- a/treap.go +++ b/treap.go @@ -16,51 +16,165 @@ import ( ) // Table is an IPv4 and IPv6 routing table. The zero value is ready to use. -type Table[T any] struct { +type Table[V any] struct { // make a treap for every IP version, the bits of the prefix are part of the weighted priority - root4 *node[T] - root6 *node[T] + root4 *node[V] + root6 *node[V] } // node is the recursive data structure of the treap. -type node[T any] struct { - maxUpper *node[T] // augment the treap, see also recalc() - left *node[T] - right *node[T] - value T +type node[V any] struct { + maxUpper *node[V] // augment the treap, see also recalc() + left *node[V] + right *node[V] + value V cidr netip.Prefix prio uint64 } -// Insert adds pfx to the table with value val, changing the original table. -// If pfx is already present in the table, its value is set to val. -func (t *Table[T]) Insert(pfx netip.Prefix, val T) { +// Lookup returns the longest-prefix-match (lpm) for given ip. +// If the ip isn't covered by any CIDR, the zero value and false is returned. +// +// Lookup does not allocate memory. +func (t Table[V]) Lookup(ip netip.Addr) (lpm netip.Prefix, value V, ok bool) { + if ip.Is4() { + // don't return the depth + lpm, value, ok, _ = t.root4.lpmIP(ip, 0) + return + } + // don't return the depth + lpm, value, ok, _ = t.root6.lpmIP(ip, 0) + return +} + +// LookupPrefix returns the longest-prefix-match (lpm) for given prefix. +// If the prefix isn't equal or covered by any CIDR in the table, the zero value and false is returned. +// +// LookupPrefix does not allocate memory. +func (t Table[V]) LookupPrefix(pfx netip.Prefix) (lpm netip.Prefix, value V, ok bool) { + pfx = pfx.Masked() // always canonicalize! + + if pfx.Addr().Is4() { + // don't return the depth + lpm, value, ok, _ = t.root4.lpmCIDR(pfx, 0) + return + } + // don't return the depth + lpm, value, ok, _ = t.root6.lpmCIDR(pfx, 0) + return +} + +// Insert adds pfx to the routing table with value of generic type V. +// If pfx is already present in the table, its value is set to the new value. +func (t *Table[V]) Insert(pfx netip.Prefix, value V) { pfx = pfx.Masked() // always canonicalize! if pfx.Addr().Is4() { - t.root4 = t.root4.insert(makeNode(pfx, val), false) + t.root4 = t.root4.insert(makeNode(pfx, value), false) return } - t.root6 = t.root6.insert(makeNode(pfx, val), false) + t.root6 = t.root6.insert(makeNode(pfx, value), false) } -// InsertImmutable adds pfx to the table with value val, returning a new table. -// If pfx is already present in the table, its value is set to val. -func (t Table[T]) InsertImmutable(pfx netip.Prefix, val T) *Table[T] { +// InsertImmutable adds pfx to the table with value of generic type V, returning a new table. +// If pfx is already present in the table, its value is set to the new value. +func (t Table[V]) InsertImmutable(pfx netip.Prefix, value V) *Table[V] { pfx = pfx.Masked() // always canonicalize! if pfx.Addr().Is4() { - t.root4 = t.root4.insert(makeNode(pfx, val), true) + t.root4 = t.root4.insert(makeNode(pfx, value), true) return &t } - t.root6 = t.root6.insert(makeNode(pfx, val), true) + t.root6 = t.root6.insert(makeNode(pfx, value), true) + return &t +} + +// Delete removes the prefix from table, returns true if it exists, false otherwise. +func (t *Table[V]) Delete(pfx netip.Prefix) bool { + pfx = pfx.Masked() // always canonicalize! + + is4 := pfx.Addr().Is4() + + n := t.root6 + if is4 { + n = t.root4 + } + + // split/join is set to mutable + l, m, r := n.split(pfx, false) + n = l.join(r, false) + + if is4 { + t.root4 = n + } else { + t.root6 = n + } + + return m != nil +} + +// DeleteImmutable removes the prefix if it exists, returns the new table and true, false if not found. +func (t Table[V]) DeleteImmutable(pfx netip.Prefix) (*Table[V], bool) { + pfx = pfx.Masked() // always canonicalize! + + is4 := pfx.Addr().Is4() + + n := t.root6 + if is4 { + n = t.root4 + } + + // split/join is set to immutable + l, m, r := n.split(pfx, true) + n = l.join(r, true) + + if is4 { + t.root4 = n + } else { + t.root6 = n + } + + ok := m != nil + return &t, ok +} + +// Clone, deep cloning of the routing table. +func (t Table[V]) Clone() *Table[V] { + t.root4 = t.root4.clone() + t.root6 = t.root6.clone() + return &t +} + +// Union combines two tables, changing the receiver table. +// If there are duplicate entries, the value is taken from the other table. +func (t *Table[V]) Union(other Table[V]) { + t.root4 = t.root4.union(other.root4, true, false) + t.root6 = t.root6.union(other.root6, true, false) +} + +// UnionImmutable combines any two tables immutable and returns the combined table. +// If there are duplicate entries, the value is taken from the other table. +func (t Table[V]) UnionImmutable(other Table[V]) *Table[V] { + t.root4 = t.root4.union(other.root4, true, true) + t.root6 = t.root6.union(other.root6, true, true) return &t } +// Walk iterates the cidrtree in ascending order. +// The callback function is called with the prefix and value of the respective node and the depth in the tree. +// If callback returns `false`, the iteration is aborted. +func (t Table[V]) Walk(cb func(pfx netip.Prefix, value V) bool) { + if !t.root4.walk(cb) { + return + } + + t.root6.walk(cb) +} + // insert into treap, changing nodes are copied, new treap is returned, // old treap is modified if immutable is false. // If node is already present in the table, its value is set to val. -func (n *node[T]) insert(m *node[T], immutable bool) *node[T] { +func (n *node[V]) insert(m *node[V], immutable bool) *node[V] { if n == nil { // recursion stop condition return m @@ -129,73 +243,9 @@ func (n *node[T]) insert(m *node[T], immutable bool) *node[T] { return n } -// DeleteImmutable removes the prefix if it exists, returns the new table and true, false if not found. -func (t Table[T]) DeleteImmutable(pfx netip.Prefix) (*Table[T], bool) { - pfx = pfx.Masked() // always canonicalize! - - is4 := pfx.Addr().Is4() - - n := t.root6 - if is4 { - n = t.root4 - } - - // split/join must be immutable - l, m, r := n.split(pfx, true) - n = l.join(r, true) - - if is4 { - t.root4 = n - } else { - t.root6 = n - } - - ok := m != nil - return &t, ok -} - -// Delete removes the prefix from table, returns true if it exists, false otherwise. -func (t *Table[T]) Delete(pfx netip.Prefix) bool { - pfx = pfx.Masked() // always canonicalize! - - is4 := pfx.Addr().Is4() - - n := t.root6 - if is4 { - n = t.root4 - } - - // split/join is mutable - l, m, r := n.split(pfx, false) - n = l.join(r, false) - - if is4 { - t.root4 = n - } else { - t.root6 = n - } - - return m != nil -} - -// UnionImmutable combines any two tables immutable and returns the combined table. -// If there are duplicate entries, the value is taken from the other table. -func (t Table[T]) UnionImmutable(other Table[T]) *Table[T] { - t.root4 = t.root4.union(other.root4, true, true) - t.root6 = t.root6.union(other.root6, true, true) - return &t -} - -// Union combines two tables, changing the receiver table. -// If there are duplicate entries, the value is taken from the other table. -func (t *Table[T]) Union(other Table[T]) { - t.root4 = t.root4.union(other.root4, true, false) - t.root6 = t.root6.union(other.root6, true, false) -} - // union two treaps. // flag overwrite isn't public but needed as input for rec-descent calls, see below when trepa are swapped. -func (n *node[T]) union(b *node[T], overwrite bool, immutable bool) *node[T] { +func (n *node[V]) union(b *node[V], overwrite bool, immutable bool) *node[V] { // recursion stop condition if n == nil { return b @@ -234,19 +284,8 @@ func (n *node[T]) union(b *node[T], overwrite bool, immutable bool) *node[T] { return n } -// Walk iterates the cidrtree in ascending order. -// The callback function is called with the prefix and value of the respective node and the depth in the tree. -// If callback returns `false`, the iteration is aborted. -func (t Table[T]) Walk(cb func(pfx netip.Prefix, val T) bool) { - if !t.root4.walk(cb) { - return - } - - t.root6.walk(cb) -} - // walk tree in ascending prefix order. -func (n *node[T]) walk(cb func(netip.Prefix, T) bool) bool { +func (n *node[V]) walk(cb func(netip.Prefix, V) bool) bool { if n == nil { return true } @@ -269,23 +308,8 @@ func (n *node[T]) walk(cb func(netip.Prefix, T) bool) bool { return true } -// Lookup returns the longest-prefix-match (lpm) for given ip. -// If the ip isn't covered by any CIDR, the zero value and false is returned. -// -// Lookup does not allocate memory. -func (t Table[T]) Lookup(ip netip.Addr) (lpm netip.Prefix, value T, ok bool) { - if ip.Is4() { - // don't return the depth - lpm, value, ok, _ = t.root4.lpmIP(ip, 0) - return - } - // don't return the depth - lpm, value, ok, _ = t.root6.lpmIP(ip, 0) - return -} - // lpmIP rec-descent -func (n *node[T]) lpmIP(ip netip.Addr, depth int) (lpm netip.Prefix, value T, ok bool, atDepth int) { +func (n *node[V]) lpmIP(ip netip.Addr, depth int) (lpm netip.Prefix, value V, ok bool, atDepth int) { for { // recursion stop condition if n == nil { @@ -322,25 +346,8 @@ func (n *node[T]) lpmIP(ip netip.Addr, depth int) (lpm netip.Prefix, value T, ok return n.left.lpmIP(ip, depth+1) } -// LookupPrefix returns the longest-prefix-match (lpm) for given prefix. -// If the prefix isn't equal or covered by any CIDR in the table, the zero value and false is returned. -// -// LookupPrefix does not allocate memory. -func (t Table[T]) LookupPrefix(pfx netip.Prefix) (lpm netip.Prefix, value T, ok bool) { - pfx = pfx.Masked() // always canonicalize! - - if pfx.Addr().Is4() { - // don't return the depth - lpm, value, ok, _ = t.root4.lpmCIDR(pfx, 0) - return - } - // don't return the depth - lpm, value, ok, _ = t.root6.lpmCIDR(pfx, 0) - return -} - // lpmCIDR rec-descent -func (n *node[T]) lpmCIDR(pfx netip.Prefix, depth int) (lpm netip.Prefix, value T, ok bool, atDepth int) { +func (n *node[V]) lpmCIDR(pfx netip.Prefix, depth int) (lpm netip.Prefix, value V, ok bool, atDepth int) { for { // recursion stop condition if n == nil { @@ -392,14 +399,7 @@ func (n *node[T]) lpmCIDR(pfx netip.Prefix, depth int) (lpm netip.Prefix, value return n.left.lpmCIDR(pfx, depth+1) } -// Clone, deep cloning of the routing table. -func (t Table[T]) Clone() *Table[T] { - t.root4 = t.root4.clone() - t.root6 = t.root6.clone() - return &t -} - -func (n *node[T]) clone() *node[T] { +func (n *node[V]) clone() *node[V] { if n == nil { return n } @@ -421,7 +421,7 @@ func (n *node[T]) clone() *node[T] { // and greater-than the provided cidr (BST key). The resulting nodes are // properly formed treaps or nil. // If the split must be immutable, first copy concerned nodes. -func (n *node[T]) split(cidr netip.Prefix, immutable bool) (left, mid, right *node[T]) { +func (n *node[V]) split(cidr netip.Prefix, immutable bool) (left, mid, right *node[V]) { // recursion stop condition if n == nil { return nil, nil, nil @@ -472,7 +472,7 @@ func (n *node[T]) split(cidr netip.Prefix, immutable bool) (left, mid, right *no // join combines two disjunct treaps. All nodes in treap n have keys <= that of treap m // for this algorithm to work correctly. If the join must be immutable, first copy concerned nodes. -func (n *node[T]) join(m *node[T], immutable bool) *node[T] { +func (n *node[V]) join(m *node[V], immutable bool) *node[V] { // recursion stop condition if n == nil { return m @@ -511,17 +511,17 @@ func (n *node[T]) join(m *node[T], immutable bool) *node[T] { // ########################################################### // makeNode, create new node with cidr. -func makeNode[T any](pfx netip.Prefix, val T) *node[T] { - n := new(node[T]) +func makeNode[V any](pfx netip.Prefix, value V) *node[V] { + n := new(node[V]) n.cidr = pfx.Masked() // always store the prefix in normalized form - n.value = val + n.value = value n.prio = mrand.Uint64() n.recalc() // init the augmented field with recalc return n } // copyNode, make a shallow copy of the pointers and the cidr. -func (n *node[T]) copyNode() *node[T] { +func (n *node[V]) copyNode() *node[V] { c := *n return &c } @@ -529,7 +529,7 @@ func (n *node[T]) copyNode() *node[T] { // recalc the augmented fields in treap node after each creation/modification // with values in descendants. // Only one level deeper must be considered. The treap datastructure is very easy to augment. -func (n *node[T]) recalc() { +func (n *node[V]) recalc() { if n == nil { return } diff --git a/whitebox_test.go b/whitebox_test.go index 9ea4c06..df6eae3 100644 --- a/whitebox_test.go +++ b/whitebox_test.go @@ -12,7 +12,7 @@ import ( "testing" ) -func TestFprintBST(t *testing.T) { +func TestFprintBSTVerbose(t *testing.T) { rtbl := new(Table[any]) for i := 1; i <= 48; i++ { rtbl.Insert(randPfx4(), nil) @@ -37,7 +37,7 @@ func TestFprintBST(t *testing.T) { t.Log(w.String()) } -func TestStatisticsRandom(t *testing.T) { +func TestStatisticsRandomVerbose(t *testing.T) { for i := 10; i <= 100_000; i *= 10 { rtbl := new(Table[any]) for c := 0; c <= i; c++ { @@ -56,7 +56,7 @@ func TestStatisticsRandom(t *testing.T) { } } -func TestStatisticsFullTable(t *testing.T) { +func TestStatisticsFullTableVerbose(t *testing.T) { rtbl := new(Table[any]) for _, cidr := range fullTable { rtbl.Insert(cidr, nil) @@ -72,7 +72,7 @@ func TestStatisticsFullTable(t *testing.T) { t.Logf("FullTable: size: %10d, maxDepth: %4d, average: %3.2f, deviation: %3.2f", size, maxDepth, average, deviation) } -func TestLPMRandom(t *testing.T) { +func TestLPMRandomVerbose(t *testing.T) { var size int var depth int var maxDepth int @@ -97,7 +97,7 @@ func TestLPMRandom(t *testing.T) { } } -func TestLPMFullTableWithDefaultRoutes(t *testing.T) { +func TestLPMFullTableWithDefaultRoutesVerbose(t *testing.T) { var size int var depth int var maxDepth int