Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+337
@@ -0,0 +1,337 @@
|
||||
// Copyright (c) 2026 Karl Gaissmaier
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gaissmai/bart/internal/art"
|
||||
)
|
||||
|
||||
// strideLen represents the byte stride length for the multibit trie.
|
||||
// Each stride processes 8 bits (1 byte) at a time.
|
||||
const strideLen = 8
|
||||
|
||||
// MaxItems defines the maximum number of prefixes or children that can be stored in a single node.
|
||||
// This corresponds to 256 possible values for an 8-bit stride.
|
||||
const MaxItems = 256
|
||||
|
||||
// MaxTreeDepth represents the maximum depth of the trie structure.
|
||||
// For IPv6 addresses, this allows up to 16 bytes of depth.
|
||||
const MaxTreeDepth = 16
|
||||
|
||||
// DepthMask is used for bounds check elimination (BCE) when accessing depth-indexed arrays.
|
||||
const DepthMask = MaxTreeDepth - 1
|
||||
|
||||
// StridePath represents a path through the trie, with a maximum depth of 16 octets for IPv6.
|
||||
type StridePath [MaxTreeDepth]uint8
|
||||
|
||||
// TrieItem, a node has no path information about its predecessors,
|
||||
// we collect this during the recursive descent.
|
||||
type TrieItem[V any] struct {
|
||||
// for traversing, Path/Depth/Idx is needed to get the CIDR back from the trie.
|
||||
Node any // BartNode, FastNode, LiteNode
|
||||
Is4 bool
|
||||
Path StridePath
|
||||
Depth int
|
||||
Idx uint8
|
||||
|
||||
// for printing
|
||||
Cidr netip.Prefix
|
||||
Val V
|
||||
}
|
||||
|
||||
// StatsT, only used for dump, tests and benchmarks
|
||||
type StatsT struct {
|
||||
Prefixes int
|
||||
Children int
|
||||
SubNodes int
|
||||
Leaves int
|
||||
Fringes int
|
||||
}
|
||||
|
||||
type nodeType byte
|
||||
|
||||
const (
|
||||
nullNode nodeType = iota // empty node
|
||||
fullNode // prefixes and children or path-compressed prefixes
|
||||
halfNode // no prefixes, only children and path-compressed prefixes
|
||||
pathNode // only children, no prefix nor path-compressed prefixes
|
||||
stopNode // no children, only prefixes or path-compressed prefixes
|
||||
)
|
||||
|
||||
// String implements Stringer for nodeType.
|
||||
func (nt nodeType) String() string {
|
||||
switch nt {
|
||||
case nullNode:
|
||||
return "NULL"
|
||||
case fullNode:
|
||||
return "FULL"
|
||||
case halfNode:
|
||||
return "HALF"
|
||||
case pathNode:
|
||||
return "PATH"
|
||||
case stopNode:
|
||||
return "STOP"
|
||||
default:
|
||||
return "unreachable"
|
||||
}
|
||||
}
|
||||
|
||||
// addrFmt, different format strings for IPv4 and IPv6, decimal versus hex.
|
||||
func addrFmt(addr byte, is4 bool) string {
|
||||
if is4 {
|
||||
return fmt.Sprintf("%d", addr)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0x%02x", addr)
|
||||
}
|
||||
|
||||
// ip stride path, different formats for IPv4 and IPv6, dotted decimal or hex.
|
||||
//
|
||||
// 127.0.0
|
||||
// 2001:0d
|
||||
func ipStridePath(path StridePath, depth int, is4 bool) string {
|
||||
buf := new(strings.Builder)
|
||||
|
||||
if is4 {
|
||||
for i, b := range path[:depth] {
|
||||
if i != 0 {
|
||||
buf.WriteString(".")
|
||||
}
|
||||
|
||||
buf.WriteString(strconv.Itoa(int(b)))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
for i, b := range path[:depth] {
|
||||
if i != 0 && i%2 == 0 {
|
||||
buf.WriteString(":")
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "%02x", b)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CmpPrefix, helper function, compare func for prefix sort,
|
||||
// all cidrs are already normalized
|
||||
func CmpPrefix(a, b netip.Prefix) int {
|
||||
if cmpAddr := a.Addr().Compare(b.Addr()); cmpAddr != 0 {
|
||||
return cmpAddr
|
||||
}
|
||||
|
||||
return cmp.Compare(a.Bits(), b.Bits())
|
||||
}
|
||||
|
||||
// LeafNode represents a path-compressed routing entry that stores both prefix and value.
|
||||
// Leaf nodes are used when a prefix doesn't align with trie stride boundaries
|
||||
// and needs to be stored as a compressed path to save memory.
|
||||
type LeafNode[V any] struct {
|
||||
Value V
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
|
||||
// NewLeafNode creates a new leaf node with the specified prefix and value.
|
||||
func NewLeafNode[V any](pfx netip.Prefix, val V) *LeafNode[V] {
|
||||
return &LeafNode[V]{Prefix: pfx, Value: val}
|
||||
}
|
||||
|
||||
// FringeNode represents a path-compressed routing entry that stores only a value.
|
||||
// The prefix is implicitly defined by the node's position in the trie.
|
||||
// Fringe nodes are used for prefixes that align exactly with stride boundaries
|
||||
// (/8, /16, /24, etc.) to save memory by not storing redundant prefix information.
|
||||
type FringeNode[V any] struct {
|
||||
Value V
|
||||
}
|
||||
|
||||
// NewFringeNode creates a new fringe node with the specified value.
|
||||
func NewFringeNode[V any](val V) *FringeNode[V] {
|
||||
return &FringeNode[V]{Value: val}
|
||||
}
|
||||
|
||||
// IsFringe determines whether a prefix qualifies as a "FringeNode".
|
||||
// Only prefixes that are stride-aligned (i.e., /8, /16, ..., /128)
|
||||
// can be fringe-compressed. If these prefixes are inserted at a position
|
||||
// where depth == (strideCount-1), they are treated as FringeNodes;
|
||||
// at positions where depth < (strideCount-1), they are treated as LeafNodes.
|
||||
//
|
||||
// Example for a stride-aligned prefix like 192.168.1.0/24 (strideCount = 3, modBits = 0):
|
||||
//
|
||||
// depth = 3, depth == strideCount : A direct prefix with 0/0 (default route for subtrie).
|
||||
// depth = 2, depth == (strideCount-1) : A path-compressed fringe.
|
||||
// depth < 2, depth < (strideCount-1) : A path-compressed leaf.
|
||||
func IsFringe(depth int, pfxLen int) bool {
|
||||
strideCount, modBits := DivMod8(pfxLen)
|
||||
return depth == strideCount-1 && modBits == 0
|
||||
}
|
||||
|
||||
// CmpIndexRank, sort indexes in prefix sort order.
|
||||
func CmpIndexRank(aIdx, bIdx uint8) int {
|
||||
// convert idx [1..255] to prefix
|
||||
aOctet, aBits := art.IdxToPfx(aIdx)
|
||||
bOctet, bBits := art.IdxToPfx(bIdx)
|
||||
|
||||
// cmp the prefixes, first by address and then by bits
|
||||
if aOctet == bOctet {
|
||||
return cmp.Compare(aBits, bBits)
|
||||
}
|
||||
return cmp.Compare(aOctet, bOctet)
|
||||
}
|
||||
|
||||
// CidrFromPath reconstructs a CIDR prefix from a stride path, depth, and index.
|
||||
// The prefix is determined by the node's position in the trie and the base index
|
||||
// from the ART algorithm's complete binary tree representation.
|
||||
//
|
||||
// Parameters:
|
||||
// - path: The stride path through the trie
|
||||
// - depth: Current depth in the trie
|
||||
// - is4: True for IPv4 processing, false for IPv6
|
||||
// - idx: The base index from the prefix table
|
||||
//
|
||||
// Returns the reconstructed netip.Prefix.
|
||||
func CidrFromPath(path StridePath, depth int, is4 bool, idx uint8) netip.Prefix {
|
||||
depth &= DepthMask // BCE
|
||||
|
||||
// retrieve the last octet and pfxLen
|
||||
octet, pfxLen := art.IdxToPfx(idx)
|
||||
|
||||
// set byte in path at depth with last octet
|
||||
path[depth] = octet
|
||||
|
||||
// canonicalize
|
||||
clear(path[depth+1:])
|
||||
|
||||
// make ip addr from octets
|
||||
var ip netip.Addr
|
||||
if is4 {
|
||||
ip = netip.AddrFrom4([4]byte(path[:4]))
|
||||
} else {
|
||||
ip = netip.AddrFrom16(path)
|
||||
}
|
||||
|
||||
// calc bits with pathLen and pfxLen
|
||||
bits := depth<<3 + int(pfxLen)
|
||||
|
||||
// PrefixFrom does not allocate and does not mask off the host bits of ip.
|
||||
// With the clear(), the non-canonical bytes have already been removed.
|
||||
return netip.PrefixFrom(ip, bits)
|
||||
}
|
||||
|
||||
// CidrForFringe reconstructs a CIDR prefix for a fringe node from the traversal path.
|
||||
// Since fringe nodes don't store their prefix explicitly, it's derived entirely
|
||||
// from the node's position in the trie and its final byte value.
|
||||
//
|
||||
// Parameters:
|
||||
// - octets: The path of previous bytes leading up to the fringe.
|
||||
// - depth: Current depth in the trie (which equals strideCount - 1).
|
||||
// - is4: True for IPv4 processing, false for IPv6.
|
||||
// - fringeByte: The actual 8-bit value (0-255) of the prefix at this final stride.
|
||||
//
|
||||
// Returns the reconstructed netip.Prefix for the fringe.
|
||||
func CidrForFringe(octets []byte, depth int, is4 bool, fringeByte uint8) netip.Prefix {
|
||||
depth &= DepthMask // BCE
|
||||
|
||||
var path StridePath
|
||||
copy(path[:], octets)
|
||||
path[depth] = fringeByte
|
||||
|
||||
// canonicalize, fringe bit boundaries are always a multiple of a byte
|
||||
clear(path[depth+1:])
|
||||
|
||||
// make ip addr from octets
|
||||
var ip netip.Addr
|
||||
if is4 {
|
||||
ip = netip.AddrFrom4([4]byte(path[:4]))
|
||||
} else {
|
||||
ip = netip.AddrFrom16(path)
|
||||
}
|
||||
|
||||
// it's a fringe, bits are always aligned on stride boundaries (/8, /16, /24, ...)
|
||||
bits := (depth + 1) << 3
|
||||
|
||||
// PrefixFrom does not allocate and does not mask off the host bits of ip.
|
||||
// With the clear(), the non-canonical bytes have already been removed.
|
||||
return netip.PrefixFrom(ip, bits)
|
||||
}
|
||||
|
||||
// DivMod8 returns the count of full 8‑bit strides (bits/8)
|
||||
// and the remaining bits in the final stride (bits%8) for pfxLen.
|
||||
//
|
||||
// ATTENTION: Split the IP prefixes at 8-bit borders, count from 0.
|
||||
//
|
||||
// /7, /15, /23, /31, ..., /127
|
||||
//
|
||||
// BitPos: [0-7],[8-15],[16-23],[24-31],[32]
|
||||
// BitPos: [0-7],[8-15],[16-23],[24-31],[32-39],[40-47],[48-55],[56-63],...,[120-127],[128]
|
||||
//
|
||||
// 0.0.0.0/0 => strideCount: 0, modBits: 0 (default route)
|
||||
// 0.0.0.0/7 => strideCount: 0, modBits: 7
|
||||
// 0.0.0.0/8 => strideCount: 1, modBits: 0 (fringe candidate)
|
||||
// 10.0.0.0/8 => strideCount: 1, modBits: 0 (fringe candidate)
|
||||
// 10.0.0.0/22 => strideCount: 2, modBits: 6
|
||||
// 10.0.0.0/29 => strideCount: 3, modBits: 5
|
||||
// 10.0.0.0/32 => strideCount: 4, modBits: 0 (fringe candidate)
|
||||
//
|
||||
// ::/0 => strideCount: 0, modBits: 0 (default route)
|
||||
// ::1/128 => strideCount: 16, modBits: 0 (fringe candidate)
|
||||
// 2001:db8::/42 => strideCount: 5, modBits: 2
|
||||
// 2001:db8::/56 => strideCount: 7, modBits: 0 (fringe candidate)
|
||||
//
|
||||
// /32 and /128 prefixes are special, they never form a new node,
|
||||
// At the end of the trie (IPv4: depth 4, IPv6: depth 16) they are always
|
||||
// inserted as a path‑compressed fringe.
|
||||
//
|
||||
// We are not splitting at /8, /16, ..., because this would mean that the
|
||||
// first node would have 512 prefixes, 9 bits from [0-8]. All remaining nodes
|
||||
// would then only have 8 bits from [9-16], [17-24], [25..32], ...
|
||||
// but the algorithm would then require a variable length bitset
|
||||
// or imply a double-sized bitset.
|
||||
//
|
||||
// If you can commit to a fixed size of [4]uint64, then the algorithm is
|
||||
// much faster due to modern CPUs.
|
||||
//
|
||||
// Perhaps a future Go version that supports SIMD instructions for the [4]uint64 vectors
|
||||
// will make the algorithm even faster on suitable hardware.
|
||||
func DivMod8(pfxLen int) (strideCount int, modBits uint8) {
|
||||
// strideCount: range from 0..4 or 0..16
|
||||
// modBits: range from 0..7
|
||||
return pfxLen >> 3, uint8(pfxLen & 7)
|
||||
}
|
||||
|
||||
// CloneLeaf creates and returns a copy of the leafNode receiver.
|
||||
// If cloneFn is nil, the value is copied directly without modification.
|
||||
// Otherwise, cloneFn is applied to the value for deep cloning.
|
||||
// The prefix field is always copied as is.
|
||||
func (l *LeafNode[V]) CloneLeaf(cloneFn func(V) V) *LeafNode[V] {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cloneFn == nil {
|
||||
return &LeafNode[V]{Prefix: l.Prefix, Value: l.Value}
|
||||
}
|
||||
return &LeafNode[V]{Prefix: l.Prefix, Value: cloneFn(l.Value)}
|
||||
}
|
||||
|
||||
// CloneFringe creates and returns a copy of the FringeNode receiver.
|
||||
// If cloneFn is nil, the value is copied directly without modification.
|
||||
// Otherwise, cloneFn is applied to the value for deep cloning.
|
||||
func (l *FringeNode[V]) CloneFringe(cloneFn func(V) V) *FringeNode[V] {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cloneFn == nil {
|
||||
return &FringeNode[V]{Value: l.Value}
|
||||
}
|
||||
return &FringeNode[V]{Value: cloneFn(l.Value)}
|
||||
}
|
||||
Reference in new issue
Block a user