1use std::{fmt, str::FromStr};
2
3use rama_core::error::BoxErrorExt as _;
4use rama_core::error::{BoxError, ErrorContext as _};
5use rama_utils::str::smol_str::SmolStr;
6
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct DeviceName(SmolStr);
10
11impl DeviceName {
12 #[must_use]
14 #[expect(
15 clippy::panic,
16 reason = "static-str invariant: panic at compile time when the static is not a valid device name"
17 )]
18 pub const fn new(name: &'static str) -> Self {
19 if !is_valid(name.as_bytes()) {
20 panic!("static str is not a valid (interface) device name");
21 }
22 Self(SmolStr::new_static(name))
23 }
24
25 #[must_use]
27 pub fn as_bytes(&self) -> &[u8] {
28 self.0.as_bytes()
29 }
30
31 #[must_use]
33 pub fn as_str(&self) -> &str {
34 self.0.as_str()
35 }
36}
37
38impl fmt::Display for DeviceName {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 write!(f, "{}", self.0)
41 }
42}
43
44impl FromStr for DeviceName {
45 type Err = BoxError;
46
47 #[inline]
48 fn from_str(s: &str) -> Result<Self, Self::Err> {
49 Self::try_from(s)
50 }
51}
52
53impl TryFrom<String> for DeviceName {
54 type Error = BoxError;
55
56 #[inline]
57 fn try_from(s: String) -> Result<Self, Self::Error> {
58 s.as_str().try_into()
59 }
60}
61
62impl TryFrom<&String> for DeviceName {
63 type Error = BoxError;
64
65 #[inline]
66 fn try_from(value: &String) -> Result<Self, Self::Error> {
67 value.as_str().try_into()
68 }
69}
70
71impl TryFrom<&str> for DeviceName {
72 type Error = BoxError;
73
74 fn try_from(s: &str) -> Result<Self, Self::Error> {
75 use rama_core::error::ErrorExt as _;
76
77 if is_valid(s.as_bytes()) {
78 return Ok(Self(SmolStr::from(s)));
79 }
80
81 Err(BoxError::from_static_str("invalid (interface) device name")
82 .context_str_field("str", s))
83 }
84}
85
86impl TryFrom<Vec<u8>> for DeviceName {
87 type Error = BoxError;
88
89 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
90 Self::try_from(bytes.as_slice())
91 }
92}
93
94impl TryFrom<&[u8]> for DeviceName {
95 type Error = BoxError;
96
97 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
98 let s = core::str::from_utf8(bytes).context("parse (interface) device name from bytes")?;
99 s.try_into()
100 }
101}
102
103use rama_utils::macros::serde_str::impl_serde_str;
104
105impl_serde_str!(as_str DeviceName);
106
107pub(super) const fn is_valid(s: &[u8]) -> bool {
108 if s.is_empty() || s.len() > DEVICE_MAX_LEN {
109 false
110 } else {
111 let mut i = 0;
112 if DEVICE_FIRST_CHARS[s[0] as usize] == 0 {
113 return false;
114 }
115 while i < s.len() {
116 if DEVICE_CHARS[s[i] as usize] == 0 {
117 return false;
118 }
119 i += 1;
120 }
121 true
122 }
123}
124
125const DEVICE_MAX_LEN: usize = 15;
127
128#[rustfmt::skip]
129const DEVICE_CHARS: [u8; 256] = [
131 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b'-', b'.', 0, b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', 0, 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', 0, 0, 0, 0, b'_', 0, b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
159
160#[rustfmt::skip]
161const DEVICE_FIRST_CHARS: [u8; 256] = [
162 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', 0, 0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
196 #[test]
197 fn test_parse_valid_device_name() {
198 for s in [
199 "eth0",
200 "eth0.100",
201 "br-lan",
202 "ens192",
203 "veth_abcd1234",
204 "lo",
205 ] {
206 let msg = format!("parsing '{s}'");
207
208 assert_eq!(s, s.parse::<DeviceName>().expect(&msg).as_str());
209 }
210 }
211
212 #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
213 #[test]
214 fn test_parse_display_device_name() {
215 for s in [
216 "eth0",
217 "eth0.100",
218 "br-lan",
219 "ens192",
220 "veth_abcd1234",
221 "lo",
222 ] {
223 let msg = format!("parsing '{s}'");
224 let name: DeviceName = s.parse().expect(&msg);
225 assert_eq!(name.to_string(), s, "{msg}");
226 }
227 }
228}