Skip to main content

async_fuser/mnt/
mount_options.rs

1use std::collections::HashSet;
2use std::io;
3use std::io::ErrorKind;
4
5use crate::SessionACL;
6
7/// Fuser session configuration, including mount options.
8#[derive(Debug, Clone, Default, Eq, PartialEq)]
9#[non_exhaustive]
10pub struct Config {
11    /// Mount options.
12    pub mount_options: Vec<MountOption>,
13    /// Who can access the filesystem.
14    pub acl: SessionACL,
15    /// Number of event loop threads. If unspecified, one thread is used.
16    pub n_threads: Option<usize>,
17    /// Use `FUSE_DEV_IOC_CLONE` to give each worker thread its own fd.
18    /// This enables more efficient request processing
19    /// when multiple threads are used. Requires Linux 4.5+.
20    pub clone_fd: bool,
21}
22
23/// Mount options accepted by the FUSE filesystem type
24/// See 'man mount.fuse' for details
25// TODO: add all options that 'man mount.fuse' documents and libfuse supports
26#[derive(Debug, Eq, PartialEq, Hash, Clone)]
27pub enum MountOption {
28    /// Set the name of the source in mtab
29    FSName(String),
30    /// Set the filesystem subtype in mtab
31    Subtype(String),
32    /// Allows passing an option which is not otherwise supported in these enums
33    #[allow(clippy::upper_case_acronyms)]
34    CUSTOM(String),
35
36    /* Parameterless options */
37    /// Automatically unmount when the mounting process exits
38    ///
39    /// `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.
40    AutoUnmount,
41    /// Enable permission checking in the kernel
42    DefaultPermissions,
43
44    /* Flags */
45    /// Enable special character and block devices
46    Dev,
47    /// Disable special character and block devices
48    NoDev,
49    /// Honor set-user-id and set-groupd-id bits on files
50    Suid,
51    /// Don't honor set-user-id and set-groupd-id bits on files
52    NoSuid,
53    /// Read-only filesystem
54    RO,
55    /// Read-write filesystem
56    RW,
57    /// Allow execution of binaries
58    Exec,
59    /// Don't allow execution of binaries
60    NoExec,
61    /// Support inode access time
62    Atime,
63    /// Don't update inode access time
64    NoAtime,
65    /// All modifications to directories will be done synchronously
66    DirSync,
67    /// All I/O will be done synchronously
68    Sync,
69    /// All I/O will be done asynchronously
70    Async,
71    /* libfuse library options, such as "direct_io", are not included since they are specific
72    to libfuse, and not part of the kernel ABI */
73}
74
75impl MountOption {
76    #[cfg(test)]
77    fn from_str(s: &str) -> MountOption {
78        match s {
79            "auto_unmount" => MountOption::AutoUnmount,
80            "default_permissions" => MountOption::DefaultPermissions,
81            "dev" => MountOption::Dev,
82            "nodev" => MountOption::NoDev,
83            "suid" => MountOption::Suid,
84            "nosuid" => MountOption::NoSuid,
85            "ro" => MountOption::RO,
86            "rw" => MountOption::RW,
87            "exec" => MountOption::Exec,
88            "noexec" => MountOption::NoExec,
89            "atime" => MountOption::Atime,
90            "noatime" => MountOption::NoAtime,
91            "dirsync" => MountOption::DirSync,
92            "sync" => MountOption::Sync,
93            "async" => MountOption::Async,
94            x if x.starts_with("fsname=") => MountOption::FSName(x[7..].into()),
95            x if x.starts_with("subtype=") => MountOption::Subtype(x[8..].into()),
96            x => MountOption::CUSTOM(x.into()),
97        }
98    }
99}
100
101#[allow(dead_code)]
102#[derive(PartialEq)]
103pub(crate) enum MountOptionGroup {
104    KernelOption,
105    KernelFlag,
106    Fusermount,
107}
108
109pub(crate) fn check_option_conflicts(config: &Config) -> Result<(), io::Error> {
110    let options = &config.mount_options;
111    let mut options_set = HashSet::new();
112    options_set.extend(options.iter().cloned());
113    let conflicting: HashSet<MountOption> = options.iter().flat_map(conflicts_with).collect();
114    let intersection: Vec<MountOption> = conflicting.intersection(&options_set).cloned().collect();
115    if intersection.is_empty() {
116        Ok(())
117    } else {
118        Err(io::Error::new(
119            ErrorKind::InvalidInput,
120            format!("Conflicting mount options found: {intersection:?}"),
121        ))
122    }
123}
124
125fn conflicts_with(option: &MountOption) -> Vec<MountOption> {
126    match option {
127        MountOption::FSName(_)
128        | MountOption::Subtype(_)
129        | MountOption::CUSTOM(_)
130        | MountOption::DirSync
131        | MountOption::AutoUnmount
132        | MountOption::DefaultPermissions => vec![],
133        MountOption::Dev => vec![MountOption::NoDev],
134        MountOption::NoDev => vec![MountOption::Dev],
135        MountOption::Suid => vec![MountOption::NoSuid],
136        MountOption::NoSuid => vec![MountOption::Suid],
137        MountOption::RO => vec![MountOption::RW],
138        MountOption::RW => vec![MountOption::RO],
139        MountOption::Exec => vec![MountOption::NoExec],
140        MountOption::NoExec => vec![MountOption::Exec],
141        MountOption::Atime => vec![MountOption::NoAtime],
142        MountOption::NoAtime => vec![MountOption::Atime],
143        MountOption::Sync => vec![MountOption::Async],
144        MountOption::Async => vec![MountOption::Sync],
145    }
146}
147
148// Format option to be passed to libfuse or kernel
149#[allow(dead_code)]
150pub(crate) fn option_to_string(option: &MountOption) -> String {
151    match option {
152        MountOption::FSName(name) => format!("fsname={name}"),
153        MountOption::Subtype(subtype) => format!("subtype={subtype}"),
154        MountOption::CUSTOM(value) => value.to_string(),
155        MountOption::AutoUnmount => "auto_unmount".to_string(),
156        MountOption::DefaultPermissions => "default_permissions".to_string(),
157        MountOption::Dev => "dev".to_string(),
158        MountOption::NoDev => "nodev".to_string(),
159        MountOption::Suid => "suid".to_string(),
160        MountOption::NoSuid => "nosuid".to_string(),
161        MountOption::RO => "ro".to_string(),
162        MountOption::RW => "rw".to_string(),
163        MountOption::Exec => "exec".to_string(),
164        MountOption::NoExec => "noexec".to_string(),
165        MountOption::Atime => "atime".to_string(),
166        MountOption::NoAtime => "noatime".to_string(),
167        MountOption::DirSync => "dirsync".to_string(),
168        MountOption::Sync => "sync".to_string(),
169        MountOption::Async => "async".to_string(),
170    }
171}
172
173#[allow(dead_code)]
174pub(crate) fn option_group(option: &MountOption) -> MountOptionGroup {
175    match option {
176        MountOption::FSName(_) => MountOptionGroup::Fusermount,
177        MountOption::Subtype(_) => MountOptionGroup::Fusermount,
178        MountOption::CUSTOM(_) => MountOptionGroup::KernelOption,
179        MountOption::AutoUnmount => MountOptionGroup::Fusermount,
180        MountOption::Dev => MountOptionGroup::KernelFlag,
181        MountOption::NoDev => MountOptionGroup::KernelFlag,
182        MountOption::Suid => MountOptionGroup::KernelFlag,
183        MountOption::NoSuid => MountOptionGroup::KernelFlag,
184        MountOption::RO => MountOptionGroup::KernelFlag,
185        MountOption::RW => MountOptionGroup::KernelFlag,
186        MountOption::Exec => MountOptionGroup::KernelFlag,
187        MountOption::NoExec => MountOptionGroup::KernelFlag,
188        MountOption::Atime => MountOptionGroup::KernelFlag,
189        MountOption::NoAtime => MountOptionGroup::KernelFlag,
190        MountOption::DirSync => MountOptionGroup::KernelFlag,
191        MountOption::Sync => MountOptionGroup::KernelFlag,
192        MountOption::Async => MountOptionGroup::KernelFlag,
193        MountOption::DefaultPermissions => MountOptionGroup::KernelOption,
194    }
195}
196
197#[cfg(target_os = "linux")]
198#[allow(dead_code)]
199pub(crate) fn option_to_flag(option: &MountOption) -> io::Result<nix::mount::MsFlags> {
200    match option {
201        MountOption::Dev => Ok(nix::mount::MsFlags::empty()), // There is no option for dev. It's the absence of NoDev
202        MountOption::NoDev => Ok(nix::mount::MsFlags::MS_NODEV),
203        MountOption::Suid => Ok(nix::mount::MsFlags::empty()),
204        MountOption::NoSuid => Ok(nix::mount::MsFlags::MS_NOSUID),
205        MountOption::RW => Ok(nix::mount::MsFlags::empty()),
206        MountOption::RO => Ok(nix::mount::MsFlags::MS_RDONLY),
207        MountOption::Exec => Ok(nix::mount::MsFlags::empty()),
208        MountOption::NoExec => Ok(nix::mount::MsFlags::MS_NOEXEC),
209        MountOption::Atime => Ok(nix::mount::MsFlags::empty()),
210        MountOption::NoAtime => Ok(nix::mount::MsFlags::MS_NOATIME),
211        MountOption::Async => Ok(nix::mount::MsFlags::empty()),
212        MountOption::Sync => Ok(nix::mount::MsFlags::MS_SYNCHRONOUS),
213        MountOption::DirSync => Ok(nix::mount::MsFlags::MS_DIRSYNC),
214        option => Err(io::Error::new(
215            io::ErrorKind::InvalidInput,
216            format!("Invalid mount option for flag conversion: {option:?}"),
217        )),
218    }
219}
220
221#[cfg(target_os = "macos")]
222#[allow(dead_code)]
223pub(crate) fn option_to_flag(option: &MountOption) -> io::Result<nix::mount::MntFlags> {
224    match option {
225        MountOption::Dev => Ok(nix::mount::MntFlags::empty()), // There is no option for dev. It's the absence of NoDev
226        MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV),
227        MountOption::Suid => Ok(nix::mount::MntFlags::empty()),
228        MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID),
229        MountOption::RW => Ok(nix::mount::MntFlags::empty()),
230        MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY),
231        MountOption::Exec => Ok(nix::mount::MntFlags::empty()),
232        MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC),
233        MountOption::Atime => Ok(nix::mount::MntFlags::empty()),
234        MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME),
235        MountOption::Async => Ok(nix::mount::MntFlags::empty()),
236        MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS),
237        option => Err(io::Error::new(
238            io::ErrorKind::InvalidInput,
239            format!("Invalid mount option for flag conversion: {option:?}"),
240        )),
241    }
242}
243
244#[cfg_attr(
245    any(
246        target_os = "freebsd",
247        target_os = "dragonfly",
248        target_os = "openbsd",
249        target_os = "netbsd"
250    ),
251    allow(dead_code)
252)]
253#[cfg(any(
254    target_os = "freebsd",
255    target_os = "dragonfly",
256    target_os = "openbsd",
257    target_os = "netbsd"
258))]
259pub(crate) fn option_to_flag(option: &MountOption) -> io::Result<nix::mount::MntFlags> {
260    match option {
261        MountOption::Dev => Ok(nix::mount::MntFlags::empty()),
262        #[cfg(target_os = "freebsd")]
263        MountOption::NoDev => Err(io::Error::new(
264            io::ErrorKind::Unsupported,
265            "NoDev option is not supported on FreeBSD",
266        )),
267        #[cfg(not(target_os = "freebsd"))]
268        MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV),
269        MountOption::Suid => Ok(nix::mount::MntFlags::empty()),
270        MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID),
271        MountOption::RW => Ok(nix::mount::MntFlags::empty()),
272        MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY),
273        MountOption::Exec => Ok(nix::mount::MntFlags::empty()),
274        MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC),
275        MountOption::Atime => Ok(nix::mount::MntFlags::empty()),
276        MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME),
277        MountOption::Async => Ok(nix::mount::MntFlags::MNT_ASYNC),
278        MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS),
279        MountOption::DirSync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS),
280        option => Err(io::Error::new(
281            io::ErrorKind::InvalidInput,
282            format!("Invalid mount option for flag conversion: {option:?}"),
283        )),
284    }
285}
286
287#[cfg(test)]
288mod test {
289    use crate::mnt::mount_options::*;
290
291    #[test]
292    fn option_checking() {
293        assert!(
294            check_option_conflicts(&Config {
295                mount_options: vec![MountOption::Suid, MountOption::NoSuid],
296                ..Config::default()
297            })
298            .is_err()
299        );
300        assert!(
301            check_option_conflicts(&Config {
302                mount_options: vec![MountOption::Suid, MountOption::NoExec],
303                ..Config::default()
304            })
305            .is_ok()
306        );
307    }
308    #[test]
309    fn option_round_trip() {
310        use crate::mnt::mount_options::MountOption::*;
311        for x in &[
312            FSName("Blah".to_owned()),
313            Subtype("Bloo".to_owned()),
314            CUSTOM("bongos".to_owned()),
315            AutoUnmount,
316            DefaultPermissions,
317            Dev,
318            NoDev,
319            Suid,
320            NoSuid,
321            RO,
322            RW,
323            Exec,
324            NoExec,
325            Atime,
326            NoAtime,
327            DirSync,
328            Sync,
329            Async,
330        ] {
331            assert_eq!(*x, MountOption::from_str(option_to_string(x).as_ref()));
332        }
333    }
334}