use std::{fmt, str::FromStr};
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext as _};
use rama_utils::str::smol_str::SmolStr;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeviceName(SmolStr);
impl DeviceName {
#[must_use]
#[expect(
clippy::panic,
reason = "static-str invariant: panic at compile time when the static is not a valid device name"
)]
pub const fn new(name: &'static str) -> Self {
if !is_valid(name.as_bytes()) {
panic!("static str is not a valid (interface) device name");
}
Self(SmolStr::new_static(name))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for DeviceName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for DeviceName {
type Err = BoxError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<String> for DeviceName {
type Error = BoxError;
#[inline]
fn try_from(s: String) -> Result<Self, Self::Error> {
s.as_str().try_into()
}
}
impl TryFrom<&String> for DeviceName {
type Error = BoxError;
#[inline]
fn try_from(value: &String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
impl TryFrom<&str> for DeviceName {
type Error = BoxError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
use rama_core::error::ErrorExt as _;
if is_valid(s.as_bytes()) {
return Ok(Self(SmolStr::from(s)));
}
Err(BoxError::from_static_str("invalid (interface) device name")
.context_str_field("str", s))
}
}
impl TryFrom<Vec<u8>> for DeviceName {
type Error = BoxError;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(bytes.as_slice())
}
}
impl TryFrom<&[u8]> for DeviceName {
type Error = BoxError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let s = core::str::from_utf8(bytes).context("parse (interface) device name from bytes")?;
s.try_into()
}
}
use rama_utils::macros::serde_str::impl_serde_str;
impl_serde_str!(as_str DeviceName);
pub(super) const fn is_valid(s: &[u8]) -> bool {
if s.is_empty() || s.len() > DEVICE_MAX_LEN {
false
} else {
let mut i = 0;
if DEVICE_FIRST_CHARS[s[0] as usize] == 0 {
return false;
}
while i < s.len() {
if DEVICE_CHARS[s[i] as usize] == 0 {
return false;
}
i += 1;
}
true
}
}
const DEVICE_MAX_LEN: usize = 15;
#[rustfmt::skip]
const DEVICE_CHARS: [u8; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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 ];
#[rustfmt::skip]
const DEVICE_FIRST_CHARS: [u8; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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 ];
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[test]
fn test_parse_valid_device_name() {
for s in [
"eth0",
"eth0.100",
"br-lan",
"ens192",
"veth_abcd1234",
"lo",
] {
let msg = format!("parsing '{s}'");
assert_eq!(s, s.parse::<DeviceName>().expect(&msg).as_str());
}
}
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[test]
fn test_parse_display_device_name() {
for s in [
"eth0",
"eth0.100",
"br-lan",
"ens192",
"veth_abcd1234",
"lo",
] {
let msg = format!("parsing '{s}'");
let name: DeviceName = s.parse().expect(&msg);
assert_eq!(name.to_string(), s, "{msg}");
}
}
}