use ntk_common::{HCoord, Naddr, Topology};
use ntk_netlink::Ipv4Net;
use std::net::Ipv4Addr;
const KIND_GLOBAL: u32 = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AddressingError {
#[error("level {level} is out of range for a {levels}-level topology")]
LevelOutOfRange { level: usize, levels: usize },
#[error(
"topology needs {needed} address bits (+2 kind bits), only 24 are available in 10.0.0.0/8"
)]
TopologyTooWide { needed: u32 },
#[error("position {pos} at level {level} is out of range: g-node size is {gsize}")]
PositionOutOfRange { level: usize, pos: u32, gsize: u32 },
}
fn level_bits(gsize: u32) -> u32 {
32 - gsize.saturating_sub(1).leading_zeros()
}
fn total_bits(topology: &Topology) -> Result<u32, AddressingError> {
let needed: u32 = topology.gsizes().iter().copied().map(level_bits).sum();
if needed + 2 > 24 {
return Err(AddressingError::TopologyTooWide { needed });
}
Ok(needed)
}
pub fn check_topology_fits(topology: &Topology) -> Result<(), AddressingError> {
total_bits(topology).map(|_| ())
}
fn pack(
topology: &Topology,
kind: u32,
zero_below: usize,
positions: impl Fn(usize) -> u32,
) -> Result<(Ipv4Addr, u8), AddressingError> {
let levels = topology.levels();
total_bits(topology)?;
let mut acc: u32 = kind;
let mut zeroed_bits: u32 = 0;
for level in (0..levels).rev() {
let gsize = topology
.gsize(level)
.expect("level < topology.levels() by loop range");
let bits = level_bits(gsize);
let value = if level < zero_below {
0
} else {
let value = positions(level);
if value >= gsize {
return Err(AddressingError::PositionOutOfRange {
level,
pos: value,
gsize,
});
}
value
};
acc = (acc << bits) | value;
if level < zero_below {
zeroed_bits += bits;
}
}
let address = Ipv4Addr::new(10, (acc >> 16) as u8, (acc >> 8) as u8, acc as u8);
let prefix = u8::try_from(32 - zeroed_bits).expect("zeroed_bits <= 24 < 32");
Ok((address, prefix))
}
pub fn host_address(naddr: &Naddr) -> Result<Ipv4Net, AddressingError> {
let (address, _) = pack(naddr.topology(), KIND_GLOBAL, 0, |level| {
naddr.pos(level).expect("level < topology.levels()")
})?;
Ok(Ipv4Net::host(address))
}
pub fn gnode_destination(my_naddr: &Naddr, hc: HCoord) -> Result<Ipv4Net, AddressingError> {
let levels = my_naddr.topology().levels();
if hc.level >= levels {
return Err(AddressingError::LevelOutOfRange {
level: hc.level,
levels,
});
}
let (address, prefix) = pack(my_naddr.topology(), KIND_GLOBAL, hc.level, |level| {
if level == hc.level {
hc.pos
} else {
my_naddr.pos(level).expect("level < topology.levels()")
}
})?;
Ok(Ipv4Net::new(address, prefix).expect("prefix computed in-range by pack()"))
}
#[cfg(test)]
mod tests {
use super::*;
fn topology() -> Topology {
Topology::new([4, 2, 2, 2]).unwrap() }
#[test]
fn host_address_matches_upstream_bit_packing() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
let net = host_address(&naddr).unwrap();
assert_eq!(net.address(), Ipv4Addr::new(10, 0, 0, 9));
assert_eq!(net.prefix_len(), 32);
let boundary_naddr = Naddr::new(topology(), [3, 0, 1, 0]).unwrap();
let boundary_net = host_address(&boundary_naddr).unwrap();
assert_eq!(boundary_net.address(), Ipv4Addr::new(10, 0, 0, 11));
assert_eq!(boundary_net.prefix_len(), 32);
}
#[test]
fn gnode_destination_zeroes_below_and_shrinks_prefix() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
let net = gnode_destination(&naddr, HCoord::new(1, 1)).unwrap();
assert_eq!(net.prefix_len(), 32 - 2); let inside = Naddr::new(topology(), [3, 1, 1, 0]).unwrap();
assert!(net.contains(host_address(&inside).unwrap().address()));
assert!(!net.contains(host_address(&naddr).unwrap().address()));
}
#[test]
fn gnode_destination_same_level_as_self_is_own_gnode() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
let net = gnode_destination(&naddr, HCoord::new(0, naddr.pos(0).unwrap())).unwrap();
assert_eq!(net.prefix_len(), 32);
assert_eq!(net.address(), host_address(&naddr).unwrap().address());
}
#[test]
fn rejects_out_of_range_level() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
assert!(matches!(
gnode_destination(&naddr, HCoord::new(4, 0)),
Err(AddressingError::LevelOutOfRange {
level: 4,
levels: 4
})
));
}
#[test]
fn rejects_topology_too_wide_for_10_8() {
let wide = Topology::new(std::iter::repeat_n(2u32, 22)).unwrap();
let naddr = Naddr::new(wide, vec![0u32; 22]).unwrap();
assert!(host_address(&naddr).is_ok());
let too_wide = Topology::new(std::iter::repeat_n(2u32, 23)).unwrap();
let naddr = Naddr::new(too_wide, vec![0u32; 23]).unwrap();
assert!(matches!(
host_address(&naddr),
Err(AddressingError::TopologyTooWide { .. })
));
}
#[test]
fn level_bits_matches_power_of_two_g_exp() {
assert_eq!(level_bits(1), 0);
assert_eq!(level_bits(2), 1);
assert_eq!(level_bits(4), 2);
assert_eq!(level_bits(8), 3);
assert_eq!(level_bits(3), 2);
assert_eq!(level_bits(5), 3);
}
#[test]
fn rejects_out_of_range_position_at_every_level() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
for (level, gsize) in [(0u32, 4u32), (1, 2), (2, 2), (3, 2)] {
let level = level as usize;
assert!(
matches!(
gnode_destination(&naddr, HCoord::new(level, gsize)),
Err(AddressingError::PositionOutOfRange {
level: err_level,
pos,
gsize: err_gsize,
}) if err_level == level && pos == gsize && err_gsize == gsize
),
"level {level} (gsize {gsize}) accepted an out-of-range position"
);
}
}
#[test]
fn out_of_range_position_no_longer_corrupts_the_adjacent_level() {
let naddr = Naddr::new(topology(), [1, 0, 1, 0]).unwrap();
assert!(matches!(
gnode_destination(&naddr, HCoord::new(0, 7)),
Err(AddressingError::PositionOutOfRange {
level: 0,
pos: 7,
gsize: 4
})
));
}
}