isoloom_spec/validate/
cidr6.rs1use std::fmt;
5use std::net::Ipv6Addr;
6use std::str::FromStr;
7
8use crate::Error;
9
10pub const PREFIX: u8 = 64;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Cidr6 {
17 pub base: u128,
19}
20
21impl Cidr6 {
22 #[must_use]
25 pub fn parse(s: &str) -> Option<Cidr6> {
26 let (ip, len) = s.split_once('/')?;
27 let ip: Ipv6Addr = ip.parse().ok()?;
28 if len.parse::<u8>().ok()? != PREFIX {
29 return None;
30 }
31 let base = u128::from(ip);
32 let host_bits = (1u128 << (128 - PREFIX)) - 1;
33 let unique_local = base >> 121 == 0x7e;
34 let global = base >> 125 == 1;
35 (base & host_bits == 0 && (unique_local || global)).then_some(Cidr6 { base })
36 }
37
38 #[must_use]
40 pub fn overlaps(self, other: Cidr6) -> bool {
41 self.base == other.base
42 }
43
44 #[must_use]
47 pub fn host(self, octet: u8) -> Ipv6Addr {
48 Ipv6Addr::from(self.base | interface_id(octet))
49 }
50
51 #[must_use]
53 pub fn gateway(self) -> Ipv6Addr {
54 Ipv6Addr::from(self.base | 1)
55 }
56}
57
58impl fmt::Display for Cidr6 {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "{}/{PREFIX}", Ipv6Addr::from(self.base))
61 }
62}
63
64impl FromStr for Cidr6 {
65 type Err = Error;
66
67 fn from_str(s: &str) -> Result<Cidr6, Error> {
68 Cidr6::parse(s).ok_or_else(|| Error::Cidr6(s.to_owned()))
69 }
70}
71
72fn interface_id(octet: u8) -> u128 {
74 u128::from(octet / 100) << 8 | u128::from(octet / 10 % 10) << 4 | u128::from(octet % 10)
75}
76
77#[cfg(test)]
78mod tests {
79 use proptest::prelude::*;
80
81 use super::*;
82
83 #[test]
84 fn a_machine_reads_like_its_ipv4_address() {
85 let c = Cidr6::parse("fd00:20::/64").unwrap();
86 assert_eq!(c.host(10).to_string(), "fd00:20::10");
87 assert_eq!(c.host(254).to_string(), "fd00:20::254");
88 assert_eq!(c.gateway().to_string(), "fd00:20::1");
89 assert_eq!(c.to_string(), "fd00:20::/64");
90 }
91
92 #[test]
93 fn a_block_reads_from_its_text_or_says_why_not() {
94 assert_eq!("fd00:20::/64".parse::<Cidr6>().map(|c| c.to_string()).ok().as_deref(), Some("fd00:20::/64"));
95 let short = "fd00:20::/48".parse::<Cidr6>().unwrap_err();
96 assert_eq!(
97 short.to_string(),
98 "`fd00:20::/48` isn't an IPv6 block: write a unique local or global network address and `/64` (`fd00:20::/64`)"
99 );
100 }
101
102 #[test]
103 fn two_64s_overlap_only_when_they_are_the_same_block() {
104 let a = Cidr6::parse("fd00:20::/64").unwrap();
105 let b = Cidr6::parse("fd00:21::/64").unwrap();
106 assert!(a.overlaps(a) && !a.overlaps(b) && !b.overlaps(a));
107 }
108
109 #[test]
110 fn only_unicast_network_addresses_of_a_64() {
111 assert!(Cidr6::parse("2001:db8:1:2::/64").is_some());
112 for bad in [
113 "fd00::1/64",
114 "fd00::/48",
115 "fe80::/64",
116 "ff02::/64",
117 "::/64",
118 "10.0.0.0/24",
119 "fd00::",
120 "fd00::/x",
121 ] {
122 assert_eq!(Cidr6::parse(bad), None, "{bad}");
123 }
124 }
125
126 proptest! {
127 #[test]
129 fn hosts_are_distinct_and_inside(prefix in any::<u64>(), a in 2u8.., b in 2u8..) {
130 let c = Cidr6 { base: u128::from(prefix) << 64 };
131 let (ha, hb) = (u128::from(c.host(a)), u128::from(c.host(b)));
132 prop_assert_eq!(ha == hb, a == b);
133 prop_assert_eq!(ha >> 64, u128::from(prefix));
134 prop_assert_ne!(c.host(a), c.gateway());
135 }
136
137 #[test]
139 fn parse_round_trips(groups in proptest::array::uniform4(any::<u16>())) {
140 let [a, b, c, d] = groups;
141 let ip = Ipv6Addr::new(0xfd00 | (a & 0xff), b, c, d, 0, 0, 0, 0);
142 let text = format!("{ip}/64");
143 let parsed = Cidr6::parse(&text).unwrap();
144 prop_assert_eq!(parsed.to_string(), text);
145 }
146
147 #[test]
149 fn host_bits_are_refused(prefix in any::<u16>(), host in 1u64..) {
150 let ip = Ipv6Addr::from(u128::from(0xfd00u16 | (prefix & 0xff)) << 112 | u128::from(host));
151 prop_assert_eq!(Cidr6::parse(&format!("{ip}/64")), None);
152 }
153 }
154}