Skip to main content

reliakit_primitives/
mac.rs

1use crate::{PrimitiveError, PrimitiveResult};
2use core::{fmt, str::FromStr};
3
4/// A 48-bit IEEE 802 MAC address, stored as six octets.
5///
6/// [`parse`](Self::parse) accepts the common `aa:bb:cc:dd:ee:ff` and
7/// `aa-bb-cc-dd-ee-ff` text forms (one consistent separator, lower- or
8/// upper-case hex). The type is allocation-free and `no_std`; [`Display`] always
9/// renders the canonical lowercase, colon-separated form.
10///
11/// [`Display`]: core::fmt::Display
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct MacAddress([u8; 6]);
14
15impl MacAddress {
16    /// Builds a MAC address directly from six octets. Always valid.
17    pub const fn from_octets(octets: [u8; 6]) -> Self {
18        Self(octets)
19    }
20
21    /// Parses a MAC address in `aa:bb:cc:dd:ee:ff` or `aa-bb-cc-dd-ee-ff` form.
22    ///
23    /// The separator must be `:` or `-` and the same throughout. Returns an
24    /// error for an empty string, the wrong length, an inconsistent separator,
25    /// or a non-hex octet.
26    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        // 6 two-digit octets plus 5 separators.
32        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    /// Returns the six octets.
61    pub const fn octets(&self) -> [u8; 6] {
62        self.0
63    }
64
65    /// Returns `true` if this is a multicast address (low bit of the first
66    /// octet set).
67    pub const fn is_multicast(&self) -> bool {
68        self.0[0] & 0x01 != 0
69    }
70
71    /// Returns `true` if this is a unicast address.
72    pub const fn is_unicast(&self) -> bool {
73        !self.is_multicast()
74    }
75
76    /// Returns `true` if this address is locally administered (second-lowest bit
77    /// of the first octet set), as opposed to a universally administered (OUI)
78    /// address.
79    pub const fn is_local(&self) -> bool {
80        self.0[0] & 0x02 != 0
81    }
82
83    /// Returns `true` if this address is universally administered.
84    pub const fn is_universal(&self) -> bool {
85        !self.is_local()
86    }
87}
88
89/// Converts one ASCII hex digit to its 0–15 value.
90fn 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()); // too short
168        assert!(MacAddress::parse("aa:bb:cc:dd:ee:ff:00").is_err()); // too long
169        assert!(MacAddress::parse("aa:bb:cc-dd:ee:ff").is_err()); // mixed separators
170        assert!(MacAddress::parse("aa:bb:cc:dd:ee:gg").is_err()); // non-hex
171        assert!(MacAddress::parse("aabb.ccdd.eeff").is_err()); // wrong format
172    }
173
174    #[test]
175    fn conversions_round_trip() {
176        let parsed = MacAddress::try_from("aa:bb:cc:dd:ee:ff").unwrap(); // TryFrom<&str>
177        let from_str: MacAddress = "aa:bb:cc:dd:ee:ff".parse().unwrap(); // FromStr
178        assert_eq!(parsed, from_str);
179
180        let octets: [u8; 6] = parsed.into(); // From<MacAddress> for [u8; 6]
181        assert_eq!(octets, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
182        let rebuilt = MacAddress::from(octets); // From<[u8; 6]>
183        assert_eq!(rebuilt, parsed);
184    }
185
186    #[test]
187    fn classification_bits() {
188        // Multicast: low bit of first octet set.
189        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        // Locally administered: second-lowest bit set.
192        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}