1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::str;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct MacAddress {
9 pub octets: [u8; 6],
11}
12
13fn normalized_hex(input: &str) -> Option<String> {
14 let trimmed = input.trim();
15
16 if trimmed.is_empty() {
17 return None;
18 }
19
20 if trimmed.contains('.') {
21 let parts: Vec<_> = trimmed.split('.').collect();
22
23 if parts.len() != 3
24 || parts.iter().any(|part| {
25 part.len() != 4 || !part.chars().all(|character| character.is_ascii_hexdigit())
26 })
27 {
28 return None;
29 }
30
31 return Some(parts.concat().to_ascii_uppercase());
32 }
33
34 if trimmed.contains(':') || trimmed.contains('-') {
35 let separator = if trimmed.contains(':') { ':' } else { '-' };
36 let parts: Vec<_> = trimmed.split(separator).collect();
37
38 if parts.len() != 6
39 || parts.iter().any(|part| {
40 part.len() != 2 || !part.chars().all(|character| character.is_ascii_hexdigit())
41 })
42 {
43 return None;
44 }
45
46 return Some(parts.concat().to_ascii_uppercase());
47 }
48
49 if trimmed.len() == 12
50 && trimmed
51 .chars()
52 .all(|character| character.is_ascii_hexdigit())
53 {
54 Some(trimmed.to_ascii_uppercase())
55 } else {
56 None
57 }
58}
59
60pub fn is_mac_address(input: &str) -> bool {
62 parse_mac_address(input).is_some()
63}
64
65pub fn parse_mac_address(input: &str) -> Option<MacAddress> {
67 let hex = normalized_hex(input)?;
68 let mut octets = [0_u8; 6];
69
70 for (index, chunk) in hex.as_bytes().chunks(2).enumerate() {
71 let chunk = str::from_utf8(chunk).ok()?;
72 octets[index] = u8::from_str_radix(chunk, 16).ok()?;
73 }
74
75 Some(MacAddress { octets })
76}
77
78pub fn format_mac_colon(mac: &MacAddress) -> String {
80 mac.octets
81 .iter()
82 .map(|octet| format!("{octet:02X}"))
83 .collect::<Vec<_>>()
84 .join(":")
85}
86
87pub fn format_mac_hyphen(mac: &MacAddress) -> String {
89 mac.octets
90 .iter()
91 .map(|octet| format!("{octet:02X}"))
92 .collect::<Vec<_>>()
93 .join("-")
94}
95
96pub fn normalize_mac(input: &str) -> Option<String> {
98 parse_mac_address(input).map(|mac| format_mac_colon(&mac))
99}
100
101pub fn is_broadcast_mac(input: &str) -> bool {
103 parse_mac_address(input).is_some_and(|mac| mac.octets.iter().all(|octet| *octet == 0xFF))
104}
105
106pub fn is_zero_mac(input: &str) -> bool {
108 parse_mac_address(input).is_some_and(|mac| mac.octets.iter().all(|octet| *octet == 0x00))
109}