1use std::path::PathBuf;
17
18use serde::{Deserialize, Serialize};
19
20pub const MAX_DRIVES: usize = 8;
22
23pub const MAX_NICS: usize = 8;
25
26pub const MAX_PMEM: usize = 4;
28
29pub const MAX_VIRTIO_MEM: usize = 1;
31
32pub const DEFAULT_STRING_CAP: usize = 256;
34
35pub const PATH_MAX: usize = 1024;
37
38pub const UDS_PATH_MAX: usize = 103;
40
41pub const MAX_VCPU_COUNT: u32 = 32;
43
44fn is_valid_identifier(s: &str) -> bool {
46 !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
47}
48
49fn ensure_no_nul(value: &str, field: &str) -> Result<(), String> {
50 if value.contains('\0') {
51 return Err(format!("{field} must not contain NUL bytes"));
52 }
53 Ok(())
54}
55
56#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
59#[serde(transparent)]
60pub struct InstanceId(String);
61
62impl InstanceId {
63 pub fn new(id: impl Into<String>) -> Result<Self, String> {
65 let id = id.into();
66 if !is_valid_identifier(&id) {
67 return Err(format!(
68 "Invalid id: must match ^[A-Za-z0-9_]{{1,64}}$ (got {} bytes)",
69 id.len()
70 ));
71 }
72 Ok(Self(id))
73 }
74
75 #[must_use]
77 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80}
81
82impl AsRef<str> for InstanceId {
83 fn as_ref(&self) -> &str {
84 &self.0
85 }
86}
87
88impl<'de> Deserialize<'de> for InstanceId {
89 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
90 let s = String::deserialize(de)?;
91 Self::new(s).map_err(serde::de::Error::custom)
92 }
93}
94
95#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
97#[serde(transparent)]
98pub struct DriveId(String);
99
100impl DriveId {
101 pub fn new(id: impl Into<String>) -> Result<Self, String> {
103 let id = id.into();
104 if !is_valid_identifier(&id) {
105 return Err("Invalid drive_id: must match ^[A-Za-z0-9_]{1,64}$".to_string());
106 }
107 Ok(Self(id))
108 }
109
110 #[must_use]
112 pub fn as_str(&self) -> &str {
113 &self.0
114 }
115}
116
117impl<'de> Deserialize<'de> for DriveId {
118 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
119 let s = String::deserialize(de)?;
120 Self::new(s).map_err(serde::de::Error::custom)
121 }
122}
123
124#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
126#[serde(transparent)]
127pub struct IfaceId(String);
128
129impl IfaceId {
130 pub fn new(id: impl Into<String>) -> Result<Self, String> {
132 let id = id.into();
133 if !is_valid_identifier(&id) {
134 return Err("Invalid iface_id: must match ^[A-Za-z0-9_]{1,64}$".to_string());
135 }
136 Ok(Self(id))
137 }
138
139 #[must_use]
141 pub fn as_str(&self) -> &str {
142 &self.0
143 }
144}
145
146impl<'de> Deserialize<'de> for IfaceId {
147 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
148 let s = String::deserialize(de)?;
149 Self::new(s).map_err(serde::de::Error::custom)
150 }
151}
152
153#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
155#[serde(transparent)]
156pub struct VsockId(String);
157
158impl VsockId {
159 pub fn new(id: impl Into<String>) -> Result<Self, String> {
161 let id = id.into();
162 if !is_valid_identifier(&id) {
163 return Err("Invalid vsock_id: must match ^[A-Za-z0-9_]{1,64}$".to_string());
164 }
165 Ok(Self(id))
166 }
167
168 #[must_use]
170 pub fn as_str(&self) -> &str {
171 &self.0
172 }
173}
174
175impl<'de> Deserialize<'de> for VsockId {
176 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
177 let s = String::deserialize(de)?;
178 Self::new(s).map_err(serde::de::Error::custom)
179 }
180}
181
182#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
189#[serde(transparent)]
190pub struct SafePath(PathBuf);
191
192impl SafePath {
193 pub fn new(path: impl Into<PathBuf>) -> Result<Self, String> {
195 let path = path.into();
196 let s = path
197 .to_str()
198 .ok_or_else(|| "path is not valid UTF-8".to_string())?;
199 if s.is_empty() {
200 return Err("path must not be empty".into());
201 }
202 if s.len() > PATH_MAX {
203 return Err(format!(
204 "path exceeds {PATH_MAX} bytes (got {} bytes)",
205 s.len()
206 ));
207 }
208 ensure_no_nul(s, "path")?;
209 Ok(Self(path))
210 }
211
212 #[must_use]
214 pub fn as_path(&self) -> &std::path::Path {
215 &self.0
216 }
217}
218
219impl AsRef<std::path::Path> for SafePath {
220 fn as_ref(&self) -> &std::path::Path {
221 &self.0
222 }
223}
224
225impl From<SafePath> for PathBuf {
226 fn from(p: SafePath) -> Self {
227 p.0
228 }
229}
230
231impl<'de> Deserialize<'de> for SafePath {
232 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
233 let s = String::deserialize(de)?;
234 Self::new(s).map_err(serde::de::Error::custom)
235 }
236}
237
238#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
245#[serde(transparent)]
246pub struct UdsPath(PathBuf);
247
248impl UdsPath {
249 pub fn new(path: impl Into<PathBuf>) -> Result<Self, String> {
251 let path = path.into();
252 let s = path
253 .to_str()
254 .ok_or_else(|| "uds path is not valid UTF-8".to_string())?;
255 if s.is_empty() {
256 return Err("uds path must not be empty".into());
257 }
258 if s.len() > UDS_PATH_MAX {
259 return Err(format!(
260 "uds path exceeds {UDS_PATH_MAX} bytes on Darwin (got {} bytes)",
261 s.len()
262 ));
263 }
264 ensure_no_nul(s, "uds path")?;
265 Ok(Self(path))
266 }
267
268 #[must_use]
270 pub fn as_path(&self) -> &std::path::Path {
271 &self.0
272 }
273}
274
275impl<'de> Deserialize<'de> for UdsPath {
276 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
277 let s = String::deserialize(de)?;
278 Self::new(s).map_err(serde::de::Error::custom)
279 }
280}
281
282#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
286#[serde(transparent)]
287pub struct MemSizeMib(u64);
288
289impl MemSizeMib {
290 pub fn new(value: u64) -> Result<Self, String> {
292 if value == 0 {
293 return Err("mem_size_mib must be >= 1".into());
294 }
295 Ok(Self(value))
296 }
297
298 #[must_use]
300 pub const fn get(self) -> u64 {
301 self.0
302 }
303}
304
305impl<'de> Deserialize<'de> for MemSizeMib {
306 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
307 let v = u64::deserialize(de)?;
308 Self::new(v).map_err(serde::de::Error::custom)
309 }
310}
311
312#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize)]
315#[serde(transparent)]
316pub struct MacAddr {
317 #[serde(with = "mac_str")]
318 bytes: [u8; 6],
319}
320
321impl MacAddr {
322 #[must_use]
324 pub const fn from_bytes(bytes: [u8; 6]) -> Self {
325 Self { bytes }
326 }
327
328 #[must_use]
332 pub const fn bytes(&self) -> [u8; 6] {
333 self.bytes
334 }
335
336 #[must_use]
338 pub fn to_canonical_string(self) -> String {
339 format!(
340 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
341 self.bytes[0],
342 self.bytes[1],
343 self.bytes[2],
344 self.bytes[3],
345 self.bytes[4],
346 self.bytes[5]
347 )
348 }
349
350 pub fn parse(s: &str) -> Result<Self, String> {
352 let mut bytes = [0u8; 6];
353 let mut count = 0usize;
354 for (i, part) in s.split(':').enumerate() {
355 if i >= 6 {
356 return Err(format!(
357 "Invalid MAC: expected 6 octets (saw at least {})",
358 i + 1
359 ));
360 }
361 if part.len() != 2 {
362 return Err(format!(
363 "Invalid MAC: octet {i} must be exactly 2 hex digits"
364 ));
365 }
366 let octet = u8::from_str_radix(part, 16)
367 .map_err(|_| format!("Invalid MAC: octet {i} ({part}) is not valid hexadecimal"))?;
368 if let Some(slot) = bytes.get_mut(i) {
371 *slot = octet;
372 }
373 count = i + 1;
374 }
375 if count != 6 {
376 return Err(format!(
377 "Invalid MAC: expected 6 colon-separated octets (got {count})"
378 ));
379 }
380 Ok(Self { bytes })
381 }
382}
383
384impl<'de> Deserialize<'de> for MacAddr {
385 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
386 let s = String::deserialize(de)?;
387 Self::parse(&s).map_err(serde::de::Error::custom)
388 }
389}
390
391mod mac_str {
392 use serde::Serializer;
393
394 #[allow(clippy::trivially_copy_pass_by_ref)] pub(super) fn serialize<S: Serializer>(bytes: &[u8; 6], s: S) -> Result<S::Ok, S::Error> {
396 let formatted = format!(
397 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
398 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
399 );
400 s.serialize_str(&formatted)
401 }
402}
403
404pub fn check_string_cap(value: &str, field: &str, max: usize) -> Result<(), String> {
407 if value.len() > max {
408 return Err(format!(
409 "{field} exceeds {max} bytes (got {} bytes)",
410 value.len()
411 ));
412 }
413 ensure_no_nul(value, field)?;
414 Ok(())
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_should_accept_valid_drive_id() {
423 let id = DriveId::new("rootfs").unwrap();
424 assert_eq!(id.as_str(), "rootfs");
425 }
426
427 #[test]
428 fn test_should_reject_drive_id_with_dash() {
429 let err = DriveId::new("root-fs").unwrap_err();
430 assert!(err.contains("Invalid drive_id"));
431 }
432
433 #[test]
434 fn test_should_reject_empty_drive_id() {
435 assert!(DriveId::new("").is_err());
436 }
437
438 #[test]
439 fn test_should_reject_drive_id_over_64_bytes() {
440 let id = "a".repeat(65);
441 assert!(DriveId::new(id).is_err());
442 }
443
444 #[test]
445 fn test_should_reject_drive_id_with_nul_byte() {
446 assert!(DriveId::new("root\0fs").is_err());
447 }
448
449 #[test]
450 fn test_should_accept_path_under_path_max() {
451 let p = SafePath::new("/tmp/kernel.bin").unwrap();
452 assert_eq!(p.as_path().as_os_str(), "/tmp/kernel.bin");
453 }
454
455 #[test]
456 fn test_should_reject_path_over_path_max() {
457 let s = format!("/tmp/{}", "a".repeat(PATH_MAX));
458 assert!(SafePath::new(s).is_err());
459 }
460
461 #[test]
462 fn test_should_reject_path_with_nul_byte() {
463 assert!(SafePath::new("/tmp/k\0ernel").is_err());
464 }
465
466 #[test]
467 fn test_should_reject_uds_path_over_103_bytes() {
468 let s = format!("/tmp/{}", "a".repeat(UDS_PATH_MAX));
469 assert!(UdsPath::new(s).is_err());
470 }
471
472 #[test]
473 fn test_should_accept_uds_path_under_cap() {
474 let p = UdsPath::new("/tmp/squib.sock").unwrap();
475 assert_eq!(p.as_path().as_os_str(), "/tmp/squib.sock");
476 }
477
478 #[test]
479 fn test_should_round_trip_mac_through_canonical_string() {
480 let m = MacAddr::parse("AA:bb:CC:dd:EE:ff").unwrap();
481 assert_eq!(m.to_canonical_string(), "aa:bb:cc:dd:ee:ff");
482 }
483
484 #[test]
485 fn test_should_reject_mac_with_invalid_octet_count() {
486 assert!(MacAddr::parse("aa:bb:cc:dd:ee").is_err());
487 }
488
489 #[test]
490 fn test_should_reject_mac_with_non_hex_octet() {
491 assert!(MacAddr::parse("zz:bb:cc:dd:ee:ff").is_err());
492 }
493
494 #[test]
495 fn test_should_reject_zero_mem_size_mib() {
496 assert!(MemSizeMib::new(0).is_err());
497 }
498
499 #[test]
500 fn test_should_accept_one_mib_and_above() {
501 assert_eq!(MemSizeMib::new(256).unwrap().get(), 256);
502 }
503}