1use std::io;
2use std::io::ErrorKind;
3use std::{collections::HashSet, ffi::OsStr};
4
5#[derive(Debug, Eq, PartialEq, Hash, Clone)]
9pub enum MountOption {
10 FSName(String),
12 Subtype(String),
14 #[allow(clippy::upper_case_acronyms)]
16 CUSTOM(String),
17
18 AllowOther,
22 AllowRoot,
24 AutoUnmount,
28 DefaultPermissions,
30
31 Dev,
34 NoDev,
36 Suid,
38 NoSuid,
40 RO,
42 RW,
44 Exec,
46 NoExec,
48 Atime,
50 NoAtime,
52 DirSync,
54 Sync,
56 Async,
58 }
61
62impl MountOption {
63 pub(crate) fn from_str(s: &str) -> MountOption {
64 match s {
65 "auto_unmount" => MountOption::AutoUnmount,
66 "allow_other" => MountOption::AllowOther,
67 "allow_root" => MountOption::AllowRoot,
68 "default_permissions" => MountOption::DefaultPermissions,
69 "dev" => MountOption::Dev,
70 "nodev" => MountOption::NoDev,
71 "suid" => MountOption::Suid,
72 "nosuid" => MountOption::NoSuid,
73 "ro" => MountOption::RO,
74 "rw" => MountOption::RW,
75 "exec" => MountOption::Exec,
76 "noexec" => MountOption::NoExec,
77 "atime" => MountOption::Atime,
78 "noatime" => MountOption::NoAtime,
79 "dirsync" => MountOption::DirSync,
80 "sync" => MountOption::Sync,
81 "async" => MountOption::Async,
82 x if x.starts_with("fsname=") => MountOption::FSName(x[7..].into()),
83 x if x.starts_with("subtype=") => MountOption::Subtype(x[8..].into()),
84 x => MountOption::CUSTOM(x.into()),
85 }
86 }
87}
88
89pub fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> {
90 let mut options_set = HashSet::new();
91 options_set.extend(options.iter().cloned());
92 let conflicting: HashSet<MountOption> = options.iter().flat_map(conflicts_with).collect();
93 let intersection: Vec<MountOption> = conflicting.intersection(&options_set).cloned().collect();
94 if !intersection.is_empty() {
95 Err(io::Error::new(
96 ErrorKind::InvalidInput,
97 format!("Conflicting mount options found: {intersection:?}"),
98 ))
99 } else {
100 Ok(())
101 }
102}
103
104fn conflicts_with(option: &MountOption) -> Vec<MountOption> {
105 match option {
106 MountOption::FSName(_) => vec![],
107 MountOption::Subtype(_) => vec![],
108 MountOption::CUSTOM(_) => vec![],
109 MountOption::AllowOther => vec![MountOption::AllowRoot],
110 MountOption::AllowRoot => vec![MountOption::AllowOther],
111 MountOption::AutoUnmount => vec![],
112 MountOption::DefaultPermissions => vec![],
113 MountOption::Dev => vec![MountOption::NoDev],
114 MountOption::NoDev => vec![MountOption::Dev],
115 MountOption::Suid => vec![MountOption::NoSuid],
116 MountOption::NoSuid => vec![MountOption::Suid],
117 MountOption::RO => vec![MountOption::RW],
118 MountOption::RW => vec![MountOption::RO],
119 MountOption::Exec => vec![MountOption::NoExec],
120 MountOption::NoExec => vec![MountOption::Exec],
121 MountOption::Atime => vec![MountOption::NoAtime],
122 MountOption::NoAtime => vec![MountOption::Atime],
123 MountOption::DirSync => vec![],
124 MountOption::Sync => vec![MountOption::Async],
125 MountOption::Async => vec![MountOption::Sync],
126 }
127}
128
129pub fn option_to_string(option: &MountOption) -> String {
131 match option {
132 MountOption::FSName(name) => format!("fsname={name}"),
133 MountOption::Subtype(subtype) => format!("subtype={subtype}"),
134 MountOption::CUSTOM(value) => value.to_string(),
135 MountOption::AutoUnmount => "auto_unmount".to_string(),
136 MountOption::AllowOther => "allow_other".to_string(),
137 MountOption::AllowRoot => "allow_other".to_string(),
140 MountOption::DefaultPermissions => "default_permissions".to_string(),
141 MountOption::Dev => "dev".to_string(),
142 MountOption::NoDev => "nodev".to_string(),
143 MountOption::Suid => "suid".to_string(),
144 MountOption::NoSuid => "nosuid".to_string(),
145 MountOption::RO => "ro".to_string(),
146 MountOption::RW => "rw".to_string(),
147 MountOption::Exec => "exec".to_string(),
148 MountOption::NoExec => "noexec".to_string(),
149 MountOption::Atime => "atime".to_string(),
150 MountOption::NoAtime => "noatime".to_string(),
151 MountOption::DirSync => "dirsync".to_string(),
152 MountOption::Sync => "sync".to_string(),
153 MountOption::Async => "async".to_string(),
154 }
155}
156
157pub(crate) fn parse_options_from_args(args: &[&OsStr]) -> io::Result<Vec<MountOption>> {
162 let err = |x| io::Error::new(ErrorKind::InvalidInput, x);
163 let args: Option<Vec<_>> = args.iter().map(|x| x.to_str()).collect();
164 let args = args.ok_or_else(|| err("Error parsing args: Invalid UTF-8".to_owned()))?;
165 let mut it = args.iter();
166 let mut out = vec![];
167 loop {
168 let opt = match it.next() {
169 None => break,
170 Some(&"-o") => *it.next().ok_or_else(|| {
171 err("Error parsing args: Expected option, reached end of args".to_owned())
172 })?,
173 Some(x) if x.starts_with("-o") => &x[2..],
174 Some(x) => return Err(err(format!("Error parsing args: expected -o, got {x}"))),
175 };
176 for x in opt.split(',') {
177 out.push(MountOption::from_str(x))
178 }
179 }
180 Ok(out)
181}
182
183#[cfg(test)]
184mod test {
185 use std::os::unix::prelude::OsStrExt;
186
187 use super::*;
188
189 #[test]
190 fn option_checking() {
191 assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid]).is_err());
192 assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec]).is_ok());
193 }
194 #[test]
195 fn option_round_trip() {
196 use super::MountOption::*;
197 for x in [
198 FSName("Blah".to_owned()),
199 Subtype("Bloo".to_owned()),
200 CUSTOM("bongos".to_owned()),
201 AllowOther,
202 AutoUnmount,
203 DefaultPermissions,
204 Dev,
205 NoDev,
206 Suid,
207 NoSuid,
208 RO,
209 RW,
210 Exec,
211 NoExec,
212 Atime,
213 NoAtime,
214 DirSync,
215 Sync,
216 Async,
217 ]
218 .iter()
219 {
220 assert_eq!(*x, MountOption::from_str(option_to_string(x).as_ref()))
221 }
222 }
223
224 #[test]
225 fn test_parse_options() {
226 use super::MountOption::*;
227
228 assert_eq!(parse_options_from_args(&[]).unwrap(), &[]);
229
230 let o: Vec<_> = "-o suid -o ro,nodev,noexec -osync"
231 .split(' ')
232 .map(OsStr::new)
233 .collect();
234 let out = parse_options_from_args(o.as_ref()).unwrap();
235 assert_eq!(out, [Suid, RO, NoDev, NoExec, Sync]);
236
237 assert!(parse_options_from_args(&[OsStr::new("-o")]).is_err());
238 assert!(parse_options_from_args(&[OsStr::new("not o")]).is_err());
239 assert!(parse_options_from_args(&[OsStr::from_bytes(b"-o\xc3\x28")]).is_err());
240 }
241}