1use std::env;
17use std::ffi::OsString;
18use std::net::{Ipv4Addr, Ipv6Addr};
19use std::path::PathBuf;
20
21use microsandbox_protocol::{
22 ENV_BLOCK_ROOT, ENV_DIR_MOUNTS, ENV_DISK_MOUNTS, ENV_FILE_MOUNTS, ENV_HANDOFF_INIT,
23 ENV_HANDOFF_INIT_ARGS, ENV_HANDOFF_INIT_ENV, ENV_HOST_ALIAS, ENV_HOSTNAME, ENV_NET,
24 ENV_NET_IPV4, ENV_NET_IPV6, ENV_RLIMITS, ENV_TMPFS, ENV_USER, HANDOFF_INIT_AUTO,
25 HANDOFF_INIT_SEP, exec::ExecRlimit,
26};
27
28use crate::error::{AgentdError, AgentdResult};
29use crate::rlimit;
30
31#[derive(Debug)]
43pub struct BootParams {
44 pub(crate) block_root: Option<BlockRootSpec>,
46
47 pub(crate) dir_mounts: Vec<DirMountSpec>,
49
50 pub(crate) file_mounts: Vec<FileMountSpec>,
52
53 pub(crate) disk_mounts: Vec<DiskMountSpec>,
55
56 pub(crate) tmpfs: Vec<TmpfsSpec>,
58
59 pub(crate) hostname: Option<String>,
61
62 pub(crate) host_alias: Option<String>,
66
67 pub(crate) net: Option<NetSpec>,
69
70 pub(crate) net_ipv4: Option<NetIpv4Spec>,
72
73 pub(crate) net_ipv6: Option<NetIpv6Spec>,
75
76 pub(crate) rlimits: Vec<ExecRlimit>,
79
80 pub(crate) handoff_init: Option<HandoffInit>,
84}
85
86#[derive(Debug)]
92pub struct HandoffInit {
93 pub(crate) cmd: PathBuf,
96
97 pub(crate) argv: Vec<OsString>,
100
101 pub(crate) env: Vec<(OsString, OsString)>,
104}
105
106#[derive(Debug)]
111pub struct AgentdConfig {
112 pub(crate) user: Option<String>,
116}
117
118#[derive(Debug)]
120pub(crate) struct TmpfsSpec {
121 pub path: String,
122 pub size_mib: Option<u32>,
123 pub mode: Option<u32>,
124 pub noexec: bool,
125 pub readonly: bool,
126}
127
128#[derive(Debug)]
130pub(crate) enum BlockRootSpec {
131 DiskImage {
133 device: String,
134 fstype: Option<String>,
135 },
136 OciErofs {
138 lower: String,
139 upper: String,
140 upper_fstype: String,
141 },
142}
143
144#[derive(Debug)]
146pub(crate) struct DirMountSpec {
147 pub tag: String,
148 pub guest_path: String,
149 pub readonly: bool,
150}
151
152#[derive(Debug)]
154pub(crate) struct FileMountSpec {
155 pub tag: String,
156 pub filename: String,
157 pub guest_path: String,
158 pub readonly: bool,
159}
160
161#[derive(Debug)]
167pub(crate) struct DiskMountSpec {
168 pub id: String,
169 pub guest_path: String,
170 pub fstype: Option<String>,
173 pub readonly: bool,
174}
175
176#[derive(Debug)]
178pub(crate) struct NetSpec {
179 pub iface: String,
180 pub mac: [u8; 6],
181 pub mtu: u16,
182}
183
184#[derive(Debug)]
186pub(crate) struct NetIpv4Spec {
187 pub address: Ipv4Addr,
188 pub prefix_len: u8,
189 pub gateway: Ipv4Addr,
190 pub dns: Option<Ipv4Addr>,
191}
192
193#[derive(Debug)]
195pub(crate) struct NetIpv6Spec {
196 pub address: Ipv6Addr,
197 pub prefix_len: u8,
198 pub gateway: Ipv6Addr,
199 pub dns: Option<Ipv6Addr>,
200}
201
202#[derive(Debug)]
206pub(crate) struct NetConfig<'a> {
207 pub net: Option<&'a NetSpec>,
208 pub ipv4: Option<&'a NetIpv4Spec>,
209 pub ipv6: Option<&'a NetIpv6Spec>,
210}
211
212impl BootParams {
217 pub fn from_env() -> AgentdResult<Self> {
222 Ok(Self {
223 block_root: read_env(ENV_BLOCK_ROOT)
224 .map(|v| parse_block_root(&v))
225 .transpose()?,
226 dir_mounts: read_env(ENV_DIR_MOUNTS)
227 .map(|v| parse_dir_mounts(&v))
228 .transpose()?
229 .unwrap_or_default(),
230 file_mounts: read_env(ENV_FILE_MOUNTS)
231 .map(|v| parse_file_mounts(&v))
232 .transpose()?
233 .unwrap_or_default(),
234 disk_mounts: read_env(ENV_DISK_MOUNTS)
235 .map(|v| parse_disk_mounts(&v))
236 .transpose()?
237 .unwrap_or_default(),
238 tmpfs: read_env(ENV_TMPFS)
239 .map(|v| parse_tmpfs_mounts(&v))
240 .transpose()?
241 .unwrap_or_default(),
242 hostname: read_env(ENV_HOSTNAME),
243 host_alias: read_env(ENV_HOST_ALIAS),
244 net: read_env(ENV_NET).map(|v| parse_net(&v)).transpose()?,
245 net_ipv4: read_env(ENV_NET_IPV4)
246 .map(|v| parse_net_ipv4(&v))
247 .transpose()?,
248 net_ipv6: read_env(ENV_NET_IPV6)
249 .map(|v| parse_net_ipv6(&v))
250 .transpose()?,
251 rlimits: read_env(ENV_RLIMITS)
252 .map(|v| parse_rlimits(&v))
253 .transpose()?
254 .unwrap_or_default(),
255 handoff_init: parse_handoff_init()?,
256 })
257 }
258
259 pub fn take_handoff_init(&mut self) -> Option<HandoffInit> {
264 self.handoff_init.take()
265 }
266
267 pub(crate) fn network(&self) -> NetConfig<'_> {
269 NetConfig {
270 net: self.net.as_ref(),
271 ipv4: self.net_ipv4.as_ref(),
272 ipv6: self.net_ipv6.as_ref(),
273 }
274 }
275}
276
277impl AgentdConfig {
278 pub fn from_env() -> AgentdResult<Self> {
282 Ok(Self {
283 user: read_env(ENV_USER),
284 })
285 }
286}
287
288fn parse_block_root(val: &str) -> AgentdResult<BlockRootSpec> {
298 let mut kv: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
299 for part in val.split(',') {
300 let Some((k, v)) = part.split_once('=') else {
301 continue;
302 };
303 if kv.insert(k, v).is_some() {
304 return Err(AgentdError::Config(format!(
305 "MSB_BLOCK_ROOT duplicate key '{k}'"
306 )));
307 }
308 }
309
310 let get = |key: &str| -> AgentdResult<String> {
311 kv.get(key)
312 .filter(|v| !v.is_empty())
313 .map(|v| v.to_string())
314 .ok_or_else(|| AgentdError::Config(format!("MSB_BLOCK_ROOT missing '{key}'")))
315 };
316
317 match kv.get("kind").copied() {
318 Some("disk-image") => {
319 let device = get("device")?;
320 let fstype = kv
321 .get("fstype")
322 .filter(|v| !v.is_empty())
323 .map(|v| v.to_string());
324 Ok(BlockRootSpec::DiskImage { device, fstype })
325 }
326 Some("oci-erofs") => {
327 let lower = get("lower")?;
328 let upper = get("upper")?;
329 let upper_fstype = get("upper_fstype")?;
330 Ok(BlockRootSpec::OciErofs {
331 lower,
332 upper,
333 upper_fstype,
334 })
335 }
336 Some(other) => Err(AgentdError::Config(format!(
337 "MSB_BLOCK_ROOT unknown kind: {other}"
338 ))),
339 None => Err(AgentdError::Config(
340 "MSB_BLOCK_ROOT missing 'kind' key".into(),
341 )),
342 }
343}
344
345fn parse_dir_mounts(val: &str) -> AgentdResult<Vec<DirMountSpec>> {
347 val.split(';')
348 .filter(|e| !e.is_empty())
349 .map(parse_dir_mount_entry)
350 .collect()
351}
352
353fn parse_dir_mount_entry(entry: &str) -> AgentdResult<DirMountSpec> {
355 let parts: Vec<&str> = entry.split(':').collect();
356 if parts.len() < 2 {
357 return Err(AgentdError::Config(format!(
358 "MSB_DIR_MOUNTS entry must be tag:path[:ro], got: {entry}"
359 )));
360 }
361
362 let tag = parts[0];
363 let guest_path = parts[1];
364 let readonly = match parts.get(2) {
365 Some(&"ro") => true,
366 None => false,
367 Some(flag) => {
368 return Err(AgentdError::Config(format!(
369 "MSB_DIR_MOUNTS unknown flag '{flag}' (expected 'ro')"
370 )));
371 }
372 };
373
374 if parts.len() > 3 {
375 return Err(AgentdError::Config(format!(
376 "MSB_DIR_MOUNTS entry has too many parts: {entry}"
377 )));
378 }
379
380 if tag.is_empty() {
381 return Err(AgentdError::Config(
382 "MSB_DIR_MOUNTS entry has empty tag".into(),
383 ));
384 }
385 if guest_path.is_empty() || !guest_path.starts_with('/') {
386 return Err(AgentdError::Config(format!(
387 "MSB_DIR_MOUNTS guest path must be absolute: {guest_path}"
388 )));
389 }
390
391 Ok(DirMountSpec {
392 tag: tag.to_string(),
393 guest_path: guest_path.to_string(),
394 readonly,
395 })
396}
397
398fn parse_file_mounts(val: &str) -> AgentdResult<Vec<FileMountSpec>> {
400 val.split(';')
401 .filter(|e| !e.is_empty())
402 .map(parse_file_mount_entry)
403 .collect()
404}
405
406fn parse_file_mount_entry(entry: &str) -> AgentdResult<FileMountSpec> {
408 let parts: Vec<&str> = entry.split(':').collect();
409 if parts.len() < 3 {
410 return Err(AgentdError::Config(format!(
411 "MSB_FILE_MOUNTS entry must be tag:filename:path[:ro], got: {entry}"
412 )));
413 }
414
415 let tag = parts[0];
416 let filename = parts[1];
417 let guest_path = parts[2];
418 let readonly = match parts.get(3) {
419 Some(&"ro") => true,
420 None => false,
421 Some(flag) => {
422 return Err(AgentdError::Config(format!(
423 "MSB_FILE_MOUNTS unknown flag '{flag}' (expected 'ro')"
424 )));
425 }
426 };
427
428 if parts.len() > 4 {
429 return Err(AgentdError::Config(format!(
430 "MSB_FILE_MOUNTS entry has too many parts: {entry}"
431 )));
432 }
433
434 if tag.is_empty() {
435 return Err(AgentdError::Config(
436 "MSB_FILE_MOUNTS entry has empty tag".into(),
437 ));
438 }
439 if filename.is_empty() {
440 return Err(AgentdError::Config(
441 "MSB_FILE_MOUNTS entry has empty filename".into(),
442 ));
443 }
444 if guest_path.is_empty() || !guest_path.starts_with('/') {
445 return Err(AgentdError::Config(format!(
446 "MSB_FILE_MOUNTS guest path must be absolute: {guest_path}"
447 )));
448 }
449
450 Ok(FileMountSpec {
451 tag: tag.to_string(),
452 filename: filename.to_string(),
453 guest_path: guest_path.to_string(),
454 readonly,
455 })
456}
457
458fn parse_disk_mounts(val: &str) -> AgentdResult<Vec<DiskMountSpec>> {
460 val.split(';')
461 .filter(|e| !e.is_empty())
462 .map(parse_disk_mount_entry)
463 .collect()
464}
465
466fn parse_disk_mount_entry(entry: &str) -> AgentdResult<DiskMountSpec> {
471 let parts: Vec<&str> = entry.split(':').collect();
472 if parts.len() < 2 {
473 return Err(AgentdError::Config(format!(
474 "MSB_DISK_MOUNTS entry must be id:guest_path[:fstype][:ro], got: {entry}"
475 )));
476 }
477
478 let id = parts[0];
479 let guest_path = parts[1];
480 let mut fstype: Option<String> = None;
481 let mut readonly = false;
482
483 if let Some(third) = parts.get(2)
484 && !third.is_empty()
485 {
486 fstype = Some((*third).to_string());
487 }
488 if let Some(fourth) = parts.get(3) {
489 match *fourth {
490 "ro" => readonly = true,
491 other => {
492 return Err(AgentdError::Config(format!(
493 "MSB_DISK_MOUNTS unknown flag '{other}' (expected 'ro')"
494 )));
495 }
496 }
497 }
498 if parts.len() > 4 {
499 return Err(AgentdError::Config(format!(
500 "MSB_DISK_MOUNTS entry has too many parts: {entry}"
501 )));
502 }
503
504 if id.is_empty() {
505 return Err(AgentdError::Config(
506 "MSB_DISK_MOUNTS entry has empty id".into(),
507 ));
508 }
509 if guest_path.is_empty() || !guest_path.starts_with('/') {
510 return Err(AgentdError::Config(format!(
511 "MSB_DISK_MOUNTS guest path must be absolute: {guest_path}"
512 )));
513 }
514
515 Ok(DiskMountSpec {
516 id: id.to_string(),
517 guest_path: guest_path.to_string(),
518 fstype,
519 readonly,
520 })
521}
522
523fn parse_tmpfs_mounts(val: &str) -> AgentdResult<Vec<TmpfsSpec>> {
525 val.split(';')
526 .filter(|e| !e.is_empty())
527 .map(parse_tmpfs_entry)
528 .collect()
529}
530
531fn parse_tmpfs_entry(entry: &str) -> AgentdResult<TmpfsSpec> {
535 let mut parts = entry.split(',');
536 let path = parts.next().unwrap(); if path.is_empty() {
538 return Err(AgentdError::Config("tmpfs entry has empty path".into()));
539 }
540
541 let mut size_mib = None;
542 let mut mode = None;
543 let mut noexec = false;
544 let mut readonly = false;
545
546 for opt in parts {
547 if opt == "noexec" {
548 noexec = true;
549 } else if opt == "ro" {
550 readonly = true;
551 } else if let Some(val) = opt.strip_prefix("size=") {
552 size_mib = Some(
553 val.parse::<u32>()
554 .map_err(|_| AgentdError::Config(format!("invalid tmpfs size: {val}")))?,
555 );
556 } else if let Some(val) = opt.strip_prefix("mode=") {
557 mode =
558 Some(u32::from_str_radix(val, 8).map_err(|_| {
559 AgentdError::Config(format!("invalid octal tmpfs mode: {val}"))
560 })?);
561 } else {
562 return Err(AgentdError::Config(format!("unknown tmpfs option: {opt}")));
563 }
564 }
565
566 Ok(TmpfsSpec {
567 path: path.to_string(),
568 size_mib,
569 mode,
570 noexec,
571 readonly,
572 })
573}
574
575fn parse_rlimits(val: &str) -> AgentdResult<Vec<ExecRlimit>> {
585 let mut seen: Vec<String> = Vec::new();
586 val.split(';')
587 .filter(|entry| !entry.is_empty())
588 .map(|entry| {
589 let rlimit = entry.parse::<ExecRlimit>().map_err(|err| {
590 AgentdError::Config(format!("{ENV_RLIMITS} entry {entry}: {err}"))
591 })?;
592 if rlimit::parse_rlimit_resource(&rlimit.resource).is_none() {
593 return Err(AgentdError::Config(format!(
594 "{ENV_RLIMITS} unknown resource: {}",
595 rlimit.resource
596 )));
597 }
598 if seen.iter().any(|name| name == &rlimit.resource) {
599 return Err(AgentdError::Config(format!(
600 "{ENV_RLIMITS} duplicate resource: {}",
601 rlimit.resource
602 )));
603 }
604 seen.push(rlimit.resource.clone());
605 Ok(rlimit)
606 })
607 .collect()
608}
609
610fn parse_net(val: &str) -> AgentdResult<NetSpec> {
616 let mut iface = None;
617 let mut mac = None;
618 let mut mtu = 1500u16;
619
620 for part in val.split(',') {
621 if let Some(v) = part.strip_prefix("iface=") {
622 iface = Some(v.to_string());
623 } else if let Some(v) = part.strip_prefix("mac=") {
624 mac = Some(parse_mac(v)?);
625 } else if let Some(v) = part.strip_prefix("mtu=") {
626 mtu = v
627 .parse()
628 .map_err(|_| AgentdError::Config(format!("invalid MTU: {v}")))?;
629 } else {
630 return Err(AgentdError::Config(format!(
631 "unknown MSB_NET option: {part}"
632 )));
633 }
634 }
635
636 let iface = iface.ok_or_else(|| AgentdError::Config("MSB_NET missing iface=".into()))?;
637 let mac = mac.ok_or_else(|| AgentdError::Config("MSB_NET missing mac=".into()))?;
638
639 Ok(NetSpec { iface, mac, mtu })
640}
641
642fn parse_net_ipv4(val: &str) -> AgentdResult<NetIpv4Spec> {
644 let mut address = None;
645 let mut prefix_len = None;
646 let mut gateway = None;
647 let mut dns = None;
648
649 for part in val.split(',') {
650 if let Some(v) = part.strip_prefix("addr=") {
651 let (addr, prefix) = parse_cidr_v4(v)?;
652 address = Some(addr);
653 prefix_len = Some(prefix);
654 } else if let Some(v) = part.strip_prefix("gw=") {
655 gateway = Some(
656 v.parse::<Ipv4Addr>()
657 .map_err(|_| AgentdError::Config(format!("invalid IPv4 gateway: {v}")))?,
658 );
659 } else if let Some(v) = part.strip_prefix("dns=") {
660 dns = Some(
661 v.parse::<Ipv4Addr>()
662 .map_err(|_| AgentdError::Config(format!("invalid IPv4 DNS: {v}")))?,
663 );
664 } else {
665 return Err(AgentdError::Config(format!(
666 "unknown MSB_NET_IPV4 option: {part}"
667 )));
668 }
669 }
670
671 let address =
672 address.ok_or_else(|| AgentdError::Config("MSB_NET_IPV4 missing addr=".into()))?;
673 let prefix_len =
674 prefix_len.ok_or_else(|| AgentdError::Config("MSB_NET_IPV4 missing addr=".into()))?;
675 let gateway = gateway.ok_or_else(|| AgentdError::Config("MSB_NET_IPV4 missing gw=".into()))?;
676
677 Ok(NetIpv4Spec {
678 address,
679 prefix_len,
680 gateway,
681 dns,
682 })
683}
684
685fn parse_net_ipv6(val: &str) -> AgentdResult<NetIpv6Spec> {
687 let mut address = None;
688 let mut prefix_len = None;
689 let mut gateway = None;
690 let mut dns = None;
691
692 for part in val.split(',') {
693 if let Some(v) = part.strip_prefix("addr=") {
694 let (addr, prefix) = parse_cidr_v6(v)?;
695 address = Some(addr);
696 prefix_len = Some(prefix);
697 } else if let Some(v) = part.strip_prefix("gw=") {
698 gateway = Some(
699 v.parse::<Ipv6Addr>()
700 .map_err(|_| AgentdError::Config(format!("invalid IPv6 gateway: {v}")))?,
701 );
702 } else if let Some(v) = part.strip_prefix("dns=") {
703 dns = Some(
704 v.parse::<Ipv6Addr>()
705 .map_err(|_| AgentdError::Config(format!("invalid IPv6 DNS: {v}")))?,
706 );
707 } else {
708 return Err(AgentdError::Config(format!(
709 "unknown MSB_NET_IPV6 option: {part}"
710 )));
711 }
712 }
713
714 let address =
715 address.ok_or_else(|| AgentdError::Config("MSB_NET_IPV6 missing addr=".into()))?;
716 let prefix_len =
717 prefix_len.ok_or_else(|| AgentdError::Config("MSB_NET_IPV6 missing addr=".into()))?;
718 let gateway = gateway.ok_or_else(|| AgentdError::Config("MSB_NET_IPV6 missing gw=".into()))?;
719
720 Ok(NetIpv6Spec {
721 address,
722 prefix_len,
723 gateway,
724 dns,
725 })
726}
727
728fn parse_mac(s: &str) -> AgentdResult<[u8; 6]> {
730 let mut mac = [0u8; 6];
731 let mut len = 0usize;
732 for (i, part) in s.split(':').enumerate() {
733 if i >= 6 {
734 return Err(AgentdError::Config(format!("invalid MAC address: {s}")));
735 }
736 mac[i] = u8::from_str_radix(part, 16)
737 .map_err(|_| AgentdError::Config(format!("invalid MAC octet: {part}")))?;
738 len = i + 1;
739 }
740 if len != 6 {
741 return Err(AgentdError::Config(format!("invalid MAC address: {s}")));
742 }
743 Ok(mac)
744}
745
746fn parse_cidr_v4(s: &str) -> AgentdResult<(Ipv4Addr, u8)> {
748 let (addr_str, prefix_str) = s
749 .split_once('/')
750 .ok_or_else(|| AgentdError::Config(format!("invalid IPv4 CIDR (missing /): {s}")))?;
751 let addr = addr_str
752 .parse::<Ipv4Addr>()
753 .map_err(|_| AgentdError::Config(format!("invalid IPv4 address: {addr_str}")))?;
754 let prefix = prefix_str
755 .parse::<u8>()
756 .map_err(|_| AgentdError::Config(format!("invalid IPv4 prefix length: {prefix_str}")))?;
757 if prefix > 32 {
758 return Err(AgentdError::Config(format!(
759 "IPv4 prefix length out of range (0-32): {prefix}"
760 )));
761 }
762 Ok((addr, prefix))
763}
764
765fn parse_cidr_v6(s: &str) -> AgentdResult<(Ipv6Addr, u8)> {
767 let (addr_str, prefix_str) = s
768 .rsplit_once('/')
769 .ok_or_else(|| AgentdError::Config(format!("invalid IPv6 CIDR (missing /): {s}")))?;
770 let addr = addr_str
771 .parse::<Ipv6Addr>()
772 .map_err(|_| AgentdError::Config(format!("invalid IPv6 address: {addr_str}")))?;
773 let prefix = prefix_str
774 .parse::<u8>()
775 .map_err(|_| AgentdError::Config(format!("invalid IPv6 prefix length: {prefix_str}")))?;
776 if prefix > 128 {
777 return Err(AgentdError::Config(format!(
778 "IPv6 prefix length out of range (0-128): {prefix}"
779 )));
780 }
781 Ok((addr, prefix))
782}
783
784fn parse_handoff_init() -> AgentdResult<Option<HandoffInit>> {
795 let Some(cmd_str) = read_env_raw(ENV_HANDOFF_INIT) else {
796 return Ok(None);
797 };
798 if cmd_str.trim().is_empty() {
799 return Ok(None);
800 }
801
802 let cmd = PathBuf::from(&cmd_str);
803 if cmd_str != HANDOFF_INIT_AUTO && !cmd.is_absolute() {
807 return Err(AgentdError::Config(format!(
808 "{ENV_HANDOFF_INIT} must be an absolute path or `auto`, got: {cmd_str}"
809 )));
810 }
811
812 let argv = match read_env_raw(ENV_HANDOFF_INIT_ARGS) {
813 Some(val) if !val.is_empty() => val.split(HANDOFF_INIT_SEP).map(OsString::from).collect(),
814 _ => Vec::new(),
815 };
816
817 let env = match read_env_raw(ENV_HANDOFF_INIT_ENV) {
818 Some(val) if !val.is_empty() => val
819 .split(HANDOFF_INIT_SEP)
820 .map(|entry| {
821 let (k, v) = entry.split_once('=').ok_or_else(|| {
822 AgentdError::Config(format!(
823 "{ENV_HANDOFF_INIT_ENV} entry missing '=': {entry}"
824 ))
825 })?;
826 if k.is_empty() {
827 return Err(AgentdError::Config(format!(
828 "{ENV_HANDOFF_INIT_ENV} entry has empty key: {entry}"
829 )));
830 }
831 Ok((OsString::from(k), OsString::from(v)))
832 })
833 .collect::<AgentdResult<Vec<_>>>()?,
834 _ => Vec::new(),
835 };
836
837 Ok(Some(HandoffInit { cmd, argv, env }))
838}
839
840fn read_env(key: &str) -> Option<String> {
846 env::var(key)
847 .ok()
848 .map(|v| v.trim().to_string())
849 .filter(|v| !v.is_empty())
850}
851
852fn read_env_raw(key: &str) -> Option<String> {
857 env::var(key).ok().filter(|v| !v.is_empty())
858}
859
860#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
871 fn test_parse_block_root_disk_image() {
872 let spec = parse_block_root("kind=disk-image,device=/dev/vda,fstype=ext4").unwrap();
873 let BlockRootSpec::DiskImage { device, fstype } = spec else {
874 panic!("expected DiskImage");
875 };
876 assert_eq!(device, "/dev/vda");
877 assert_eq!(fstype.as_deref(), Some("ext4"));
878 }
879
880 #[test]
881 fn test_parse_block_root_disk_image_no_fstype() {
882 let spec = parse_block_root("kind=disk-image,device=/dev/vda").unwrap();
883 let BlockRootSpec::DiskImage { device, fstype } = spec else {
884 panic!("expected DiskImage");
885 };
886 assert_eq!(device, "/dev/vda");
887 assert_eq!(fstype, None);
888 }
889
890 #[test]
891 fn test_parse_block_root_oci_erofs() {
892 let spec =
893 parse_block_root("kind=oci-erofs,lower=/dev/vda,upper=/dev/vdb,upper_fstype=ext4")
894 .unwrap();
895 let BlockRootSpec::OciErofs {
896 lower,
897 upper,
898 upper_fstype,
899 } = spec
900 else {
901 panic!("expected OciErofs");
902 };
903 assert_eq!(lower, "/dev/vda");
904 assert_eq!(upper, "/dev/vdb");
905 assert_eq!(upper_fstype, "ext4");
906 }
907
908 #[test]
909 fn test_parse_block_root_unknown_kind_errors() {
910 let err = parse_block_root("kind=bogus,device=/dev/vda").unwrap_err();
911 assert!(err.to_string().contains("unknown kind"));
912 }
913
914 #[test]
915 fn test_parse_block_root_missing_kind_errors() {
916 let err = parse_block_root("/dev/vda").unwrap_err();
917 assert!(err.to_string().contains("missing 'kind' key"));
918 }
919
920 #[test]
921 fn test_parse_block_root_disk_image_missing_device_errors() {
922 let err = parse_block_root("kind=disk-image").unwrap_err();
923 assert!(err.to_string().contains("missing 'device'"));
924 }
925
926 #[test]
927 fn test_parse_block_root_oci_erofs_missing_upper_errors() {
928 let err = parse_block_root("kind=oci-erofs,lower=/dev/vda,upper_fstype=ext4").unwrap_err();
929 assert!(err.to_string().contains("missing 'upper'"));
930 }
931
932 #[test]
933 fn test_parse_block_root_duplicate_key_errors() {
934 let err = parse_block_root("kind=disk-image,device=/dev/vda,device=/dev/vdb").unwrap_err();
935 assert!(err.to_string().contains("duplicate key 'device'"));
936 }
937
938 #[test]
941 fn test_parse_file_mount_entry_basic() {
942 let spec = parse_file_mount_entry("fm_config:app.conf:/etc/app.conf").unwrap();
943 assert_eq!(spec.tag, "fm_config");
944 assert_eq!(spec.filename, "app.conf");
945 assert_eq!(spec.guest_path, "/etc/app.conf");
946 assert!(!spec.readonly);
947 }
948
949 #[test]
950 fn test_parse_file_mount_entry_readonly() {
951 let spec = parse_file_mount_entry("fm_config:app.conf:/etc/app.conf:ro").unwrap();
952 assert!(spec.readonly);
953 }
954
955 #[test]
956 fn test_parse_file_mount_entry_too_few_parts() {
957 assert!(parse_file_mount_entry("fm_config:/etc/app.conf").is_err());
958 }
959
960 #[test]
961 fn test_parse_file_mount_entry_empty_filename() {
962 assert!(parse_file_mount_entry("fm_config::/etc/app.conf").is_err());
963 }
964
965 #[test]
966 fn test_parse_file_mount_entry_relative_path() {
967 assert!(parse_file_mount_entry("fm_config:app.conf:relative/path").is_err());
968 }
969
970 #[test]
971 fn test_parse_file_mount_entry_too_many_parts() {
972 assert!(parse_file_mount_entry("fm_config:app.conf:/etc/app.conf:ro:extra").is_err());
973 }
974
975 #[test]
976 fn test_parse_file_mount_entry_unknown_flag() {
977 assert!(parse_file_mount_entry("fm_config:app.conf:/etc/app.conf:rw").is_err());
978 }
979
980 #[test]
981 fn test_parse_file_mount_entry_empty_tag() {
982 assert!(parse_file_mount_entry(":app.conf:/etc/app.conf").is_err());
983 }
984
985 #[test]
988 fn test_parse_path_only() {
989 let spec = parse_tmpfs_entry("/tmp").unwrap();
990 assert_eq!(spec.path, "/tmp");
991 assert_eq!(spec.size_mib, None);
992 assert_eq!(spec.mode, None);
993 assert!(!spec.noexec);
994 }
995
996 #[test]
997 fn test_parse_with_size() {
998 let spec = parse_tmpfs_entry("/tmp,size=256").unwrap();
999 assert_eq!(spec.path, "/tmp");
1000 assert_eq!(spec.size_mib, Some(256));
1001 }
1002
1003 #[test]
1004 fn test_parse_with_noexec() {
1005 let spec = parse_tmpfs_entry("/tmp,noexec").unwrap();
1006 assert_eq!(spec.path, "/tmp");
1007 assert!(spec.noexec);
1008 }
1009
1010 #[test]
1013 fn test_parse_disk_mount_entry_basic() {
1014 let spec = parse_disk_mount_entry("data_abc:/data:ext4").unwrap();
1015 assert_eq!(spec.id, "data_abc");
1016 assert_eq!(spec.guest_path, "/data");
1017 assert_eq!(spec.fstype.as_deref(), Some("ext4"));
1018 assert!(!spec.readonly);
1019 }
1020
1021 #[test]
1022 fn test_parse_disk_mount_entry_readonly() {
1023 let spec = parse_disk_mount_entry("seed_7f:/seed:ext4:ro").unwrap();
1024 assert!(spec.readonly);
1025 assert_eq!(spec.fstype.as_deref(), Some("ext4"));
1026 }
1027
1028 #[test]
1029 fn test_parse_disk_mount_entry_empty_fstype_means_autodetect() {
1030 let spec = parse_disk_mount_entry("probe_1:/data::ro").unwrap();
1031 assert!(spec.fstype.is_none());
1032 assert!(spec.readonly);
1033 }
1034
1035 #[test]
1036 fn test_parse_disk_mount_entry_autodetect_no_ro() {
1037 let spec = parse_disk_mount_entry("probe_1:/data").unwrap();
1038 assert!(spec.fstype.is_none());
1039 assert!(!spec.readonly);
1040 }
1041
1042 #[test]
1043 fn test_parse_disk_mount_entry_rejects_unknown_flag() {
1044 let err = parse_disk_mount_entry("id:/data:ext4:rw").unwrap_err();
1045 assert!(err.to_string().contains("unknown flag"));
1046 }
1047
1048 #[test]
1049 fn test_parse_disk_mount_entry_rejects_relative_path() {
1050 assert!(parse_disk_mount_entry("id:relative").is_err());
1051 }
1052
1053 #[test]
1054 fn test_parse_disk_mount_entry_rejects_empty_id() {
1055 assert!(parse_disk_mount_entry(":/data:ext4").is_err());
1056 }
1057
1058 #[test]
1059 fn test_parse_disk_mount_entry_rejects_too_many_parts() {
1060 assert!(parse_disk_mount_entry("id:/data:ext4:ro:extra").is_err());
1061 }
1062
1063 #[test]
1064 fn test_parse_disk_mounts_multiple_entries() {
1065 let specs = parse_disk_mounts("data_1:/data:ext4;seed_2:/seed::ro;probe_3:/p").unwrap();
1066 assert_eq!(specs.len(), 3);
1067 assert_eq!(specs[0].guest_path, "/data");
1068 assert!(specs[1].readonly);
1069 assert!(specs[2].fstype.is_none());
1070 }
1071
1072 #[test]
1073 fn test_parse_with_ro() {
1074 let spec = parse_tmpfs_entry("/seed,size=64,ro").unwrap();
1075 assert_eq!(spec.path, "/seed");
1076 assert_eq!(spec.size_mib, Some(64));
1077 assert!(spec.readonly);
1078 assert!(!spec.noexec);
1079 }
1080
1081 #[test]
1082 fn test_parse_ro_defaults_to_false_when_absent() {
1083 let spec = parse_tmpfs_entry("/tmp,size=256").unwrap();
1084 assert!(!spec.readonly);
1085 }
1086
1087 #[test]
1088 fn test_parse_with_octal_mode() {
1089 let spec = parse_tmpfs_entry("/tmp,mode=1777").unwrap();
1090 assert_eq!(spec.mode, Some(0o1777));
1091
1092 let spec = parse_tmpfs_entry("/data,mode=755").unwrap();
1093 assert_eq!(spec.mode, Some(0o755));
1094 }
1095
1096 #[test]
1097 fn test_parse_multi_options() {
1098 let spec = parse_tmpfs_entry("/tmp,size=256,mode=1777,noexec").unwrap();
1099 assert_eq!(spec.path, "/tmp");
1100 assert_eq!(spec.size_mib, Some(256));
1101 assert_eq!(spec.mode, Some(0o1777));
1102 assert!(spec.noexec);
1103 }
1104
1105 #[test]
1106 fn test_parse_unknown_option_errors() {
1107 let err = parse_tmpfs_entry("/tmp,bogus=42").unwrap_err();
1108 assert!(err.to_string().contains("unknown tmpfs option"));
1109 }
1110
1111 #[test]
1112 fn test_parse_invalid_size_errors() {
1113 let err = parse_tmpfs_entry("/tmp,size=abc").unwrap_err();
1114 assert!(err.to_string().contains("invalid tmpfs size"));
1115 }
1116
1117 #[test]
1118 fn test_parse_invalid_mode_errors() {
1119 let err = parse_tmpfs_entry("/tmp,mode=zzz").unwrap_err();
1120 assert!(err.to_string().contains("invalid octal tmpfs mode"));
1121 }
1122
1123 #[test]
1124 fn test_parse_empty_path_errors() {
1125 let err = parse_tmpfs_entry(",size=256").unwrap_err();
1126 assert!(err.to_string().contains("empty path"));
1127 }
1128
1129 #[test]
1132 fn test_parse_net_full() {
1133 let spec = parse_net("iface=eth0,mac=02:5a:7b:13:01:02,mtu=1500").unwrap();
1134 assert_eq!(spec.iface, "eth0");
1135 assert_eq!(spec.mac, [0x02, 0x5a, 0x7b, 0x13, 0x01, 0x02]);
1136 assert_eq!(spec.mtu, 1500);
1137 }
1138
1139 #[test]
1140 fn test_parse_net_default_mtu() {
1141 let spec = parse_net("iface=eth0,mac=02:00:00:00:00:01").unwrap();
1142 assert_eq!(spec.mtu, 1500);
1143 }
1144
1145 #[test]
1146 fn test_parse_net_missing_iface() {
1147 assert!(parse_net("mac=02:00:00:00:00:01").is_err());
1148 }
1149
1150 #[test]
1151 fn test_parse_net_missing_mac() {
1152 assert!(parse_net("iface=eth0").is_err());
1153 }
1154
1155 #[test]
1156 fn test_parse_net_unknown_option() {
1157 assert!(parse_net("iface=eth0,mac=02:00:00:00:00:01,bogus=42").is_err());
1158 }
1159
1160 #[test]
1161 fn test_parse_net_ipv4() {
1162 let spec = parse_net_ipv4("addr=100.96.1.2/30,gw=100.96.1.1,dns=100.96.1.1").unwrap();
1163 assert_eq!(spec.address, Ipv4Addr::new(100, 96, 1, 2));
1164 assert_eq!(spec.prefix_len, 30);
1165 assert_eq!(spec.gateway, Ipv4Addr::new(100, 96, 1, 1));
1166 assert_eq!(spec.dns, Some(Ipv4Addr::new(100, 96, 1, 1)));
1167 }
1168
1169 #[test]
1170 fn test_parse_net_ipv4_no_dns() {
1171 let spec = parse_net_ipv4("addr=10.0.0.2/24,gw=10.0.0.1").unwrap();
1172 assert_eq!(spec.dns, None);
1173 }
1174
1175 #[test]
1176 fn test_parse_net_ipv4_missing_addr() {
1177 assert!(parse_net_ipv4("gw=10.0.0.1").is_err());
1178 }
1179
1180 #[test]
1181 fn test_parse_net_ipv6() {
1182 let spec = parse_net_ipv6(
1183 "addr=fd42:6d73:62:2a::2/64,gw=fd42:6d73:62:2a::1,dns=fd42:6d73:62:2a::1",
1184 )
1185 .unwrap();
1186 assert_eq!(
1187 spec.address,
1188 "fd42:6d73:62:2a::2".parse::<Ipv6Addr>().unwrap()
1189 );
1190 assert_eq!(spec.prefix_len, 64);
1191 assert_eq!(
1192 spec.gateway,
1193 "fd42:6d73:62:2a::1".parse::<Ipv6Addr>().unwrap()
1194 );
1195 assert!(spec.dns.is_some());
1196 }
1197
1198 #[test]
1199 fn test_parse_mac_valid() {
1200 let mac = parse_mac("02:5a:7b:13:01:02").unwrap();
1201 assert_eq!(mac, [0x02, 0x5a, 0x7b, 0x13, 0x01, 0x02]);
1202 }
1203
1204 #[test]
1205 fn test_parse_mac_invalid() {
1206 assert!(parse_mac("02:5a:7b").is_err());
1207 assert!(parse_mac("zz:00:00:00:00:00").is_err());
1208 }
1209
1210 #[test]
1211 fn test_parse_cidr_v4() {
1212 let (addr, prefix) = parse_cidr_v4("100.96.1.2/30").unwrap();
1213 assert_eq!(addr, Ipv4Addr::new(100, 96, 1, 2));
1214 assert_eq!(prefix, 30);
1215 }
1216
1217 #[test]
1218 fn test_parse_cidr_v6() {
1219 let (addr, prefix) = parse_cidr_v6("fd42:6d73:62:2a::2/64").unwrap();
1220 assert_eq!(addr, "fd42:6d73:62:2a::2".parse::<Ipv6Addr>().unwrap());
1221 assert_eq!(prefix, 64);
1222 }
1223
1224 #[test]
1227 fn test_parse_rlimits_happy_path() {
1228 let rlimits = parse_rlimits("nofile=65535;nproc=4096:8192").unwrap();
1229 assert_eq!(rlimits.len(), 2);
1230 assert_eq!(rlimits[0].resource, "nofile");
1231 assert_eq!(rlimits[0].soft, 65535);
1232 assert_eq!(rlimits[0].hard, 65535);
1233 assert_eq!(rlimits[1].resource, "nproc");
1234 assert_eq!(rlimits[1].soft, 4096);
1235 assert_eq!(rlimits[1].hard, 8192);
1236 }
1237
1238 #[test]
1239 fn test_parse_rlimits_ignores_empty_entries() {
1240 let rlimits = parse_rlimits("nofile=1024;").unwrap();
1241 assert_eq!(rlimits.len(), 1);
1242 assert_eq!(rlimits[0].resource, "nofile");
1243 }
1244
1245 #[test]
1246 fn test_parse_rlimits_rejects_unknown_resource() {
1247 let err = parse_rlimits("bogus=1024").unwrap_err();
1248 assert!(
1249 matches!(err, AgentdError::Config(msg) if msg.contains("unknown resource: bogus")),
1250 "unexpected error shape"
1251 );
1252 }
1253
1254 #[test]
1255 fn test_parse_rlimits_rejects_duplicate_resource() {
1256 let err = parse_rlimits("nofile=1024;nofile=65535").unwrap_err();
1257 assert!(
1258 matches!(err, AgentdError::Config(msg) if msg.contains("duplicate resource: nofile")),
1259 "unexpected error shape"
1260 );
1261 }
1262
1263 #[test]
1264 fn test_parse_rlimits_rejects_malformed_entry() {
1265 assert!(parse_rlimits("nofile").is_err());
1266 assert!(parse_rlimits("nofile=abc").is_err());
1267 assert!(parse_rlimits("nofile=65535:1024").is_err()); }
1269
1270 static HANDOFF_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1275
1276 fn with_handoff_env<R>(
1277 cmd: Option<&str>,
1278 args: Option<&str>,
1279 env_var: Option<&str>,
1280 f: impl FnOnce() -> R,
1281 ) -> R {
1282 let _guard = HANDOFF_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1283 unsafe {
1284 match cmd {
1285 Some(v) => env::set_var(ENV_HANDOFF_INIT, v),
1286 None => env::remove_var(ENV_HANDOFF_INIT),
1287 }
1288 match args {
1289 Some(v) => env::set_var(ENV_HANDOFF_INIT_ARGS, v),
1290 None => env::remove_var(ENV_HANDOFF_INIT_ARGS),
1291 }
1292 match env_var {
1293 Some(v) => env::set_var(ENV_HANDOFF_INIT_ENV, v),
1294 None => env::remove_var(ENV_HANDOFF_INIT_ENV),
1295 }
1296 }
1297 let out = f();
1298 unsafe {
1299 env::remove_var(ENV_HANDOFF_INIT);
1300 env::remove_var(ENV_HANDOFF_INIT_ARGS);
1301 env::remove_var(ENV_HANDOFF_INIT_ENV);
1302 }
1303 out
1304 }
1305
1306 #[test]
1307 fn test_parse_handoff_init_unset_returns_none() {
1308 let res = with_handoff_env(None, None, None, parse_handoff_init).unwrap();
1309 assert!(res.is_none());
1310 }
1311
1312 #[test]
1313 fn test_parse_handoff_init_empty_returns_none() {
1314 let res = with_handoff_env(Some(""), None, None, parse_handoff_init).unwrap();
1315 assert!(res.is_none());
1316 }
1317
1318 #[test]
1319 fn test_parse_handoff_init_cmd_only() {
1320 let res = with_handoff_env(Some("/lib/systemd/systemd"), None, None, parse_handoff_init)
1321 .unwrap()
1322 .unwrap();
1323 assert_eq!(res.cmd, PathBuf::from("/lib/systemd/systemd"));
1324 assert!(res.argv.is_empty());
1325 assert!(res.env.is_empty());
1326 }
1327
1328 #[test]
1329 fn test_parse_handoff_init_with_argv() {
1330 let argv = format!("--unit=multi-user.target{HANDOFF_INIT_SEP}--log-level=warning");
1331 let res = with_handoff_env(
1332 Some("/lib/systemd/systemd"),
1333 Some(&argv),
1334 None,
1335 parse_handoff_init,
1336 )
1337 .unwrap()
1338 .unwrap();
1339 assert_eq!(
1340 res.argv,
1341 vec![
1342 OsString::from("--unit=multi-user.target"),
1343 OsString::from("--log-level=warning"),
1344 ]
1345 );
1346 }
1347
1348 #[test]
1349 fn test_parse_handoff_init_with_env() {
1350 let envs = format!("container=microsandbox{HANDOFF_INIT_SEP}LANG=C.UTF-8");
1351 let res = with_handoff_env(Some("/sbin/init"), None, Some(&envs), parse_handoff_init)
1352 .unwrap()
1353 .unwrap();
1354 assert_eq!(
1355 res.env,
1356 vec![
1357 (OsString::from("container"), OsString::from("microsandbox")),
1358 (OsString::from("LANG"), OsString::from("C.UTF-8")),
1359 ]
1360 );
1361 }
1362
1363 #[test]
1364 fn test_parse_handoff_init_argv_with_spaces_preserved() {
1365 let argv = format!("--label=hello world{HANDOFF_INIT_SEP}--config=/etc/foo;bar");
1367 let res = with_handoff_env(Some("/sbin/init"), Some(&argv), None, parse_handoff_init)
1368 .unwrap()
1369 .unwrap();
1370 assert_eq!(
1371 res.argv,
1372 vec![
1373 OsString::from("--label=hello world"),
1374 OsString::from("--config=/etc/foo;bar"),
1375 ]
1376 );
1377 }
1378
1379 #[test]
1380 fn test_parse_handoff_init_rejects_relative_path() {
1381 let err = with_handoff_env(Some("sbin/init"), None, None, parse_handoff_init).unwrap_err();
1382 assert!(err.to_string().contains("absolute path"));
1383 }
1384
1385 #[test]
1386 fn test_parse_handoff_init_env_entry_missing_equals() {
1387 let envs = format!("KEY=value{HANDOFF_INIT_SEP}NOEQUALS");
1388 let err = with_handoff_env(Some("/sbin/init"), None, Some(&envs), parse_handoff_init)
1389 .unwrap_err();
1390 assert!(err.to_string().contains("missing '='"));
1391 }
1392
1393 #[test]
1394 fn test_parse_handoff_init_env_entry_empty_key_rejected() {
1395 let envs = "=value".to_string();
1397 let err = with_handoff_env(Some("/sbin/init"), None, Some(&envs), parse_handoff_init)
1398 .unwrap_err();
1399 assert!(err.to_string().contains("empty key"));
1400 }
1401
1402 #[test]
1403 fn test_parse_handoff_init_env_value_with_equals_is_value() {
1404 let envs = "PATH=/a:/b=/c".to_string();
1406 let res = with_handoff_env(Some("/sbin/init"), None, Some(&envs), parse_handoff_init)
1407 .unwrap()
1408 .unwrap();
1409 assert_eq!(
1410 res.env,
1411 vec![(OsString::from("PATH"), OsString::from("/a:/b=/c"))]
1412 );
1413 }
1414}