Skip to main content

hardware_address/
mac.rs

1addr_ty!(
2  /// Represents a physical hardware address (MAC address).
3  #[doc(alias = "Eui48Addr")]
4  MacAddr[6]
5);
6
7#[cfg(test)]
8mod tests {
9  use super::*;
10  use crate::{ParseError, TestCase};
11
12  use std::{string::ToString, vec, vec::Vec};
13
14  const MAC_ADDRESS_SIZE: usize = 6;
15
16  fn test_cases() -> Vec<TestCase<MAC_ADDRESS_SIZE>> {
17    vec![
18      // RFC 7042, Section 2.1.1
19      TestCase {
20        input: "00:00:5e:00:53:01",
21        output: Some(vec![0x00, 0x00, 0x5e, 0x00, 0x53, 0x01]),
22        err: None,
23      },
24      TestCase {
25        input: "00-00-5e-00-53-01",
26        output: Some(vec![0x00, 0x00, 0x5e, 0x00, 0x53, 0x01]),
27        err: None,
28      },
29      TestCase {
30        input: "0000.5e00.5301",
31        output: Some(vec![0x00, 0x00, 0x5e, 0x00, 0x53, 0x01]),
32        err: None,
33      },
34      TestCase {
35        input: "ab:cd:ef:AB:CD:EF",
36        output: Some(vec![0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef]),
37        err: None,
38      },
39      // Invalid MAC-48 cases
40      TestCase {
41        input: "01.02.03.04.05.06",
42        output: None,
43        err: Some(ParseError::InvalidSeparator(b'.')),
44      },
45      TestCase {
46        input: "01:02:03:04:05:06:",
47        output: None,
48        err: Some(ParseError::InvalidLength(18)),
49      },
50      TestCase {
51        input: "x1:02:03:04:05:06",
52        output: None,
53        err: Some(ParseError::InvalidHexDigit([b'x', b'1'])),
54      },
55      TestCase {
56        input: "01-02:03:04:05:06",
57        output: None,
58        err: Some(ParseError::UnexpectedSeparator {
59          expected: b'-',
60          actual: b':',
61        }),
62      },
63    ]
64  }
65
66  #[test]
67  fn parse() {
68    let cases = test_cases();
69    for (i, test) in cases.iter().enumerate() {
70      let result = MacAddr::try_from(test.input);
71
72      match (result, &test.output) {
73        (Ok(out), Some(expected)) => {
74          assert_eq!(
75            expected.as_slice(),
76            out,
77            "Test case {}: MacAddr::parse({}) output mismatch",
78            i,
79            test.input
80          );
81
82          // Test round-trip if this was a valid case
83          if test.err.is_none() {
84            let formatted = out.to_string();
85            let round_trip = MacAddr::try_from(formatted.as_str());
86            assert!(
87              round_trip.is_ok(),
88              "Test case {}: Round-trip parse failed for {}",
89              i,
90              formatted
91            );
92            assert_eq!(
93              round_trip.unwrap(),
94              out,
95              "Test case {}: Round-trip value mismatch",
96              i
97            );
98          }
99        }
100        (Err(err), None) => {
101          assert_eq!(
102            Some(&err),
103            test.err.as_ref(),
104            "Test case {}: Expected error containing '{:?}', got '{:?}'",
105            i,
106            test.err,
107            err
108          );
109        }
110        (Ok(out), None) => {
111          panic!(
112            "Test case {}: Expected error '{:?}', got success: {:?}",
113            i, test.err, out
114          );
115        }
116        (Err(err), Some(expected)) => {
117          panic!(
118            "Test case {}: Expected {:?}, got error: {:?}",
119            i, expected, err
120          );
121        }
122      }
123    }
124  }
125
126  #[test]
127  fn test_default() {
128    let addr = MacAddr::default();
129    assert_eq!(addr.octets(), [0, 0, 0, 0, 0, 0]);
130  }
131
132  #[test]
133  fn formatted() {
134    let addr = MacAddr::try_from("00:00:5e:00:53:01").unwrap();
135    assert_eq!(addr.to_string(), "00:00:5e:00:53:01");
136    assert_eq!(addr.to_colon_separated(), "00:00:5e:00:53:01");
137
138    let dot = addr.to_dot_separated_array();
139    let dot_str = core::str::from_utf8(&dot).unwrap();
140    assert_eq!(dot_str, "0000.5e00.5301");
141    assert_eq!(addr.to_dot_separated(), "0000.5e00.5301");
142
143    let dashed = addr.to_hyphen_separated_array();
144    let dashed_str = core::str::from_utf8(&dashed).unwrap();
145    assert_eq!(dashed_str, "00-00-5e-00-53-01");
146    assert_eq!(addr.to_hyphen_separated(), "00-00-5e-00-53-01");
147  }
148
149  #[cfg(feature = "serde")]
150  #[test]
151  fn serde_human_readable() {
152    let addr = MacAddr::try_from("00:00:5e:00:53:01").unwrap();
153    let json = serde_json::to_string(&addr).unwrap();
154    assert_eq!(json, "\"00:00:5e:00:53:01\"");
155
156    let addr2: MacAddr = serde_json::from_str(&json).unwrap();
157    assert_eq!(addr, addr2);
158  }
159
160  #[cfg(feature = "serde")]
161  #[test]
162  fn serde_human_unreadable() {
163    let addr = MacAddr::try_from("00:00:5e:00:53:01").unwrap();
164    let json = bincode::serde::encode_to_vec(addr, bincode::config::standard()).unwrap();
165    assert_eq!(json, [0, 0, 94, 0, 83, 1]);
166    assert_eq!(addr.octets(), [0, 0, 94, 0, 83, 1]);
167
168    let addr2: MacAddr = bincode::serde::decode_from_slice(&json, bincode::config::standard())
169      .unwrap()
170      .0;
171    assert_eq!(addr, addr2);
172
173    let addr3 = MacAddr::from_raw([0, 0, 94, 0, 83, 1]);
174    assert_eq!(addr, addr3);
175
176    println!("{:?}", addr);
177  }
178
179  // ------------------------------------------------------------------
180  // `arbitrary` / `quickcheck` Arbitrary impls
181  // ------------------------------------------------------------------
182
183  #[cfg(feature = "arbitrary")]
184  #[test]
185  fn arbitrary_is_deterministic() {
186    use arbitrary::{Arbitrary, Unstructured};
187
188    // The generated impl delegates to `<[u8; 6]>::arbitrary`, so the
189    // same input bytes must produce the same output every time.
190    let data = [0xAA; 32];
191    let a = MacAddr::arbitrary(&mut Unstructured::new(&data)).expect("arbitrary should succeed");
192    let b = MacAddr::arbitrary(&mut Unstructured::new(&data)).expect("arbitrary should succeed");
193    assert_eq!(a, b, "arbitrary should be deterministic for a fixed input");
194  }
195
196  #[cfg(feature = "arbitrary")]
197  #[test]
198  fn arbitrary_size_hint_matches_byte_array() {
199    use arbitrary::Arbitrary;
200
201    // The generated impl must forward `size_hint` to the byte array's
202    // — otherwise fuzzers will over- or under-budget the input.
203    let hint = MacAddr::size_hint(0);
204    let expected = <[u8; MAC_ADDRESS_SIZE] as Arbitrary>::size_hint(0);
205    assert_eq!(hint, expected);
206  }
207
208  #[cfg(feature = "arbitrary")]
209  #[test]
210  fn arbitrary_consumes_expected_bytes() {
211    use arbitrary::{Arbitrary, Unstructured};
212
213    // A MacAddr is 6 bytes; pulling one from an Unstructured should
214    // leave `buf.len() - 6` bytes available for the next draw.
215    let data = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
216    let mut u = Unstructured::new(&data);
217    let _first = MacAddr::arbitrary(&mut u).unwrap();
218    let _second = MacAddr::arbitrary(&mut u).unwrap();
219    // Two 6-byte draws from 12 bytes must succeed; a third draw
220    // against an empty buffer would not, but we don't assert on the
221    // exact error kind (that's an `arbitrary` implementation detail).
222  }
223
224  #[cfg(feature = "quickcheck")]
225  #[test]
226  fn quickcheck_arbitrary_roundtrips_through_string() {
227    use quickcheck::{Arbitrary, Gen};
228
229    let mut g = Gen::new(32);
230    for _ in 0..128 {
231      let addr = MacAddr::arbitrary(&mut g);
232      let parsed =
233        MacAddr::try_from(addr.to_string().as_str()).expect("to_string() output must parse back");
234      assert_eq!(addr, parsed);
235    }
236  }
237
238  #[cfg(feature = "quickcheck")]
239  #[test]
240  fn quickcheck_shrink_terminates_and_preserves_length() {
241    use quickcheck::Arbitrary;
242
243    let addr = MacAddr::from_raw([0xFF; MAC_ADDRESS_SIZE]);
244    // Cap the collect so a hypothetical upstream change that made
245    // shrink lazy-infinite can't hang the test suite.
246    let shrinks: Vec<_> = addr.shrink().take(4096).collect();
247    assert!(!shrinks.is_empty(), "non-zero address should yield shrinks");
248    // The impl filters `Vec<u8>::shrink` down to length-N vecs before
249    // rebuilding the tuple struct — every yielded item is therefore a
250    // well-formed MacAddr (implicit by type).
251    for s in &shrinks {
252      assert_eq!(s.octets().len(), MAC_ADDRESS_SIZE);
253    }
254  }
255
256  #[cfg(feature = "quickcheck")]
257  #[test]
258  fn quickcheck_shrink_zero_is_empty() {
259    use quickcheck::Arbitrary;
260
261    // A zero MacAddr is already minimal: each byte's u8::shrink is
262    // empty, and the length-reducing shrinks are filtered out by our
263    // `.filter_map` guard, so the overall iterator must be empty.
264    let zero = MacAddr::from_raw([0; MAC_ADDRESS_SIZE]);
265    let shrinks: Vec<_> = zero.shrink().collect();
266    assert!(
267      shrinks.is_empty(),
268      "zero address should yield no shrinks, got {:?}",
269      shrinks
270    );
271  }
272
273  #[cfg(feature = "quickcheck")]
274  #[test]
275  fn quickcheck_roundtrip_property() {
276    // Smoke-test that the `quickcheck` harness accepts our Arbitrary
277    // impl (the full library integration, not just the trait in
278    // isolation). The property below is the same string-roundtrip as
279    // `quickcheck_arbitrary_roundtrips_through_string`, but driven by
280    // `quickcheck::quickcheck` rather than a hand-rolled loop.
281    fn prop(addr: MacAddr) -> bool {
282      MacAddr::try_from(addr.to_string().as_str())
283        .map(|p| p == addr)
284        .unwrap_or(false)
285    }
286    quickcheck::quickcheck(prop as fn(MacAddr) -> bool);
287  }
288}