use std::{
fmt,
hash::{Hash, Hasher},
net::IpAddr,
};
use globset::{Glob, GlobMatcher};
use crate::dquic::qbase::net::Family;
#[derive(Clone)]
pub enum BindHost {
Ip {
addr: IpAddr,
repr: String,
},
Glob {
family: Option<Family>,
matcher: GlobMatcher,
},
Exact {
family: Option<Family>,
nic: String,
},
}
impl BindHost {
pub(crate) fn classify(raw: &str, family: Option<Family>) -> Result<Self, &'static str> {
use std::net::Ipv6Addr;
if let Ok(addr) = raw.parse::<IpAddr>() {
if family.is_some() {
return Err("family prefix is not valid for IP addresses");
}
return Ok(Self::Ip {
addr,
repr: raw.to_owned(),
});
}
if let Some(inner) = raw.strip_prefix('[').and_then(|s| s.strip_suffix(']'))
&& let Ok(addr) = inner.parse::<Ipv6Addr>()
{
if family.is_some() {
return Err("family prefix is not valid for IP addresses");
}
return Ok(Self::Ip {
addr: addr.into(),
repr: inner.to_owned(),
});
}
match Glob::new(raw) {
Ok(glob) => Ok(Self::Glob {
family,
matcher: glob.compile_matcher(),
}),
Err(_) => Ok(Self::Exact {
family,
nic: raw.to_owned(),
}),
}
}
#[must_use]
pub fn is_glob(&self) -> bool {
match self {
Self::Ip { .. } | Self::Exact { .. } => false,
Self::Glob { matcher, .. } => {
let pat = matcher.glob().glob();
pat.contains(['*', '?', '[', '{'])
}
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Ip { repr, .. } | Self::Exact { nic: repr, .. } => repr,
Self::Glob { matcher, .. } => matcher.glob().glob(),
}
}
#[must_use]
pub fn is_ip_addr(&self) -> bool {
matches!(self, Self::Ip { .. })
}
#[must_use]
pub fn as_ip_addr(&self) -> Option<IpAddr> {
match self {
Self::Ip { addr, .. } => Some(*addr),
_ => None,
}
}
#[must_use]
pub fn family(&self) -> Option<Family> {
match self {
Self::Ip { .. } => None,
Self::Glob { family, .. } | Self::Exact { family, .. } => *family,
}
}
#[must_use]
pub fn families(&self) -> &'static [Family] {
const V4_ONLY: [Family; 1] = [Family::V4];
const V6_ONLY: [Family; 1] = [Family::V6];
const BOTH: [Family; 2] = [Family::V4, Family::V6];
match self.family() {
Some(Family::V4) => &V4_ONLY,
Some(Family::V6) => &V6_ONLY,
None => &BOTH,
}
}
#[must_use]
pub fn matches(&self, name: &str) -> bool {
match self {
Self::Ip { .. } => false,
Self::Glob { matcher, .. } => matcher.is_match(name),
Self::Exact { nic, .. } => nic == name,
}
}
}
impl PartialEq for BindHost {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ip { repr: a, .. }, Self::Ip { repr: b, .. }) => a == b,
(
Self::Glob {
family: fa,
matcher: ma,
},
Self::Glob {
family: fb,
matcher: mb,
},
) => fa == fb && ma.glob().glob() == mb.glob().glob(),
(
Self::Exact {
family: fa,
nic: na,
},
Self::Exact {
family: fb,
nic: nb,
},
) => fa == fb && na == nb,
_ => false,
}
}
}
impl Eq for BindHost {}
impl Hash for BindHost {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Ip { repr, .. } => repr.hash(state),
Self::Glob {
family, matcher, ..
} => {
family.hash(state);
matcher.glob().glob().hash(state);
}
Self::Exact { family, nic, .. } => {
family.hash(state);
nic.hash(state);
}
}
}
}
impl fmt::Debug for BindHost {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ip { addr, repr } => f
.debug_struct("Ip")
.field("addr", addr)
.field("repr", repr)
.finish(),
Self::Glob { family, matcher } => f
.debug_struct("Glob")
.field("family", family)
.field("pattern", &matcher.glob().glob())
.finish(),
Self::Exact { family, nic } => f
.debug_struct("Exact")
.field("family", family)
.field("nic", nic)
.finish(),
}
}
}
impl fmt::Display for BindHost {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use super::*;
use crate::dquic::qbase::net::Family;
fn hash(host: &BindHost) -> u64 {
let mut hasher = DefaultHasher::new();
host.hash(&mut hasher);
hasher.finish()
}
#[test]
fn classify_ip_and_glob_variants() {
let ipv4 = BindHost::classify("127.0.0.1", None).unwrap();
assert!(ipv4.is_ip_addr());
assert!(!ipv4.is_glob());
assert_eq!(ipv4.as_str(), "127.0.0.1");
assert_eq!(ipv4.as_ip_addr(), Some("127.0.0.1".parse().unwrap()));
let ipv6 = BindHost::classify("[::1]", None).unwrap();
assert!(ipv6.is_ip_addr());
assert_eq!(ipv6.as_str(), "::1");
assert_eq!(ipv6.families(), [Family::V4, Family::V6]);
let glob = BindHost::classify("en*", None).unwrap();
assert!(!glob.is_ip_addr());
assert!(glob.is_glob());
assert_eq!(glob.as_str(), "en*");
assert_eq!(glob.families(), [Family::V4, Family::V6]);
assert!(BindHost::classify("en?", None).unwrap().is_glob());
assert!(
BindHost::classify("{docker,bridge}0", None)
.unwrap()
.is_glob()
);
}
#[test]
fn classify_rejects_family_for_ip_and_falls_back_to_exact_on_bad_glob() {
let err = BindHost::classify("127.0.0.1", Some(Family::V4))
.expect_err("family prefix should be rejected for plain IP address");
assert_eq!(err, "family prefix is not valid for IP addresses");
let err = BindHost::classify("[::1]", Some(Family::V6))
.expect_err("family prefix should be rejected for bracketed IPv6");
assert_eq!(err, "family prefix is not valid for IP addresses");
let exact = BindHost::classify("[", None).unwrap();
assert!(matches!(exact, BindHost::Exact { family: None, ref nic } if nic == "["));
assert!(!exact.is_glob());
assert_eq!(exact.family(), None);
assert_eq!(exact.families(), [Family::V4, Family::V6]);
}
#[test]
fn host_matching_and_identity_accessors() {
let exact = BindHost::classify("enp17s0", Some(Family::V4)).unwrap();
assert_eq!(exact.family(), Some(Family::V4));
assert_eq!(exact.as_str(), "enp17s0");
assert!(!exact.matches("eth0"));
assert!(exact.matches("enp17s0"));
let ip = BindHost::classify("192.168.0.1", None).unwrap();
assert!(ip.is_ip_addr());
assert_eq!(ip.as_ip_addr().unwrap().to_string(), "192.168.0.1");
assert!(!ip.matches("192.168.0.1"));
}
#[test]
fn exact_host_with_v6_family_matches_and_compares_by_family() {
let exact = BindHost::classify("[", Some(Family::V6)).unwrap();
assert_eq!(exact.family(), Some(Family::V6));
assert_eq!(exact.families(), [Family::V6]);
assert!(exact.matches("["));
assert!(!exact.matches("]"));
assert_eq!(exact, BindHost::classify("[", Some(Family::V6)).unwrap());
assert_ne!(exact, BindHost::classify("[", Some(Family::V4)).unwrap());
}
#[test]
fn host_display_debug_and_hash() {
let literal = BindHost::classify("enp17s0", None).unwrap();
assert_eq!(format!("{literal}"), "enp17s0");
assert!(
matches!(&format!("{literal:?}"), s if s.contains("Glob") && s.contains("pattern"))
);
assert_eq!(
hash(&literal),
hash(&BindHost::classify("enp17s0", None).unwrap())
);
let exact = BindHost::classify("[", None).unwrap();
assert_eq!(format!("{exact}"), "[");
assert_eq!(exact.as_str(), "[");
assert!(matches!(&format!("{exact:?}"), s if s.contains("Exact") && s.contains("nic")));
assert_eq!(hash(&exact), hash(&BindHost::classify("[", None).unwrap()));
let ip = BindHost::classify("::1", None).unwrap();
let ip_debug = format!("{ip:?}");
assert!(ip_debug.contains("Ip"));
assert!(ip_debug.contains("addr"));
assert!(ip_debug.contains("repr"));
assert_eq!(format!("{ip}"), "::1");
assert_eq!(hash(&ip), hash(&BindHost::classify("::1", None).unwrap()));
}
#[test]
fn partial_eq_for_all_variants() {
let exact_a = BindHost::classify("[", None).unwrap();
let exact_b = BindHost::classify("[", None).unwrap();
assert_eq!(exact_a, exact_b);
let exact_c = BindHost::classify("[", Some(Family::V4)).unwrap();
assert_ne!(exact_a, exact_c);
let glob_a = BindHost::classify("en*", None).unwrap();
let glob_b = BindHost::classify("en*", Some(Family::V6)).unwrap();
assert_ne!(glob_a, glob_b);
assert_eq!(glob_a, BindHost::classify("en*", None).unwrap());
assert_ne!(exact_a, glob_a);
let ip_a = BindHost::classify("::1", None).unwrap();
let ip_b = BindHost::classify("::1", None).unwrap();
let ipv4 = BindHost::classify("127.0.0.1", None).unwrap();
assert_eq!(ip_a, ip_b);
assert_ne!(ip_a, ipv4);
assert_ne!(glob_a, ip_a);
}
}