use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone)]
pub enum UnicastLinkLocalIpAddrParseError {
InvalidAddr(AddrParseError),
NotUnicastLinkLocal(UnicastLinkLocalIpAddrError),
}
impl std::fmt::Display for UnicastLinkLocalIpAddrParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidAddr(error) => error.fmt(f),
Self::NotUnicastLinkLocal(error) => error.fmt(f),
}
}
}
impl std::error::Error for UnicastLinkLocalIpAddrParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidAddr(error) => std::error::Error::source(error),
Self::NotUnicastLinkLocal(error) => std::error::Error::source(error),
}
}
}
#[derive(Copy, Debug, Clone, PartialEq)]
pub struct UnicastLinkLocalIpAddrError(
pub IpAddr,
);
impl std::fmt::Display for UnicastLinkLocalIpAddrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "input is not unicast link-local: {}", self.0)
}
}
impl std::error::Error for UnicastLinkLocalIpAddrError {}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum UnicastLinkLocalIpAddr {
V4(UnicastLinkLocalIpv4Addr),
V6(UnicastLinkLocalIpv6Addr),
}
impl UnicastLinkLocalIpAddr {
pub fn new(addr: IpAddr) -> Result<Self, UnicastLinkLocalIpAddrError> {
match addr {
IpAddr::V4(ip4) => UnicastLinkLocalIpv4Addr::new(ip4).map(Self::V4),
IpAddr::V6(ip6) => UnicastLinkLocalIpv6Addr::new(ip6).map(Self::V6),
}
}
pub fn is_ipv4(&self) -> bool {
matches!(self, Self::V4(_))
}
pub fn is_ipv6(&self) -> bool {
matches!(self, Self::V6(_))
}
}
impl From<UnicastLinkLocalIpv4Addr> for UnicastLinkLocalIpAddr {
fn from(value: UnicastLinkLocalIpv4Addr) -> Self {
Self::V4(value)
}
}
impl From<UnicastLinkLocalIpv6Addr> for UnicastLinkLocalIpAddr {
fn from(value: UnicastLinkLocalIpv6Addr) -> Self {
Self::V6(value)
}
}
impl TryFrom<IpAddr> for UnicastLinkLocalIpAddr {
type Error = UnicastLinkLocalIpAddrError;
fn try_from(value: IpAddr) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<Ipv4Addr> for UnicastLinkLocalIpAddr {
type Error = UnicastLinkLocalIpAddrError;
fn try_from(value: Ipv4Addr) -> Result<Self, Self::Error> {
Self::new(value.into())
}
}
impl TryFrom<Ipv6Addr> for UnicastLinkLocalIpAddr {
type Error = UnicastLinkLocalIpAddrError;
fn try_from(value: Ipv6Addr) -> Result<Self, Self::Error> {
Self::new(value.into())
}
}
impl From<UnicastLinkLocalIpAddr> for IpAddr {
fn from(value: UnicastLinkLocalIpAddr) -> Self {
match value {
UnicastLinkLocalIpAddr::V4(ip4) => IpAddr::V4(ip4.into_addr()),
UnicastLinkLocalIpAddr::V6(ip6) => IpAddr::V6(ip6.into_addr()),
}
}
}
impl std::fmt::Display for UnicastLinkLocalIpAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnicastLinkLocalIpAddr::V4(inner) => write!(f, "{inner}"),
UnicastLinkLocalIpAddr::V6(inner) => write!(f, "{inner}"),
}
}
}
impl std::str::FromStr for UnicastLinkLocalIpAddr {
type Err = UnicastLinkLocalIpAddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: IpAddr = s
.parse()
.map_err(UnicastLinkLocalIpAddrParseError::InvalidAddr)?;
Self::try_from(addr).map_err(UnicastLinkLocalIpAddrParseError::NotUnicastLinkLocal)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UnicastLinkLocalIpAddr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let addr = <IpAddr as serde::Deserialize>::deserialize(deserializer)?;
Self::new(addr).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UnicastLinkLocalIpAddr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde::Serialize::serialize(&IpAddr::from(*self), serializer)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for UnicastLinkLocalIpAddr {
fn schema_name() -> String {
"UnicastLinkLocalIpAddr".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use crate::schema_util::label_schema;
schemars::schema::SchemaObject {
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
one_of: Some(vec![
label_schema("v4", gen.subschema_for::<UnicastLinkLocalIpv4Addr>()),
label_schema("v6", gen.subschema_for::<UnicastLinkLocalIpv6Addr>()),
]),
..Default::default()
})),
extensions: crate::schema_util::extension("UnicastLinkLocalIpAddr", "0.1.7"),
..Default::default()
}
.into()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct UnicastLinkLocalIpv4Addr(Ipv4Addr);
impl UnicastLinkLocalIpv4Addr {
pub fn new(addr: Ipv4Addr) -> Result<Self, UnicastLinkLocalIpAddrError> {
if addr.is_link_local() {
return Ok(Self(addr));
}
Err(UnicastLinkLocalIpAddrError(addr.into()))
}
pub fn into_addr(self) -> Ipv4Addr {
self.0
}
}
impl TryFrom<Ipv4Addr> for UnicastLinkLocalIpv4Addr {
type Error = UnicastLinkLocalIpAddrError;
fn try_from(value: Ipv4Addr) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<UnicastLinkLocalIpv4Addr> for Ipv4Addr {
fn from(value: UnicastLinkLocalIpv4Addr) -> Self {
value.into_addr()
}
}
impl From<UnicastLinkLocalIpv4Addr> for IpAddr {
fn from(value: UnicastLinkLocalIpv4Addr) -> Self {
IpAddr::V4(value.into_addr())
}
}
impl std::ops::Deref for UnicastLinkLocalIpv4Addr {
type Target = Ipv4Addr;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for UnicastLinkLocalIpv4Addr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for UnicastLinkLocalIpv4Addr {
type Err = UnicastLinkLocalIpAddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: Ipv4Addr = s
.parse()
.map_err(UnicastLinkLocalIpAddrParseError::InvalidAddr)?;
Self::new(addr).map_err(UnicastLinkLocalIpAddrParseError::NotUnicastLinkLocal)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UnicastLinkLocalIpv4Addr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let addr = <Ipv4Addr as serde::Deserialize>::deserialize(deserializer)?;
Self::new(addr).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UnicastLinkLocalIpv4Addr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde::Serialize::serialize(&self.0, serializer)
}
}
#[cfg(feature = "schemars")]
const UNICAST_LINK_LOCAL_IPV4_ADDR_REGEX: &str = concat!(
r"^169\.254\.",
r"([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.",
r"([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$",
);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for UnicastLinkLocalIpv4Addr {
fn schema_name() -> String {
"UnicastLinkLocalIpv4Addr".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = gen.subschema_for::<Ipv4Addr>();
let mut schema_object = schema.into_object();
schema_object.metadata = Some(Box::new(schemars::schema::Metadata {
title: Some("A unicast link-local IPv4 address".to_string()),
description: Some("An IPv4 address in 169.254.0.0/16".to_string()),
examples: vec!["169.254.1.1".into()],
..Default::default()
}));
schema_object.string = Some(Box::new(schemars::schema::StringValidation {
pattern: Some(UNICAST_LINK_LOCAL_IPV4_ADDR_REGEX.to_string()),
..Default::default()
}));
schema_object.extensions =
crate::schema_util::extension("UnicastLinkLocalIpv4Addr", "0.1.7");
schema_object.into()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct UnicastLinkLocalIpv6Addr(Ipv6Addr);
impl UnicastLinkLocalIpv6Addr {
pub fn new(addr: Ipv6Addr) -> Result<Self, UnicastLinkLocalIpAddrError> {
if addr.is_unicast_link_local() {
return Ok(Self(addr));
}
Err(UnicastLinkLocalIpAddrError(addr.into()))
}
pub fn into_addr(self) -> Ipv6Addr {
self.0
}
}
impl TryFrom<Ipv6Addr> for UnicastLinkLocalIpv6Addr {
type Error = UnicastLinkLocalIpAddrError;
fn try_from(value: Ipv6Addr) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<UnicastLinkLocalIpv6Addr> for Ipv6Addr {
fn from(value: UnicastLinkLocalIpv6Addr) -> Self {
value.into_addr()
}
}
impl From<UnicastLinkLocalIpv6Addr> for IpAddr {
fn from(value: UnicastLinkLocalIpv6Addr) -> Self {
IpAddr::V6(value.into_addr())
}
}
impl std::ops::Deref for UnicastLinkLocalIpv6Addr {
type Target = Ipv6Addr;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for UnicastLinkLocalIpv6Addr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for UnicastLinkLocalIpv6Addr {
type Err = UnicastLinkLocalIpAddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: Ipv6Addr = s
.parse()
.map_err(UnicastLinkLocalIpAddrParseError::InvalidAddr)?;
Self::new(addr).map_err(UnicastLinkLocalIpAddrParseError::NotUnicastLinkLocal)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UnicastLinkLocalIpv6Addr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let addr = <Ipv6Addr as serde::Deserialize>::deserialize(deserializer)?;
Self::new(addr).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UnicastLinkLocalIpv6Addr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde::Serialize::serialize(&self.0, serializer)
}
}
#[cfg(feature = "schemars")]
const UNICAST_LINK_LOCAL_IPV6_ADDR_REGEX: &str = r"^[fF][eE][89aAbB][0-9a-fA-F]:";
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for UnicastLinkLocalIpv6Addr {
fn schema_name() -> String {
"UnicastLinkLocalIpv6Addr".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = gen.subschema_for::<Ipv6Addr>();
let mut schema_object = schema.into_object();
schema_object.metadata = Some(Box::new(schemars::schema::Metadata {
title: Some("A unicast link-local IPv6 address".to_string()),
description: Some("An IPv6 address in fe80::/10".to_string()),
examples: vec!["fe80::1".into()],
..Default::default()
}));
schema_object.string = Some(Box::new(schemars::schema::StringValidation {
pattern: Some(UNICAST_LINK_LOCAL_IPV6_ADDR_REGEX.to_string()),
..Default::default()
}));
schema_object.extensions =
crate::schema_util::extension("UnicastLinkLocalIpv6Addr", "0.1.7");
schema_object.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_parses_ipv4_link_local_address() {
let addr: UnicastLinkLocalIpv4Addr = "169.254.1.2".parse().unwrap();
assert_eq!(addr.into_addr(), Ipv4Addr::new(169, 254, 1, 2));
}
#[test]
fn from_str_parses_ipv6_link_local_address() {
let addr: UnicastLinkLocalIpv6Addr = "fe80::1".parse().unwrap();
assert_eq!(addr.into_addr(), "fe80::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn from_str_parses_either_address_family() {
let ipv4: UnicastLinkLocalIpAddr = "169.254.1.2".parse().unwrap();
let ipv6: UnicastLinkLocalIpAddr = "febf::1".parse().unwrap();
assert!(ipv4.is_ipv4());
assert!(ipv6.is_ipv6());
}
#[test]
fn from_str_rejects_malformed_address() {
let error = "not-an-address"
.parse::<UnicastLinkLocalIpAddr>()
.unwrap_err();
assert!(matches!(
error,
UnicastLinkLocalIpAddrParseError::InvalidAddr(_)
));
}
#[test]
fn from_str_rejects_non_link_local_addresses() {
for addr in ["192.0.2.1", "2001:db8::1"] {
let error = addr.parse::<UnicastLinkLocalIpAddr>().unwrap_err();
assert!(matches!(
error,
UnicastLinkLocalIpAddrParseError::NotUnicastLinkLocal(_)
));
}
}
#[test]
fn parse_errors_transparently_report_the_inner_error() {
for (input, expected) in [
("not-an-address", "invalid IP address syntax"),
("192.0.2.1", "input is not unicast link-local: 192.0.2.1"),
] {
let error = input.parse::<UnicastLinkLocalIpAddr>().unwrap_err();
assert_eq!(error.to_string(), expected);
assert!(std::error::Error::source(&error).is_none());
}
}
#[test]
fn ipv4_constructors_accept_entire_link_local_range() {
for octets in [[169, 254, 0, 0], [169, 254, 255, 255]] {
let expected = Ipv4Addr::from(octets);
let validated = UnicastLinkLocalIpv4Addr::new(expected).unwrap();
let generic = UnicastLinkLocalIpAddr::from(validated);
assert_eq!(validated.into_addr(), expected);
assert_eq!(validated.octets(), octets);
assert_eq!(validated.to_bits(), expected.to_bits());
assert_eq!(
UnicastLinkLocalIpAddr::new(expected.into()).unwrap(),
generic
);
assert_eq!(UnicastLinkLocalIpv4Addr::try_from(expected), Ok(validated));
assert_eq!(UnicastLinkLocalIpAddr::try_from(expected), Ok(generic));
assert_eq!(
UnicastLinkLocalIpAddr::try_from(IpAddr::V4(expected)),
Ok(generic)
);
assert_eq!(Ipv4Addr::from(validated), expected);
assert_eq!(IpAddr::from(validated), IpAddr::V4(expected));
assert_eq!(IpAddr::from(generic), IpAddr::V4(expected));
}
}
#[test]
fn ipv4_constructors_reject_addresses_outside_link_local_range() {
for octets in [[169, 253, 255, 255], [169, 255, 0, 0]] {
let addr = Ipv4Addr::from(octets);
let expected = IpAddr::V4(addr);
let errors = [
UnicastLinkLocalIpv4Addr::new(addr).unwrap_err(),
UnicastLinkLocalIpv4Addr::try_from(addr).unwrap_err(),
UnicastLinkLocalIpAddr::new(expected).unwrap_err(),
UnicastLinkLocalIpAddr::try_from(addr).unwrap_err(),
UnicastLinkLocalIpAddr::try_from(expected).unwrap_err(),
];
for error in errors {
assert_eq!(error.0, expected);
}
}
}
#[test]
fn ipv6_constructors_accept_link_local_range_boundaries() {
for segments in [
[0xfe80, 0, 0, 0, 0, 0, 0, 0],
[
0xfebf, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
],
] {
let expected = Ipv6Addr::from(segments);
let validated = UnicastLinkLocalIpv6Addr::new(expected).unwrap();
let generic = UnicastLinkLocalIpAddr::from(validated);
assert_eq!(validated.into_addr(), expected);
assert_eq!(validated.octets(), expected.octets());
assert_eq!(validated.segments(), segments);
assert_eq!(validated.to_bits(), expected.to_bits());
assert_eq!(
UnicastLinkLocalIpAddr::new(expected.into()).unwrap(),
generic
);
assert_eq!(UnicastLinkLocalIpv6Addr::try_from(expected), Ok(validated));
assert_eq!(UnicastLinkLocalIpAddr::try_from(expected), Ok(generic));
assert_eq!(
UnicastLinkLocalIpAddr::try_from(IpAddr::V6(expected)),
Ok(generic)
);
assert_eq!(Ipv6Addr::from(validated), expected);
assert_eq!(IpAddr::from(validated), IpAddr::V6(expected));
assert_eq!(IpAddr::from(generic), IpAddr::V6(expected));
}
}
#[test]
fn ipv6_constructors_reject_addresses_outside_link_local_range() {
for segments in [
[
0xfe7f, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
],
[0xfec0, 0, 0, 0, 0, 0, 0, 0],
[0xff02, 0, 0, 0, 0, 0, 0, 1],
] {
let addr = Ipv6Addr::from(segments);
let expected = IpAddr::V6(addr);
let errors = [
UnicastLinkLocalIpv6Addr::new(addr).unwrap_err(),
UnicastLinkLocalIpv6Addr::try_from(addr).unwrap_err(),
UnicastLinkLocalIpAddr::new(expected).unwrap_err(),
UnicastLinkLocalIpAddr::try_from(addr).unwrap_err(),
UnicastLinkLocalIpAddr::try_from(expected).unwrap_err(),
];
for error in errors {
assert_eq!(error.0, expected);
}
}
}
#[cfg(all(feature = "serde", feature = "schemars"))]
#[test]
fn serde_serializes_all_address_types_as_canonical_strings() {
let generic_v4: UnicastLinkLocalIpAddr = "169.254.1.2".parse().unwrap();
let generic_v6: UnicastLinkLocalIpAddr = "FE80:0:0:0:0:0:0:1".parse().unwrap();
let ipv4: UnicastLinkLocalIpv4Addr = "169.254.1.2".parse().unwrap();
let ipv6: UnicastLinkLocalIpv6Addr = "FE80:0:0:0:0:0:0:1".parse().unwrap();
assert_eq!(
serde_json::to_string(&generic_v4).unwrap(),
r#""169.254.1.2""#
);
assert_eq!(serde_json::to_string(&generic_v6).unwrap(), r#""fe80::1""#);
assert_eq!(serde_json::to_string(&ipv4).unwrap(), r#""169.254.1.2""#);
assert_eq!(serde_json::to_string(&ipv6).unwrap(), r#""fe80::1""#);
}
#[cfg(all(feature = "serde", feature = "schemars"))]
#[test]
fn serde_round_trips_all_address_types() {
let generic_v4: UnicastLinkLocalIpAddr = "169.254.1.2".parse().unwrap();
let generic_v6: UnicastLinkLocalIpAddr = "fe80::1".parse().unwrap();
let ipv4: UnicastLinkLocalIpv4Addr = "169.254.1.2".parse().unwrap();
let ipv6: UnicastLinkLocalIpv6Addr = "fe80::1".parse().unwrap();
let generic_v4_json = serde_json::to_string(&generic_v4).unwrap();
let generic_v6_json = serde_json::to_string(&generic_v6).unwrap();
let ipv4_json = serde_json::to_string(&ipv4).unwrap();
let ipv6_json = serde_json::to_string(&ipv6).unwrap();
assert_eq!(
serde_json::from_str::<UnicastLinkLocalIpAddr>(&generic_v4_json).unwrap(),
generic_v4
);
assert_eq!(
serde_json::from_str::<UnicastLinkLocalIpAddr>(&generic_v6_json).unwrap(),
generic_v6
);
assert_eq!(
serde_json::from_str::<UnicastLinkLocalIpv4Addr>(&ipv4_json).unwrap(),
ipv4
);
assert_eq!(
serde_json::from_str::<UnicastLinkLocalIpv6Addr>(&ipv6_json).unwrap(),
ipv6
);
}
#[cfg(all(feature = "serde", feature = "schemars"))]
#[test]
fn serde_accepts_link_local_range_boundaries() {
for addr in [r#""169.254.0.0""#, r#""169.254.255.255""#] {
assert!(serde_json::from_str::<UnicastLinkLocalIpAddr>(addr).is_ok());
assert!(serde_json::from_str::<UnicastLinkLocalIpv4Addr>(addr).is_ok());
}
for addr in [
r#""fe80::""#,
r#""febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff""#,
] {
assert!(serde_json::from_str::<UnicastLinkLocalIpAddr>(addr).is_ok());
assert!(serde_json::from_str::<UnicastLinkLocalIpv6Addr>(addr).is_ok());
}
}
#[cfg(all(feature = "serde", feature = "schemars"))]
#[test]
fn serde_rejects_non_link_local_addresses() {
for addr in [r#""169.253.255.255""#, r#""169.255.0.0""#, r#""192.0.2.1""#] {
assert!(serde_json::from_str::<UnicastLinkLocalIpAddr>(addr).is_err());
assert!(serde_json::from_str::<UnicastLinkLocalIpv4Addr>(addr).is_err());
}
for addr in [
r#""fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff""#,
r#""fec0::""#,
r#""2001:db8::1""#,
] {
assert!(serde_json::from_str::<UnicastLinkLocalIpAddr>(addr).is_err());
assert!(serde_json::from_str::<UnicastLinkLocalIpv6Addr>(addr).is_err());
}
}
#[cfg(feature = "schemars")]
#[test]
fn schema_patterns_match_link_local_boundaries() {
let ipv4 = regress::Regex::new(UNICAST_LINK_LOCAL_IPV4_ADDR_REGEX).unwrap();
let ipv6 = regress::Regex::new(UNICAST_LINK_LOCAL_IPV6_ADDR_REGEX).unwrap();
for addr in ["169.254.0.0", "169.254.255.255"] {
assert!(ipv4.find(addr).is_some(), "expected {addr} to match");
}
for addr in ["169.253.255.255", "169.255.0.0"] {
assert!(ipv4.find(addr).is_none(), "expected {addr} not to match");
}
for addr in ["fe80::", "FE9a::1", "feaf::1", "febf:ffff::1"] {
assert!(ipv6.find(addr).is_some(), "expected {addr} to match");
}
for addr in ["fe7f::", "fec0::", "ff02::1"] {
assert!(ipv6.find(addr).is_none(), "expected {addr} not to match");
}
}
}