Files
tsnet-proxy/vendor/github.com/gaissmai/bart/internal/nodes/nodebasics.go
T
2026-10-22 12:27:19 +01:00

338 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)}
}