use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IdentifierError {
#[error("{kind}: must not be empty")]
Empty {
kind: &'static str,
},
#[error("{kind}: exceeds {max} bytes (got {len} bytes)")]
TooLong {
kind: &'static str,
len: usize,
max: usize,
},
#[error("{kind}: contains a NUL byte")]
ContainsNul {
kind: &'static str,
},
}
pub const HOST_DEV_NAME_MAX_BYTES: usize = 64;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
#[serde(transparent)]
pub struct HostDevName(String);
impl HostDevName {
pub fn new(name: impl Into<String>) -> Result<Self, IdentifierError> {
let name = name.into();
if name.is_empty() {
return Err(IdentifierError::Empty {
kind: "host_dev_name",
});
}
if name.len() > HOST_DEV_NAME_MAX_BYTES {
return Err(IdentifierError::TooLong {
kind: "host_dev_name",
len: name.len(),
max: HOST_DEV_NAME_MAX_BYTES,
});
}
if name.as_bytes().contains(&0) {
return Err(IdentifierError::ContainsNul {
kind: "host_dev_name",
});
}
Ok(Self(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for HostDevName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for HostDevName {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_accept_short_ascii_name() {
let n = HostDevName::new("vmnet0").unwrap();
assert_eq!(n.as_str(), "vmnet0");
}
#[test]
fn test_should_reject_empty() {
let err = HostDevName::new("").unwrap_err();
assert!(matches!(err, IdentifierError::Empty { .. }));
}
#[test]
fn test_should_reject_overlong() {
let huge = "a".repeat(HOST_DEV_NAME_MAX_BYTES + 1);
let err = HostDevName::new(huge).unwrap_err();
assert!(matches!(err, IdentifierError::TooLong { .. }));
}
#[test]
fn test_should_reject_nul_byte() {
let err = HostDevName::new("vmnet\0bad").unwrap_err();
assert!(matches!(err, IdentifierError::ContainsNul { .. }));
}
#[test]
fn test_should_round_trip_through_serde_json() {
let n = HostDevName::new("eth0").unwrap();
let json = serde_json::to_string(&n).unwrap();
assert_eq!(json, "\"eth0\"");
let back: HostDevName = serde_json::from_str(&json).unwrap();
assert_eq!(back.as_str(), "eth0");
}
#[test]
fn test_should_reject_invalid_value_through_deserialize() {
let json = "\"\""; let err = serde_json::from_str::<HostDevName>(json).unwrap_err();
assert!(err.to_string().contains("empty"));
}
}