Skip to main content

hardware_address/
lib.rs

1// The code is inspired by https://github.com/golang/go/blob/master/src/net/mac.go
2
3#![doc = include_str!("../README.md")]
4#![cfg_attr(not(feature = "std"), no_std)]
5#![cfg_attr(docsrs, feature(doc_cfg))]
6#![cfg_attr(docsrs, allow(unused_attributes))]
7#![deny(missing_docs)]
8
9#[cfg(feature = "std")]
10extern crate std;
11
12#[cfg(all(feature = "alloc", not(feature = "std")))]
13#[allow(unused_extern_crates)]
14extern crate alloc as std;
15
16/// A macro for defining address types.
17#[macro_export]
18macro_rules! addr_ty {
19  (
20    $(#[$attr:meta])*
21    $name:ident[$n:expr]
22  ) => {
23    paste::paste! {
24      pub use [< __ $name:snake __ >]::{$name, [< Parse $name Error >]};
25
26      #[doc(hidden)]
27      #[allow(unused)]
28      mod [< __ $name:snake __ >] {
29        #[cfg(feature = "pyo3")]
30        use $crate::__private::pyo3 as __pyo3;
31
32        #[cfg(feature = "wasm-bindgen")]
33        use $crate::__private::wasm_bindgen as __wasm_bindgen;
34
35        #[doc = "Represents an error that occurred while parsing `" $name "`."]
36        pub type [< Parse $name Error >] = $crate::ParseError<$n>;
37
38        $(#[$attr])*
39        #[derive(::core::clone::Clone, ::core::marker::Copy, ::core::cmp::Eq, ::core::cmp::PartialEq, ::core::cmp::Ord, ::core::cmp::PartialOrd, ::core::hash::Hash)]
40        // `from_py_object` explicitly opts in to the `FromPyObject`
41        // derive that pyo3 used to generate automatically for `Clone`
42        // types. Address types are tiny (`[u8; N]` with `N` in {6, 8, 20}),
43        // so the Clone-based conversion is effectively free and lets them
44        // be passed as arguments to `#[pyfunction]` / `#[pymethods]`.
45        #[cfg_attr(feature = "pyo3", $crate::__private::pyo3::pyclass(crate = "__pyo3", from_py_object))]
46        #[cfg_attr(feature = "wasm-bindgen", $crate::__private::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = __wasm_bindgen))]
47        #[repr(transparent)]
48        pub struct $name(pub(crate) [::core::primitive::u8; $n]);
49      }
50    }
51
52    #[allow(unexpected_cfgs)]
53    const _: () = {
54      impl ::core::default::Default for $name {
55        #[inline]
56        fn default() -> Self {
57          $name::new()
58        }
59      }
60
61      impl $name {
62        /// The size of the address in bytes.
63        pub const SIZE: ::core::primitive::usize = $n;
64
65        /// Creates a zeroed address.
66        #[inline]
67        pub const fn new() -> Self {
68          $name([0; $n])
69        }
70
71        /// Creates from raw byte array address.
72        #[inline]
73        pub const fn from_raw(addr: [::core::primitive::u8; $n]) -> Self {
74          $name(addr)
75        }
76
77        /// Returns the address as a byte slice.
78        #[inline]
79        pub const fn as_bytes(&self) -> &[::core::primitive::u8] {
80          &self.0
81        }
82
83        /// Returns the octets of the address.
84        #[inline]
85        pub const fn octets(&self) -> [::core::primitive::u8; $n] {
86          self.0
87        }
88
89        /// Returns an array contains a colon formatted address.
90        ///
91        /// The returned array can be used to directly convert to `str`
92        /// by using [`core::str::from_utf8(&array).unwrap( )`](core::str::from_utf8).
93        #[inline]
94        pub const fn to_colon_separated_array(&self) -> [::core::primitive::u8; $n * 3 - 1] {
95          let mut buf = [0u8; $n * 3 - 1];
96          let mut i = 0;
97
98          while i < $n {
99            if i > 0 {
100              buf[i * 3 - 1] = b':';
101            }
102
103            buf[i * 3] = $crate::__private::HEX_DIGITS[(self.0[i] >> 4) as ::core::primitive::usize];
104            buf[i * 3 + 1] = $crate::__private::HEX_DIGITS[(self.0[i] & 0xF) as ::core::primitive::usize];
105            i += 1;
106          }
107
108          buf
109        }
110
111        /// Returns an array contains a hyphen formatted address.
112        ///
113        /// The returned array can be used to directly convert to `str`
114        /// by using [`core::str::from_utf8(&array).unwrap( )`](core::str::from_utf8).
115        #[inline]
116        pub const fn to_hyphen_separated_array(&self) -> [::core::primitive::u8; $n * 3 - 1] {
117          let mut buf = [0u8; $n * 3 - 1];
118          let mut i = 0;
119
120          while i < $n {
121            if i > 0 {
122              buf[i * 3 - 1] = b'-';
123            }
124
125            buf[i * 3] = $crate::__private::HEX_DIGITS[(self.0[i] >> 4) as ::core::primitive::usize];
126            buf[i * 3 + 1] = $crate::__private::HEX_DIGITS[(self.0[i] & 0xF) as ::core::primitive::usize];
127            i += 1;
128          }
129
130          buf
131        }
132
133        /// Returns an array contains a dot formatted address.
134        ///
135        /// The returned array can be used to directly convert to `str`
136        /// by using [`core::str::from_utf8(&array).unwrap( )`](core::str::from_utf8).
137        #[inline]
138        pub const fn to_dot_separated_array(&self) -> [::core::primitive::u8; $n * 2 + ($n / 2 - 1)] {
139          let mut buf = [0u8; $n * 2 + ($n / 2 - 1)];
140          let mut i = 0;
141
142          while i < $n {
143            // Convert first nibble to hex char
144            buf[i * 2 + i / 2] = $crate::__private::HEX_DIGITS[(self.0[i] >> 4) as ::core::primitive::usize];
145            // Convert second nibble to hex char
146            buf[i * 2 + 1 + i / 2] = $crate::__private::HEX_DIGITS[(self.0[i] & 0xF) as ::core::primitive::usize];
147
148            // Add dot every 2 bytes except for the last group
149            if i % 2 == 1 && i != $n - 1 {
150              buf[i * 2 + 2 + i / 2] = b'.';
151            }
152            i += 1;
153          }
154
155          buf
156        }
157
158        /// Converts to colon-separated format string.
159        #[cfg(any(feature = "alloc", feature = "std"))]
160        #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
161        pub fn to_colon_separated(&self) -> $crate::__private::String {
162          let buf = self.to_colon_separated_array();
163          // SAFETY: The buffer is always valid UTF-8 as it only contains ASCII characters.
164          unsafe { $crate::__private::ToString::to_string(::core::str::from_utf8_unchecked(&buf)) }
165        }
166
167        /// Converts to hyphen-separated format string.
168        #[cfg(any(feature = "alloc", feature = "std"))]
169        #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
170        pub fn to_hyphen_separated(&self) -> $crate::__private::String {
171          let buf = self.to_hyphen_separated_array();
172          // SAFETY: The buffer is always valid UTF-8 as it only contains ASCII characters.
173          unsafe { $crate::__private::ToString::to_string(::core::str::from_utf8_unchecked(&buf)) }
174        }
175
176        /// Converts to dot-separated format string.
177        #[cfg(any(feature = "alloc", feature = "std"))]
178        #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
179        pub fn to_dot_separated(&self) -> $crate::__private::String {
180          let buf = self.to_dot_separated_array();
181          // SAFETY: The buffer is always valid UTF-8 as it only contains ASCII characters.
182          unsafe { $crate::__private::ToString::to_string(::core::str::from_utf8_unchecked(&buf)) }
183        }
184      }
185
186      impl ::core::str::FromStr for $name {
187        type Err = $crate::__private::paste::paste! { [< Parse $name Error >] };
188
189        #[inline]
190        fn from_str(src: &str) -> ::core::result::Result<Self, Self::Err> {
191          $crate::parse::<$n>(src.as_bytes()).map(Self)
192        }
193      }
194
195      impl ::core::cmp::PartialEq<[::core::primitive::u8]> for $name {
196        #[inline]
197        fn eq(&self, other: &[::core::primitive::u8]) -> bool {
198          self.0.eq(other)
199        }
200      }
201
202      impl ::core::cmp::PartialEq<$name> for [::core::primitive::u8] {
203        #[inline]
204        fn eq(&self, other: &$name) -> bool {
205          other.eq(self)
206        }
207      }
208
209      impl ::core::cmp::PartialEq<&[::core::primitive::u8]> for $name {
210        #[inline]
211        fn eq(&self, other: &&[::core::primitive::u8]) -> bool {
212          ::core::cmp::PartialEq::eq(self, *other)
213        }
214      }
215
216      impl ::core::cmp::PartialEq<$name> for &[::core::primitive::u8] {
217        #[inline]
218        fn eq(&self, other: &$name) -> bool {
219          ::core::cmp::PartialEq::eq(*self, other)
220        }
221      }
222
223      impl ::core::borrow::Borrow<[::core::primitive::u8]> for $name {
224        #[inline]
225        fn borrow(&self) -> &[::core::primitive::u8] {
226          self
227        }
228      }
229
230      impl ::core::ops::Deref for $name {
231        type Target = [::core::primitive::u8];
232
233        #[inline]
234        fn deref(&self) -> &Self::Target {
235          self.as_bytes()
236        }
237      }
238
239      impl ::core::convert::AsRef<[::core::primitive::u8]> for $name {
240        #[inline]
241        fn as_ref(&self) -> &[::core::primitive::u8] {
242          ::core::borrow::Borrow::borrow(self)
243        }
244      }
245
246      impl ::core::convert::From<[::core::primitive::u8; $n]> for $name {
247        #[inline]
248        fn from(addr: [::core::primitive::u8; $n]) -> Self {
249          $name(addr)
250        }
251      }
252
253      impl ::core::convert::From<$name> for [::core::primitive::u8; $n] {
254        #[inline]
255        #[allow(unexpected_cfgs)]
256        fn from(addr: $name) -> Self {
257          addr.0
258        }
259      }
260
261      impl ::core::convert::TryFrom<&str> for $name {
262        type Error = $crate::__private::paste::paste! { [< Parse $name Error >] };
263
264        #[inline]
265        fn try_from(src: &str) -> ::core::result::Result<Self, Self::Error> {
266          <$name as ::core::str::FromStr>::from_str(src)
267        }
268      }
269
270      impl ::core::fmt::Debug for $name {
271        #[inline]
272        fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
273          ::core::fmt::Display::fmt(self, f)
274        }
275      }
276
277      impl core::fmt::Display for $name {
278        #[inline]
279        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280          let buf = self.to_colon_separated_array();
281          write!(
282            f,
283            "{}",
284            // SAFETY: The buffer is always valid UTF-8 as it only contains ASCII characters.
285            unsafe { ::core::str::from_utf8_unchecked(&buf) },
286          )
287        }
288      }
289    };
290
291    #[cfg(feature = "serde")]
292    const _: () = {
293      impl $crate::__private::serde::Serialize for $name {
294        fn serialize<S>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error>
295        where
296          S: $crate::__private::serde::Serializer,
297        {
298          if serializer.is_human_readable() {
299            let buf = self.to_colon_separated_array();
300            // SAFETY: The buffer is always valid UTF-8 as it only contains ASCII characters.
301            serializer.serialize_str(unsafe { ::core::str::from_utf8_unchecked(&buf) })
302          } else {
303            <[::core::primitive::u8; $n] as $crate::__private::serde::Serialize>::serialize(&self.0, serializer)
304          }
305        }
306      }
307
308      impl<'a> $crate::__private::serde::Deserialize<'a> for $name {
309        fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
310        where
311          D: $crate::__private::serde::Deserializer<'a>,
312        {
313          if deserializer.is_human_readable() {
314            let s = <&str as $crate::__private::serde::Deserialize>::deserialize(deserializer)?;
315            <$name as ::core::str::FromStr>::from_str(s).map_err($crate::__private::serde::de::Error::custom)
316          } else {
317            let bytes = <[::core::primitive::u8; $n] as $crate::__private::serde::Deserialize>::deserialize(deserializer)?;
318            ::core::result::Result::Ok($name(bytes))
319          }
320        }
321      }
322    };
323
324    #[cfg(feature = "arbitrary")]
325    $crate::__addr_ty_arbitrary! { $name[$n] }
326
327    #[cfg(feature = "quickcheck")]
328    $crate::__addr_ty_quickcheck! { $name[$n] }
329
330    #[cfg(feature = "pyo3")]
331    $crate::__addr_ty_pyo3! { $name[$n] }
332
333    #[cfg(feature = "wasm-bindgen")]
334    $crate::__addr_ty_wasm_bindgen! { $name[$n] }
335  }
336}
337
338mod mac;
339pub use mac::*;
340
341mod eui64;
342pub use eui64::*;
343
344mod infini_band;
345pub use infini_band::*;
346
347#[cfg(feature = "pyo3")]
348mod py;
349#[cfg(feature = "wasm-bindgen")]
350mod wasm;
351
352#[cfg(feature = "arbitrary")]
353mod arbitrary;
354
355#[cfg(feature = "quickcheck")]
356mod quickcheck;
357
358#[doc(hidden)]
359pub mod __private {
360  /// Lowercase ASCII hex digits for formatting.
361  pub const HEX_DIGITS: [::core::primitive::u8; 16] = *b"0123456789abcdef";
362
363  /// Lookup table: ASCII byte → nibble value (`0..=15`), or `0xFF` for
364  /// anything that isn't a valid hex digit. Branch-free alternative to
365  /// chained `match` arms.
366  pub const HEX_VAL: [::core::primitive::u8; 256] = {
367    let mut t = [0xFFu8; 256];
368    let mut i = 0;
369    while i < 10 {
370      t[b'0' as usize + i] = i as u8;
371      i += 1;
372    }
373    let mut i = 0;
374    while i < 6 {
375      t[b'a' as usize + i] = (i + 10) as u8;
376      t[b'A' as usize + i] = (i + 10) as u8;
377      i += 1;
378    }
379    t
380  };
381
382  /// Parse two ASCII hex characters into a single byte.
383  ///
384  /// Returns `None` if either character isn't a valid hex digit. Used
385  /// by [`crate::parse`] and (internally) by [`crate::xtoi2`].
386  #[inline]
387  pub const fn hex_byte(
388    hi: ::core::primitive::u8,
389    lo: ::core::primitive::u8,
390  ) -> ::core::option::Option<::core::primitive::u8> {
391    let hi_val = HEX_VAL[hi as usize];
392    if hi_val == 0xFF {
393      return ::core::option::Option::None;
394    }
395    let lo_val = HEX_VAL[lo as usize];
396    if lo_val == 0xFF {
397      return ::core::option::Option::None;
398    }
399    ::core::option::Option::Some((hi_val << 4) | lo_val)
400  }
401
402  #[cfg(feature = "serde")]
403  pub use serde;
404
405  #[cfg(feature = "arbitrary")]
406  pub use arbitrary;
407
408  #[cfg(feature = "quickcheck")]
409  pub use quickcheck;
410
411  #[cfg(feature = "pyo3")]
412  pub use pyo3;
413
414  #[cfg(all(feature = "pyo3", feature = "std"))]
415  pub use std::hash::DefaultHasher;
416  #[cfg(all(feature = "pyo3", not(feature = "std")))]
417  pub type DefaultHasher = ::core::hash::BuildHasherDefault<::core::hash::SipHasher>;
418
419  #[cfg(feature = "wasm-bindgen")]
420  pub use wasm_bindgen;
421
422  #[cfg(any(feature = "alloc", feature = "std"))]
423  pub use std::{
424    boxed::Box,
425    string::{String, ToString},
426    vec::Vec,
427  };
428
429  pub use paste;
430}
431
432/// Converts a hexadecimal slice to an integer.
433///
434/// Reads as many leading ASCII hex digits from `bytes` as fit in an
435/// `i32` and stops at the first non-hex byte.
436///
437/// Returns a tuple of `(parsed, consumed)` — the value and the number
438/// of bytes consumed — or `None` if:
439///
440/// - `bytes` is empty,
441/// - the first byte isn't a valid hex digit, or
442/// - the accumulated value would overflow `i32` (i.e. exceed
443///   `i32::MAX`).
444#[inline]
445pub const fn xtoi(bytes: &[::core::primitive::u8]) -> Option<(i32, ::core::primitive::usize)> {
446  // Use `u32` internally so `n * 16 + digit` never panics on overflow
447  // in debug builds. We still cap at `i32::MAX` to preserve the
448  // public signature's non-negative `i32` contract.
449  let mut n: u32 = 0;
450  let mut idx = 0;
451  let num_bytes = bytes.len();
452
453  while idx < num_bytes {
454    let digit = __private::HEX_VAL[bytes[idx] as usize];
455    if digit == 0xFF {
456      break;
457    }
458
459    n = match n.checked_mul(16) {
460      Some(v) => v,
461      None => return None,
462    };
463    n = match n.checked_add(digit as u32) {
464      Some(v) => v,
465      None => return None,
466    };
467    if n > i32::MAX as u32 {
468      return None;
469    }
470
471    idx += 1;
472  }
473
474  if idx == 0 {
475    return None;
476  }
477
478  Some((n as i32, idx))
479}
480
481/// Converts the next two hex digits of `s` into a byte.
482///
483/// If `s` is longer than 2 bytes then the third byte must match `e`.
484/// Returns `None` if either of the first two bytes isn't a valid hex
485/// digit, or if `s.len() > 2` and `s[2] != e`.
486#[inline]
487pub const fn xtoi2(s: &[u8], e: u8) -> Option<::core::primitive::u8> {
488  if s.len() < 2 {
489    return None;
490  }
491  if s.len() > 2 && s[2] != e {
492    return None;
493  }
494  __private::hex_byte(s[0], s[1])
495}
496
497#[inline]
498const fn dot_separated_format_len<const N: ::core::primitive::usize>() -> ::core::primitive::usize {
499  N * 2 + (N / 2 - 1)
500}
501
502#[inline]
503const fn colon_separated_format_len<const N: ::core::primitive::usize>() -> ::core::primitive::usize
504{
505  N * 3 - 1
506}
507
508/// ParseError represents an error that occurred while parsing hex address.
509#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
510pub enum ParseError<const N: ::core::primitive::usize> {
511  /// Returned when the input string has a invalid length.
512  #[error("invalid length: colon or hyphen separated format requires {ch_len} bytes, dot separated format requires {dlen} bytes, but got {0} bytes", ch_len = colon_separated_format_len::<N>(), dlen = dot_separated_format_len::<N>())]
513  InvalidLength(::core::primitive::usize),
514  /// Returned when the input string has an invalid seperator.
515  #[error("unexpected separator: expected {expected}, but got {actual}")]
516  UnexpectedSeparator {
517    /// The expected separator.
518    expected: u8,
519    /// The actual separator.
520    actual: u8,
521  },
522  /// Returned when the input string has an unexpected separator.
523  #[error("invalid separator: {0}")]
524  InvalidSeparator(u8),
525  /// Invalid digit.
526  #[error("invalid digit: {0:?}")]
527  InvalidHexDigit([::core::primitive::u8; 2]),
528}
529
530impl<const N: ::core::primitive::usize> ParseError<N> {
531  /// Returns the length of the address.
532  #[inline]
533  pub const fn invalid_length(len: ::core::primitive::usize) -> Self {
534    Self::InvalidLength(len)
535  }
536
537  /// Returns an error for an unexpected separator.
538  #[inline]
539  pub const fn unexpected_separator(expected: u8, actual: u8) -> Self {
540    Self::UnexpectedSeparator { expected, actual }
541  }
542
543  /// Returns an error for an invalid separator.
544  #[inline]
545  pub const fn invalid_separator(sep: u8) -> Self {
546    Self::InvalidSeparator(sep)
547  }
548
549  /// Returns an error for an invalid hex digit.
550  #[inline]
551  pub const fn invalid_hex_digit(digit: [::core::primitive::u8; 2]) -> Self {
552    Self::InvalidHexDigit(digit)
553  }
554}
555
556/// Parses s as an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet
557/// IP over InfiniBand link-layer address and etc using one of the following formats:
558///
559/// - Colon-separated:
560///   - `00:00:5e:00:53:01`
561///   - `02:00:5e:10:00:00:00:01`
562///
563/// - Hyphen-separated:
564///   - `00-00-5e-00-53-01`
565///   - `02-00-5e-10-00-00-00-01`
566///
567/// - Dot-separated:
568///   - `0000.5e00.5301`
569///   - `0200.5e10.0000.0001`
570pub const fn parse<const N: ::core::primitive::usize>(
571  src: &[u8],
572) -> Result<[::core::primitive::u8; N], ParseError<N>> {
573  let dot_separated_len = dot_separated_format_len::<N>();
574  let colon_separated_len = colon_separated_format_len::<N>();
575  let len = src.len();
576
577  if len == dot_separated_len {
578    let mut hw = [0u8; N];
579    let mut x = 0usize;
580    let mut i = 0usize;
581
582    while i < N {
583      // Validate the `.` separator between each 4-hex-digit group,
584      // except when we're at the end of the input.
585      if x + 4 < len && src[x + 4] != b'.' {
586        return Err(ParseError::unexpected_separator(b'.', src[x + 4]));
587      }
588
589      match __private::hex_byte(src[x], src[x + 1]) {
590        Some(byte) => hw[i] = byte,
591        None => return Err(ParseError::invalid_hex_digit([src[x], src[x + 1]])),
592      }
593      match __private::hex_byte(src[x + 2], src[x + 3]) {
594        Some(byte) => hw[i + 1] = byte,
595        None => return Err(ParseError::invalid_hex_digit([src[x + 2], src[x + 3]])),
596      }
597
598      x += 5;
599      i += 2;
600    }
601
602    return Ok(hw);
603  }
604
605  if len == colon_separated_len {
606    let sep = src[2];
607    if sep != b':' && sep != b'-' {
608      return Err(ParseError::invalid_separator(sep));
609    }
610
611    let mut hw = [0u8; N];
612    let mut x = 0usize;
613    let mut i = 0usize;
614
615    while i < N {
616      if x + 2 < len {
617        let csep = src[x + 2];
618        if csep != sep {
619          return Err(ParseError::unexpected_separator(sep, csep));
620        }
621      }
622
623      match __private::hex_byte(src[x], src[x + 1]) {
624        Some(byte) => hw[i] = byte,
625        None => return Err(ParseError::invalid_hex_digit([src[x], src[x + 1]])),
626      }
627
628      x += 3;
629      i += 1;
630    }
631
632    return Ok(hw);
633  }
634
635  Err(ParseError::invalid_length(len))
636}
637
638#[cfg(test)]
639struct TestCase<const N: ::core::primitive::usize> {
640  input: &'static str,
641  output: Option<std::vec::Vec<::core::primitive::u8>>,
642  err: Option<ParseError<N>>,
643}
644
645#[cfg(test)]
646mod tests {
647  use super::*;
648
649  #[test]
650  fn test_xtoi() {
651    assert_eq!(xtoi(b""), None);
652    assert_eq!(xtoi(b"0"), Some((0, 1)));
653    assert_eq!(xtoi(b"12"), Some((0x12, 2)));
654    assert_eq!(xtoi(b"1a"), Some((0x1a, 2)));
655    assert_eq!(xtoi(b"1A"), Some((0x1a, 2)));
656    assert_eq!(xtoi(b"12x"), Some((0x12, 2)));
657    assert_eq!(xtoi(b"x12"), None);
658  }
659
660  #[test]
661  fn test_xtoi2() {
662    assert_eq!(xtoi2(b"12", b'\0'), Some(0x12));
663    assert_eq!(xtoi2(b"12x", b'x'), Some(0x12));
664    assert_eq!(xtoi2(b"12y", b'x'), None);
665    assert_eq!(xtoi2(b"1", b'\0'), None);
666    assert_eq!(xtoi2(b"xy", b'\0'), None);
667  }
668
669  /// Regression test for the pre-fix overflow bug: `xtoi("FFFFFFFF")`
670  /// used to silently return `Some((-1, 8))` in release builds and
671  /// panic in debug builds due to the broken `if n == BIG` equality
672  /// check being placed *after* multiplication. The fix uses checked
673  /// arithmetic and caps at `i32::MAX`.
674  #[test]
675  fn test_xtoi_overflow_is_detected() {
676    // 8 'F's = 0xFFFF_FFFF, exceeds i32::MAX.
677    assert_eq!(xtoi(b"FFFFFFFF"), None);
678    // 7 'F's = 0x0FFF_FFFF, fits in i32.
679    assert_eq!(xtoi(b"FFFFFFF"), Some((0x0FFF_FFFF, 7)));
680    // 8 '7's + digits just below i32::MAX also fine.
681    assert_eq!(xtoi(b"7FFFFFFF"), Some((0x7FFF_FFFF, 8)));
682    // One above i32::MAX must reject.
683    assert_eq!(xtoi(b"80000000"), None);
684    // Long all-hex string rejects cleanly instead of wrapping/panicking.
685    assert_eq!(xtoi(b"0123456789ABCDEF"), None);
686  }
687
688  /// `parse` is `const fn`, so we can build `MacAddr`-style constants
689  /// at compile time. This test exercises that directly.
690  #[test]
691  fn test_parse_is_const() {
692    const MAC: [u8; 6] = match parse::<6>(b"00:11:22:33:44:55") {
693      Ok(v) => v,
694      Err(_) => panic!(),
695    };
696    assert_eq!(MAC, [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
697
698    // Also covers the hyphen-separated form.
699    const MAC2: [u8; 6] = match parse::<6>(b"aa-bb-cc-dd-ee-ff") {
700      Ok(v) => v,
701      Err(_) => panic!(),
702    };
703    assert_eq!(MAC2, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
704
705    // And the dot-separated form.
706    const MAC3: [u8; 6] = match parse::<6>(b"0000.5e00.5301") {
707      Ok(v) => v,
708      Err(_) => panic!(),
709    };
710    assert_eq!(MAC3, [0x00, 0x00, 0x5E, 0x00, 0x53, 0x01]);
711  }
712
713  /// Fast-path `hex_byte` sanity: all valid digits, plus a few
714  /// invalids at boundary values.
715  #[test]
716  fn test_hex_byte() {
717    use crate::__private::hex_byte;
718
719    assert_eq!(hex_byte(b'0', b'0'), Some(0x00));
720    assert_eq!(hex_byte(b'f', b'f'), Some(0xFF));
721    assert_eq!(hex_byte(b'F', b'F'), Some(0xFF));
722    assert_eq!(hex_byte(b'1', b'a'), Some(0x1A));
723    assert_eq!(hex_byte(b'1', b'A'), Some(0x1A));
724
725    // Invalid first nibble.
726    assert_eq!(hex_byte(b'g', b'0'), None);
727    assert_eq!(hex_byte(b'/', b'0'), None); // '/' = '0' - 1
728    assert_eq!(hex_byte(b':', b'0'), None); // ':' = '9' + 1
729                                            // Invalid second nibble.
730    assert_eq!(hex_byte(b'0', b'g'), None);
731    // Both invalid.
732    assert_eq!(hex_byte(0, 0), None);
733  }
734}