outrig_cli/cli/
volume_arg.rs1use std::path::PathBuf;
13
14use outrig::config::MountAccess;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct CliVolume {
19 pub host: PathBuf,
20 pub container: PathBuf,
21 pub access: MountAccess,
22}
23
24pub fn parse_volume(raw: &str) -> Result<CliVolume, String> {
26 let parts: Vec<&str> = raw.split(':').collect();
27 let (host, container, access) = match parts.as_slice() {
28 [host, container] => (*host, *container, MountAccess::ReadOnly),
29 [host, container, mode] => {
30 let access = match *mode {
31 "ro" => MountAccess::ReadOnly,
32 "rw" => MountAccess::ReadWrite,
33 other => {
34 return Err(format!(
35 "invalid access {other:?} in --volume {raw:?}: expected `ro` or `rw`"
36 ));
37 }
38 };
39 (*host, *container, access)
40 }
41 _ => {
42 return Err(format!(
43 "invalid --volume {raw:?}: expected HOST:CONTAINER[:ro|rw]"
44 ));
45 }
46 };
47
48 if host.is_empty() {
49 return Err(format!("invalid --volume {raw:?}: empty host path"));
50 }
51 if container.is_empty() {
52 return Err(format!("invalid --volume {raw:?}: empty container path"));
53 }
54
55 Ok(CliVolume {
56 host: PathBuf::from(host),
57 container: PathBuf::from(container),
58 access,
59 })
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn two_segments_default_to_read_only() {
68 let v = parse_volume("/host/data:/data").unwrap();
69 assert_eq!(v.host, PathBuf::from("/host/data"));
70 assert_eq!(v.container, PathBuf::from("/data"));
71 assert_eq!(v.access, MountAccess::ReadOnly);
72 }
73
74 #[test]
75 fn explicit_rw_and_ro_are_honored() {
76 assert_eq!(
77 parse_volume("/h:/c:rw").unwrap().access,
78 MountAccess::ReadWrite
79 );
80 assert_eq!(
81 parse_volume("/h:/c:ro").unwrap().access,
82 MountAccess::ReadOnly
83 );
84 }
85
86 #[test]
87 fn relative_host_path_is_preserved_verbatim() {
88 let v = parse_volume("data:/data:rw").unwrap();
90 assert_eq!(v.host, PathBuf::from("data"));
91 }
92
93 #[test]
94 fn rejects_unknown_access_segment() {
95 let err = parse_volume("/h:/c:rwx").unwrap_err();
96 assert!(err.contains("expected `ro` or `rw`"), "got: {err}");
97 }
98
99 #[test]
100 fn rejects_too_many_segments() {
101 let err = parse_volume("/h:/c:rw:extra").unwrap_err();
102 assert!(err.contains("HOST:CONTAINER[:ro|rw]"), "got: {err}");
103 }
104
105 #[test]
106 fn rejects_single_segment() {
107 let err = parse_volume("/h").unwrap_err();
108 assert!(err.contains("HOST:CONTAINER[:ro|rw]"), "got: {err}");
109 }
110
111 #[test]
112 fn rejects_empty_host() {
113 let err = parse_volume(":/c").unwrap_err();
114 assert!(err.contains("empty host path"), "got: {err}");
115 }
116
117 #[test]
118 fn rejects_empty_container() {
119 let err = parse_volume("/h:").unwrap_err();
120 assert!(err.contains("empty container path"), "got: {err}");
121 }
122}