This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+337
View File
@@ -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)}
}