squib_core/
identifiers.rs1use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17#[derive(Debug, Error, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum IdentifierError {
21 #[error("{kind}: must not be empty")]
23 Empty {
24 kind: &'static str,
27 },
28 #[error("{kind}: exceeds {max} bytes (got {len} bytes)")]
30 TooLong {
31 kind: &'static str,
33 len: usize,
35 max: usize,
37 },
38 #[error("{kind}: contains a NUL byte")]
40 ContainsNul {
41 kind: &'static str,
43 },
44}
45
46pub const HOST_DEV_NAME_MAX_BYTES: usize = 64;
49
50#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
60#[serde(transparent)]
61pub struct HostDevName(String);
62
63impl HostDevName {
64 pub fn new(name: impl Into<String>) -> Result<Self, IdentifierError> {
69 let name = name.into();
70 if name.is_empty() {
71 return Err(IdentifierError::Empty {
72 kind: "host_dev_name",
73 });
74 }
75 if name.len() > HOST_DEV_NAME_MAX_BYTES {
76 return Err(IdentifierError::TooLong {
77 kind: "host_dev_name",
78 len: name.len(),
79 max: HOST_DEV_NAME_MAX_BYTES,
80 });
81 }
82 if name.as_bytes().contains(&0) {
83 return Err(IdentifierError::ContainsNul {
84 kind: "host_dev_name",
85 });
86 }
87 Ok(Self(name))
88 }
89
90 #[must_use]
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95
96 #[must_use]
98 pub fn into_string(self) -> String {
99 self.0
100 }
101}
102
103impl AsRef<str> for HostDevName {
104 fn as_ref(&self) -> &str {
105 &self.0
106 }
107}
108
109impl<'de> Deserialize<'de> for HostDevName {
110 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
111 let s = String::deserialize(de)?;
112 Self::new(s).map_err(serde::de::Error::custom)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_should_accept_short_ascii_name() {
122 let n = HostDevName::new("vmnet0").unwrap();
123 assert_eq!(n.as_str(), "vmnet0");
124 }
125
126 #[test]
127 fn test_should_reject_empty() {
128 let err = HostDevName::new("").unwrap_err();
129 assert!(matches!(err, IdentifierError::Empty { .. }));
130 }
131
132 #[test]
133 fn test_should_reject_overlong() {
134 let huge = "a".repeat(HOST_DEV_NAME_MAX_BYTES + 1);
135 let err = HostDevName::new(huge).unwrap_err();
136 assert!(matches!(err, IdentifierError::TooLong { .. }));
137 }
138
139 #[test]
140 fn test_should_reject_nul_byte() {
141 let err = HostDevName::new("vmnet\0bad").unwrap_err();
142 assert!(matches!(err, IdentifierError::ContainsNul { .. }));
143 }
144
145 #[test]
146 fn test_should_round_trip_through_serde_json() {
147 let n = HostDevName::new("eth0").unwrap();
148 let json = serde_json::to_string(&n).unwrap();
149 assert_eq!(json, "\"eth0\"");
150 let back: HostDevName = serde_json::from_str(&json).unwrap();
151 assert_eq!(back.as_str(), "eth0");
152 }
153
154 #[test]
155 fn test_should_reject_invalid_value_through_deserialize() {
156 let json = "\"\""; let err = serde_json::from_str::<HostDevName>(json).unwrap_err();
158 assert!(err.to_string().contains("empty"));
159 }
160}