reliakit_primitives/
mac.rs1use crate::{PrimitiveError, PrimitiveResult};
2use core::{fmt, str::FromStr};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct MacAddress([u8; 6]);
14
15impl MacAddress {
16 pub const fn from_octets(octets: [u8; 6]) -> Self {
18 Self(octets)
19 }
20
21 pub fn parse(value: &str) -> PrimitiveResult<Self> {
27 if value.is_empty() {
28 return Err(PrimitiveError::Empty);
29 }
30 let bytes = value.as_bytes();
31 if bytes.len() != 17 {
33 return Err(PrimitiveError::Invalid {
34 message: "MAC address must be 17 characters: six octets and five separators",
35 });
36 }
37 let sep = bytes[2];
38 if sep != b':' && sep != b'-' {
39 return Err(PrimitiveError::Invalid {
40 message: "MAC address separator must be ':' or '-'",
41 });
42 }
43 let mut octets = [0u8; 6];
44 let mut i = 0;
45 while i < 6 {
46 let pos = i * 3;
47 if i < 5 && bytes[pos + 2] != sep {
48 return Err(PrimitiveError::Invalid {
49 message: "MAC address must use a single, consistent separator",
50 });
51 }
52 let hi = hex_digit(bytes[pos])?;
53 let lo = hex_digit(bytes[pos + 1])?;
54 octets[i] = (hi << 4) | lo;
55 i += 1;
56 }
57 Ok(Self(octets))
58 }
59
60 pub const fn octets(&self) -> [u8; 6] {
62 self.0
63 }
64
65 pub const fn is_multicast(&self) -> bool {
68 self.0[0] & 0x01 != 0
69 }
70
71 pub const fn is_unicast(&self) -> bool {
73 !self.is_multicast()
74 }
75
76 pub const fn is_local(&self) -> bool {
80 self.0[0] & 0x02 != 0
81 }
82
83 pub const fn is_universal(&self) -> bool {
85 !self.is_local()
86 }
87}
88
89fn hex_digit(b: u8) -> PrimitiveResult<u8> {
91 match b {
92 b'0'..=b'9' => Ok(b - b'0'),
93 b'a'..=b'f' => Ok(b - b'a' + 10),
94 b'A'..=b'F' => Ok(b - b'A' + 10),
95 _ => Err(PrimitiveError::Invalid {
96 message: "MAC address octets must be hexadecimal",
97 }),
98 }
99}
100
101impl fmt::Display for MacAddress {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 let o = self.0;
104 write!(
105 f,
106 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
107 o[0], o[1], o[2], o[3], o[4], o[5]
108 )
109 }
110}
111
112impl From<[u8; 6]> for MacAddress {
113 fn from(octets: [u8; 6]) -> Self {
114 Self::from_octets(octets)
115 }
116}
117
118impl From<MacAddress> for [u8; 6] {
119 fn from(mac: MacAddress) -> Self {
120 mac.0
121 }
122}
123
124impl TryFrom<&str> for MacAddress {
125 type Error = PrimitiveError;
126
127 fn try_from(value: &str) -> Result<Self, Self::Error> {
128 Self::parse(value)
129 }
130}
131
132impl FromStr for MacAddress {
133 type Err = PrimitiveError;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 Self::parse(s)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::MacAddress;
143 use crate::PrimitiveErrorKind;
144
145 #[test]
146 fn parses_colon_and_dash_forms() {
147 let m = MacAddress::parse("0A:1b:2C:3d:4E:5f").unwrap();
148 assert_eq!(m.octets(), [0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f]);
149 let d = MacAddress::parse("0a-1b-2c-3d-4e-5f").unwrap();
150 assert_eq!(d, m);
151 }
152
153 #[test]
154 fn display_is_canonical_lowercase_colon() {
155 let m = MacAddress::from_octets([0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45]);
156 extern crate alloc;
157 use alloc::string::ToString;
158 assert_eq!(m.to_string(), "ab:cd:ef:01:23:45");
159 }
160
161 #[test]
162 fn rejects_malformed() {
163 assert_eq!(
164 MacAddress::parse("").unwrap_err().kind(),
165 PrimitiveErrorKind::Empty
166 );
167 assert!(MacAddress::parse("aa:bb:cc:dd:ee").is_err()); assert!(MacAddress::parse("aa:bb:cc:dd:ee:ff:00").is_err()); assert!(MacAddress::parse("aa:bb:cc-dd:ee:ff").is_err()); assert!(MacAddress::parse("aa:bb:cc:dd:ee:gg").is_err()); assert!(MacAddress::parse("aabb.ccdd.eeff").is_err()); }
173
174 #[test]
175 fn conversions_round_trip() {
176 let parsed = MacAddress::try_from("aa:bb:cc:dd:ee:ff").unwrap(); let from_str: MacAddress = "aa:bb:cc:dd:ee:ff".parse().unwrap(); assert_eq!(parsed, from_str);
179
180 let octets: [u8; 6] = parsed.into(); assert_eq!(octets, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
182 let rebuilt = MacAddress::from(octets); assert_eq!(rebuilt, parsed);
184 }
185
186 #[test]
187 fn classification_bits() {
188 assert!(MacAddress::from_octets([0x01, 0, 0, 0, 0, 0]).is_multicast());
190 assert!(MacAddress::from_octets([0x02, 0, 0, 0, 0, 0]).is_unicast());
191 assert!(MacAddress::from_octets([0x02, 0, 0, 0, 0, 0]).is_local());
193 assert!(MacAddress::from_octets([0x00, 0, 0, 0, 0, 0]).is_universal());
194 }
195}