use indexmap::IndexMap;
use crate::cables::is_link_network;
use crate::document::NetworkDoc;
use crate::draft::Draft;
use crate::{Error, Lan, Result, Topology};
fn usable(prefix: u8) -> std::ops::RangeInclusive<u8> {
let host_bits = 32 - prefix.clamp(24, 30);
let broadcast = u8::MAX >> (8 - host_bits);
let first = if host_bits == 8 { 10 } else { 2 };
first..=broadcast - 3
}
fn prefix_of(cidr: &str) -> Option<u8> {
let (addr, len) = cidr.split_once('/')?;
let octets = addr.split('.').filter_map(|o| o.parse::<u8>().ok()).count() == 4;
let len = len.parse::<u8>().ok()?;
(octets && len <= 32).then_some(len)
}
pub(crate) fn address(topo: &Topology, draft: &mut Draft) -> Result<()> {
for (name, lan) in &topo.networks {
address_one(topo, draft, name, lan)?;
}
check_known(topo)
}
fn address_one(topo: &Topology, draft: &mut Draft, name: &str, lan: &Lan) -> Result<()> {
if is_link_network(name) {
return Err(Error::ReservedLanName(name.into()));
}
let prefix = prefix_of(&lan.cidr).ok_or_else(|| Error::BadCidr {
lan: name.into(),
cidr: lan.cidr.clone(),
})?;
draft
.networks
.insert(name.into(), NetworkDoc::new(lan.cidr.clone(), lan.internet, lan.tc.clone()));
let taken = asked_addresses(topo, draft, name)?;
let mut free = usable(prefix).filter(|o| !taken.contains_key(o));
for (node, _) in members(topo, draft, name).into_iter().filter(|(_, octet)| octet.is_none()) {
let octet = free.next().ok_or_else(|| Error::LanFull {
lan: name.into(),
cidr: lan.cidr.clone(),
})?;
draft.attach(node, name, octet);
}
Ok(())
}
fn members<'t>(topo: &'t Topology, draft: &Draft, lan: &str) -> Vec<(&'t str, Option<u8>)> {
topo.nodes
.iter()
.filter(|(node, _)| !draft.is_router(node))
.filter_map(|(node, n)| n.networks.get(lan).map(|octet| (node.as_str(), *octet)))
.collect()
}
fn asked_addresses(topo: &Topology, draft: &mut Draft, lan: &str) -> Result<IndexMap<u8, String>> {
let asked: Vec<(&str, u8)> = members(topo, draft, lan).into_iter().filter_map(|(node, octet)| Some((node, octet?))).collect();
let mut taken: IndexMap<u8, String> = IndexMap::new();
for (node, octet) in asked {
if let Some(first) = taken.insert(octet, node.to_owned()) {
return Err(Error::AddressTaken {
lan: lan.into(),
octet,
first,
second: node.to_owned(),
});
}
draft.attach(node, lan, octet);
}
Ok(taken)
}
fn check_known(topo: &Topology) -> Result<()> {
for (node, n) in &topo.nodes {
if let Some(lan) = n.networks.keys().find(|l| !topo.networks.contains_key(*l)) {
return Err(Error::UnknownLan {
node: node.clone(),
lan: lan.clone(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_block_offers_what_isoloom_leaves_to_machines() {
assert_eq!(usable(24), 10..=252);
assert_eq!(usable(29), 2..=4);
assert_eq!(usable(16), 10..=252, "larger blocks are read as a /24 of addresses");
assert_eq!(usable(25), 2..=124);
assert_eq!(usable(28), 2..=12);
assert!(usable(30).is_empty(), "a /30 leaves no address to a machine");
assert_eq!(usable(32), usable(30));
}
#[test]
fn a_cidr_has_four_octets_and_a_prefix() {
assert_eq!(prefix_of("10.1.2.0/24"), Some(24));
assert_eq!(prefix_of("10.1.2.0/32"), Some(32));
for bad in ["10.1.2/24", "office", "10.1.2.0/33", "10.1.2.0", "10.1.2.256/24", "10.1.2.0/x"] {
assert_eq!(prefix_of(bad), None, "{bad}");
}
}
}