use std::{fmt, str::FromStr}; use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum MacParseError { #[error("MAC address must contain exactly 6 colon-separated octets")] WrongOctetCount, #[error("invalid MAC octet {index}: {value:?}")] InvalidOctet { index: usize, value: String }, } #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Deserialize, serde::Serialize, )] pub struct MacAddr([u8; 6]); impl MacAddr { pub const BROADCAST: Self = Self([0xff; 6]); pub const ZERO: Self = Self([0; 6]); #[must_use] pub const fn new(bytes: [u8; 6]) -> Self { Self(bytes) } #[must_use] pub const fn octets(self) -> [u8; 6] { self.0 } #[must_use] pub const fn is_zero(self) -> bool { let bytes = self.0; bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0 && bytes[4] == 0 && bytes[5] == 0 } #[must_use] pub const fn is_broadcast(self) -> bool { let bytes = self.0; bytes[0] == 0xff && bytes[1] == 0xff && bytes[2] == 0xff && bytes[3] == 0xff && bytes[4] == 0xff && bytes[5] == 0xff } #[must_use] pub const fn is_multicast(self) -> bool { self.0[0] & 0x01 != 0 } #[must_use] pub const fn is_unicast(self) -> bool { !self.is_multicast() } #[must_use] pub const fn is_locally_administered(self) -> bool { self.0[0] & 0x02 != 0 } #[must_use] pub const fn is_valid_unicast(self) -> bool { self.is_unicast() && !self.is_zero() && !self.is_broadcast() } #[must_use] pub const fn is_valid_client_identity(self) -> bool { self.is_valid_unicast() && self.is_locally_administered() } } impl From<[u8; 6]> for MacAddr { fn from(value: [u8; 6]) -> Self { Self(value) } } impl From for [u8; 6] { fn from(value: MacAddr) -> Self { value.octets() } } impl fmt::Display for MacAddr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let [a, b, c, d, e, g] = self.0; write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}") } } impl FromStr for MacAddr { type Err = MacParseError; fn from_str(value: &str) -> Result { let mut octets = [0; 6]; let mut count = 0; for (index, octet) in value.split(':').enumerate() { if index >= octets.len() { return Err(MacParseError::WrongOctetCount); } if octet.len() != 2 { return Err(MacParseError::InvalidOctet { index, value: octet.to_owned(), }); } octets[index] = u8::from_str_radix(octet, 16).map_err(|_| MacParseError::InvalidOctet { index, value: octet.to_owned(), })?; count += 1; } if count != octets.len() { return Err(MacParseError::WrongOctetCount); } Ok(Self(octets)) } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_and_formats_mac_addresses() { let mac: MacAddr = "02:0a:ff:10:20:30".parse().unwrap(); assert_eq!(mac.octets(), [0x02, 0x0a, 0xff, 0x10, 0x20, 0x30]); assert_eq!(mac.to_string(), "02:0a:ff:10:20:30"); } #[test] fn classifies_client_identity_addresses() { assert!(MacAddr::new([0x02, 1, 2, 3, 4, 5]).is_valid_client_identity()); assert!(!MacAddr::new([0x00, 1, 2, 3, 4, 5]).is_valid_client_identity()); assert!(!MacAddr::new([0x03, 1, 2, 3, 4, 5]).is_valid_client_identity()); assert!(!MacAddr::ZERO.is_valid_client_identity()); assert!(!MacAddr::BROADCAST.is_valid_client_identity()); } #[test] fn rejects_wrong_mac_shape() { assert_eq!( "02:00:00:00:00".parse::().unwrap_err(), MacParseError::WrongOctetCount ); assert!(matches!( "02:00:00:00:00:xx".parse::().unwrap_err(), MacParseError::InvalidOctet { index: 5, .. } )); assert!(matches!( "02:00:00:00:00:0".parse::().unwrap_err(), MacParseError::InvalidOctet { index: 5, .. } )); } }