use std::fmt;
use std::net::Ipv6Addr;
use std::str::FromStr;
use crate::Error;
pub const PREFIX: u8 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cidr6 {
pub base: u128,
}
impl Cidr6 {
#[must_use]
pub fn parse(s: &str) -> Option<Cidr6> {
let (ip, len) = s.split_once('/')?;
let ip: Ipv6Addr = ip.parse().ok()?;
if len.parse::<u8>().ok()? != PREFIX {
return None;
}
let base = u128::from(ip);
let host_bits = (1u128 << (128 - PREFIX)) - 1;
let unique_local = base >> 121 == 0x7e;
let global = base >> 125 == 1;
(base & host_bits == 0 && (unique_local || global)).then_some(Cidr6 { base })
}
#[must_use]
pub fn overlaps(self, other: Cidr6) -> bool {
self.base == other.base
}
#[must_use]
pub fn host(self, octet: u8) -> Ipv6Addr {
Ipv6Addr::from(self.base | interface_id(octet))
}
#[must_use]
pub fn gateway(self) -> Ipv6Addr {
Ipv6Addr::from(self.base | 1)
}
}
impl fmt::Display for Cidr6 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{PREFIX}", Ipv6Addr::from(self.base))
}
}
impl FromStr for Cidr6 {
type Err = Error;
fn from_str(s: &str) -> Result<Cidr6, Error> {
Cidr6::parse(s).ok_or_else(|| Error::Cidr6(s.to_owned()))
}
}
fn interface_id(octet: u8) -> u128 {
u128::from(octet / 100) << 8 | u128::from(octet / 10 % 10) << 4 | u128::from(octet % 10)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn a_machine_reads_like_its_ipv4_address() {
let c = Cidr6::parse("fd00:20::/64").unwrap();
assert_eq!(c.host(10).to_string(), "fd00:20::10");
assert_eq!(c.host(254).to_string(), "fd00:20::254");
assert_eq!(c.gateway().to_string(), "fd00:20::1");
assert_eq!(c.to_string(), "fd00:20::/64");
}
#[test]
fn a_block_reads_from_its_text_or_says_why_not() {
assert_eq!("fd00:20::/64".parse::<Cidr6>().map(|c| c.to_string()).ok().as_deref(), Some("fd00:20::/64"));
let short = "fd00:20::/48".parse::<Cidr6>().unwrap_err();
assert_eq!(
short.to_string(),
"`fd00:20::/48` isn't an IPv6 block: write a unique local or global network address and `/64` (`fd00:20::/64`)"
);
}
#[test]
fn two_64s_overlap_only_when_they_are_the_same_block() {
let a = Cidr6::parse("fd00:20::/64").unwrap();
let b = Cidr6::parse("fd00:21::/64").unwrap();
assert!(a.overlaps(a) && !a.overlaps(b) && !b.overlaps(a));
}
#[test]
fn only_unicast_network_addresses_of_a_64() {
assert!(Cidr6::parse("2001:db8:1:2::/64").is_some());
for bad in [
"fd00::1/64",
"fd00::/48",
"fe80::/64",
"ff02::/64",
"::/64",
"10.0.0.0/24",
"fd00::",
"fd00::/x",
] {
assert_eq!(Cidr6::parse(bad), None, "{bad}");
}
}
proptest! {
#[test]
fn hosts_are_distinct_and_inside(prefix in any::<u64>(), a in 2u8.., b in 2u8..) {
let c = Cidr6 { base: u128::from(prefix) << 64 };
let (ha, hb) = (u128::from(c.host(a)), u128::from(c.host(b)));
prop_assert_eq!(ha == hb, a == b);
prop_assert_eq!(ha >> 64, u128::from(prefix));
prop_assert_ne!(c.host(a), c.gateway());
}
#[test]
fn parse_round_trips(groups in proptest::array::uniform4(any::<u16>())) {
let [a, b, c, d] = groups;
let ip = Ipv6Addr::new(0xfd00 | (a & 0xff), b, c, d, 0, 0, 0, 0);
let text = format!("{ip}/64");
let parsed = Cidr6::parse(&text).unwrap();
prop_assert_eq!(parsed.to_string(), text);
}
#[test]
fn host_bits_are_refused(prefix in any::<u16>(), host in 1u64..) {
let ip = Ipv6Addr::from(u128::from(0xfd00u16 | (prefix & 0xff)) << 112 | u128::from(host));
prop_assert_eq!(Cidr6::parse(&format!("{ip}/64")), None);
}
}
}