#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
use std::fmt;
use std::net::Ipv6Addr;
use std::num::ParseIntError;
use std::str::FromStr;
use rand::Rng;
use rand::rngs::ThreadRng;
pub const ULA_PREFIX_LEN: u8 = 48;
pub const ULA_FIRST_OCTET: u8 = 0xfd;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ipv6Prefix {
addr: Ipv6Addr,
prefix_len: u8,
}
impl Ipv6Prefix {
#[must_use]
pub const fn new(addr: Ipv6Addr, prefix_len: u8) -> Option<Self> {
if prefix_len > 128 {
None
} else {
Some(Self { addr, prefix_len })
}
}
#[must_use]
pub const fn addr(&self) -> Ipv6Addr {
self.addr
}
#[must_use]
pub const fn prefix_len(&self) -> u8 {
self.prefix_len
}
}
impl fmt::Display for Ipv6Prefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.addr, self.prefix_len)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParsePrefixError {
MissingSlash,
InvalidAddress(std::net::AddrParseError),
InvalidLength(ParseIntError),
LengthOutOfRange(u8),
}
impl fmt::Display for ParsePrefixError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingSlash => f.write_str("missing '/' separator"),
Self::InvalidAddress(e) => write!(f, "invalid IPv6 address: {e}"),
Self::InvalidLength(e) => write!(f, "invalid prefix length: {e}"),
Self::LengthOutOfRange(n) => write!(f, "prefix length {n} exceeds 128"),
}
}
}
impl std::error::Error for ParsePrefixError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidAddress(e) => Some(e),
Self::InvalidLength(e) => Some(e),
_ => None,
}
}
}
impl FromStr for Ipv6Prefix {
type Err = ParsePrefixError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (addr_part, len_part) = s.split_once('/').ok_or(ParsePrefixError::MissingSlash)?;
let addr: Ipv6Addr = addr_part
.parse()
.map_err(ParsePrefixError::InvalidAddress)?;
let prefix_len: u8 = len_part.parse().map_err(ParsePrefixError::InvalidLength)?;
Self::new(addr, prefix_len).ok_or(ParsePrefixError::LengthOutOfRange(prefix_len))
}
}
impl From<Ipv6Prefix> for (Ipv6Addr, u8) {
fn from(p: Ipv6Prefix) -> Self {
(p.addr, p.prefix_len)
}
}
pub fn generate_ula_prefix_with<R: Rng + ?Sized>(rng: &mut R) -> Ipv6Prefix {
let mut octets = [0u8; 16];
rng.fill_bytes(&mut octets[..6]);
octets[0] = ULA_FIRST_OCTET;
for o in &mut octets[6..] {
*o = 0;
}
Ipv6Prefix {
addr: Ipv6Addr::from(octets),
prefix_len: ULA_PREFIX_LEN,
}
}
#[must_use]
pub fn generate_ula_prefix() -> Ipv6Prefix {
let mut rng: ThreadRng = rand::rng();
generate_ula_prefix_with(&mut rng)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn prefix_is_in_fd00_8() {
for _ in 0..1000 {
let p = generate_ula_prefix();
assert_eq!(p.addr().octets()[0], 0xfd);
assert_eq!(p.prefix_len(), 48);
}
}
#[test]
fn host_bits_below_48_are_zero() {
for _ in 0..256 {
let p = generate_ula_prefix();
let octets = p.addr().octets();
for (i, b) in octets.iter().enumerate().skip(6) {
assert_eq!(*b, 0, "octet {i} should be zero, got {b:#x}");
}
}
}
#[test]
fn deterministic_with_seeded_rng() {
let mut rng = StdRng::seed_from_u64(0xDEAD_BEEF);
let a = generate_ula_prefix_with(&mut rng);
let mut rng = StdRng::seed_from_u64(0xDEAD_BEEF);
let b = generate_ula_prefix_with(&mut rng);
assert_eq!(a, b);
}
#[test]
fn display_round_trips() {
let p = "fd12:3456:789a::/48".parse::<Ipv6Prefix>().unwrap();
assert_eq!(p.to_string(), "fd12:3456:789a::/48");
assert_eq!(p.prefix_len(), 48);
}
#[test]
fn parse_errors() {
assert!(matches!(
"fd12::".parse::<Ipv6Prefix>(),
Err(ParsePrefixError::MissingSlash)
));
assert!(matches!(
"not-an-addr/48".parse::<Ipv6Prefix>(),
Err(ParsePrefixError::InvalidAddress(_))
));
assert!(matches!(
"fd12::/notanum".parse::<Ipv6Prefix>(),
Err(ParsePrefixError::InvalidLength(_))
));
assert!(matches!(
"fd12::/200".parse::<Ipv6Prefix>(),
Err(ParsePrefixError::LengthOutOfRange(200))
));
}
#[test]
fn new_validates_length() {
assert!(Ipv6Prefix::new(Ipv6Addr::UNSPECIFIED, 129).is_none());
assert!(Ipv6Prefix::new(Ipv6Addr::UNSPECIFIED, 128).is_some());
assert!(Ipv6Prefix::new(Ipv6Addr::UNSPECIFIED, 0).is_some());
}
}