fuser/mnt/
mount_options.rs

1use std::io;
2use std::io::ErrorKind;
3use std::{collections::HashSet, ffi::OsStr};
4
5/// Mount options accepted by the FUSE filesystem type
6/// See 'man mount.fuse' for details
7// TODO: add all options that 'man mount.fuse' documents and libfuse supports
8#[derive(Debug, Eq, PartialEq, Hash, Clone)]
9pub enum MountOption {
10    /// Set the name of the source in mtab
11    FSName(String),
12    /// Set the filesystem subtype in mtab
13    Subtype(String),
14    /// Allows passing an option which is not otherwise supported in these enums
15    #[allow(clippy::upper_case_acronyms)]
16    CUSTOM(String),
17
18    /* Parameterless options */
19    /// Allow all users to access files on this filesystem. By default access is restricted to the
20    /// user who mounted it
21    AllowOther,
22    /// Allow the root user to access this filesystem, in addition to the user who mounted it
23    AllowRoot,
24    /// Automatically unmount when the mounting process exits
25    ///
26    /// `AutoUnmount` requires `AllowOther` or `AllowRoot`. If `AutoUnmount` is set and neither `Allow...` is set, the FUSE configuration must permit `allow_other`, otherwise mounting will fail.
27    AutoUnmount,
28    /// Enable permission checking in the kernel
29    DefaultPermissions,
30
31    /* Flags */
32    /// Enable special character and block devices
33    Dev,
34    /// Disable special character and block devices
35    NoDev,
36    /// Honor set-user-id and set-groupd-id bits on files
37    Suid,
38    /// Don't honor set-user-id and set-groupd-id bits on files
39    NoSuid,
40    /// Read-only filesystem
41    RO,
42    /// Read-write filesystem
43    RW,
44    /// Allow execution of binaries
45    Exec,
46    /// Don't allow execution of binaries
47    NoExec,
48    /// Support inode access time
49    Atime,
50    /// Don't update inode access time
51    NoAtime,
52    /// All modifications to directories will be done synchronously
53    DirSync,
54    /// All I/O will be done synchronously
55    Sync,
56    /// All I/O will be done asynchronously
57    Async,
58    /* libfuse library options, such as "direct_io", are not included since they are specific
59    to libfuse, and not part of the kernel ABI */
60}
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
129// Format option to be passed to libfuse or kernel
130pub 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        // AllowRoot is implemented by allowing everyone access and then restricting to
138        // root + owner within fuser
139        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
157/// Parses mount command args.
158///
159/// Input: ["-o", "suid", "-o", "ro,nodev,noexec", "-osync"]
160/// Output Ok([Suid, RO, NoDev, NoExec, Sync])
161pub(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}