338 lines
10 KiB
Go
338 lines
10 KiB
Go
// 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)}
|
||
}
|