sys-mount 1.5.1

High level FFI binding around the sys mount & umount2 calls
Documentation
// Copyright 2018-2022 System76 <info@system76.com>
// SPDX-License-Identifier: MIT

use crate::MountBuilder;

use super::to_cstring;
use fstype::FilesystemType;
use libc::*;
use std::{
    ffi::{CString, OsStr},
    io,
    os::unix::ffi::OsStrExt,
    path::Path,
    ptr,
};
use umount::{unmount_, Unmount, UnmountDrop, UnmountFlags};

bitflags! {
    /// Flags which may be specified when mounting a file system.
    pub struct MountFlags: c_ulong {
        /// Perform a bind mount, making a file or a directory subtree visible at another
        /// point within a file system. Bind mounts may cross file system boundaries and
        /// span chroot(2) jails. The filesystemtype and data arguments are ignored. Up
        /// until Linux 2.6.26, mountflags was also ignored (the bind mount has the same
        /// mount options as the underlying mount point).
        const BIND = MS_BIND;

        /// Make directory changes on this file system synchronous.(This property can be
        /// obtained for individual directories or subtrees using chattr(1).)
        const DIRSYNC = MS_DIRSYNC;

        /// Permit mandatory locking on files in this file system. (Mandatory locking must
        /// still be enabled on a per-file basis, as described in fcntl(2).)
        const MANDLOCK = MS_MANDLOCK;

        /// Move a subtree. source specifies an existing mount point and target specifies
        /// the new location. The move is atomic: at no point is the subtree unmounted.
        /// The filesystemtype, mountflags, and data arguments are ignored.
        const MOVE = MS_MOVE;

        /// Do not update access times for (all types of) files on this file system.
        const NOATIME = MS_NOATIME;

        /// Do not allow access to devices (special files) on this file system.
        const NODEV = MS_NODEV;

        /// Do not update access times for directories on this file system. This flag provides
        /// a subset of the functionality provided by MS_NOATIME; that is, MS_NOATIME implies
        /// MS_NODIRATIME.
        const NODIRATIME = MS_NODIRATIME;

        /// Do not allow programs to be executed from this file system.
        const NOEXEC = MS_NOEXEC;

        /// Do not honor set-user-ID and set-group-ID bits when executing programs from this
        /// file system.
        const NOSUID = MS_NOSUID;

        /// Mount file system read-only.
        const RDONLY = MS_RDONLY;

        /// When a file on this file system is accessed, only update the file's last access
        /// time (atime) if the current value of atime is less than or equal to the file's
        /// last modification time (mtime) or last status change time (ctime). This option is
        /// useful for programs, such as mutt(1), that need to know when a file has been read
        /// since it was last modified. Since Linux 2.6.30, the kernel defaults to the behavior
        /// provided by this flag (unless MS_NOATIME was specified), and the MS_STRICTATIME
        /// flag is required to obtain traditional semantics. In addition, since Linux 2.6.30,
        /// the file's last access time is always updated if it is more than 1 day old.
        const RELATIME = MS_RELATIME;

        /// Remount an existing mount. This allows you to change the mountflags and data of an
        /// existing mount without having to unmount and remount the file system. target should
        /// be the same value specified in the initial mount() call; source and filesystemtype
        /// are ignored.
        ///
        /// The following mountflags can be changed: MS_RDONLY, MS_SYNCHRONOUS, MS_MANDLOCK;
        /// before kernel 2.6.16, the following could also be changed: MS_NOATIME and
        /// MS_NODIRATIME; and, additionally, before kernel 2.4.10, the following could also
        /// be changed: MS_NOSUID, MS_NODEV, MS_NOEXEC.
        const REMOUNT = MS_REMOUNT;

        /// Suppress the display of certain (printk()) warning messages in the kernel log.
        /// This flag supersedes the misnamed and obsolete MS_VERBOSE flag (available
        /// since Linux 2.4.12), which has the same meaning.
        const SILENT = MS_SILENT;

        /// Always update the last access time (atime) when files on this file system are
        /// accessed. (This was the default behavior before Linux 2.6.30.) Specifying this
        /// flag overrides the effect of setting the MS_NOATIME and MS_RELATIME flags.
        const STRICTATIME = MS_STRICTATIME;

        /// Make writes on this file system synchronous (as though the O_SYNC flag to
        /// open(2) was specified for all file opens to this file system).
        const SYNCHRONOUS = MS_SYNCHRONOUS;
    }
}

/// Handle for managing a mounted file system.
#[derive(Debug)]
pub struct Mount {
    pub(crate) target: CString,
    pub(crate) fstype: String,
    #[cfg(feature = "loop")]
    loopback: Option<loopdev::LoopDevice>,
    #[cfg(feature = "loop")]
    loop_path: Option<std::path::PathBuf>,
}

impl Unmount for Mount {
    fn unmount(&self, flags: UnmountFlags) -> io::Result<()> {
        unsafe {
            unmount_(self.target.as_ptr(), flags)?;
        }

        #[cfg(feature = "loop")]
        if let Some(ref loopback) = self.loopback {
            loopback.detach()?;
        }

        Ok(())
    }
}

impl Mount {
    /// Performs a mount with builder syntax.
    ///
    /// ```
    ///
    /// ```
    pub fn builder<'a>() -> MountBuilder<'a> {
        MountBuilder::default()
    }

    /// Mounts a file system at `source` to a `target` path in the system.
    ///
    /// ```rust,no_run
    /// extern crate sys_mount;
    ///
    /// use sys_mount::{
    ///     Mount,
    ///     MountFlags,
    ///     SupportedFilesystems
    /// };
    ///
    /// fn main() {
    ///     // Fetch a list of supported file systems.
    ///     // When mounting, a file system will be selected from this.
    ///     let supported = SupportedFilesystems::new().unwrap();
    ///
    ///     // Attempt to mount the src device to the dest directory.
    ///     let mount_result = Mount::new(
    ///         "/imaginary/block/device",
    ///         "/tmp/location",
    ///         &supported,
    ///         MountFlags::empty(),
    ///         None
    ///     );
    /// }
    /// ```
    /// # Notes
    ///
    /// The provided `source` device and `target` destinations must exist within the file system.
    ///
    /// If the `loop` feature is enabled, and the `source` is a file with an extension, a loopback
    /// device will be created, and the file will be associated with the loopback device. If the
    /// extension is `iso` or `squashfs`, the filesystem type will be set accordingly, and the
    /// `MountFlags` will also be modified to ensure that the `MountFlags::RDONLY` flag is set
    /// before mounting.
    ///
    /// The `fstype` parameter accepts either a `&str` or `&SupportedFilesystem` as input. If the
    /// input is a `&str`, then a particular file system will be used to mount the `source` with.
    /// If the input is a `&SupportedFilesystems`, then the file system will be selected
    /// automatically from the list.
    ///
    /// The automatic variant of `fstype` works by attempting to mount the `source` with all
    /// supported device-based file systems until it succeeds, or fails after trying all
    /// possible options.
    #[cfg_attr(not(feature = "loop"), allow(unused_mut))]
    pub fn new<'a, S, T, F>(
        source: S,
        target: T,
        fstype: F,
        mut flags: MountFlags,
        data: Option<&str>,
    ) -> io::Result<Self>
    where
        S: AsRef<Path>,
        T: AsRef<Path>,
        F: Into<FilesystemType<'a>>,
    {
        let mut fstype = fstype.into();
        let source = source.as_ref();
        let mut c_source = None;

        #[cfg(feature = "loop")]
        let mut loopback = None;
        #[cfg(feature = "loop")]
        let mut loop_path = None;

        if !source.as_os_str().is_empty() {
            // Create a loopback device if an iso or squashfs is being mounted.
            #[cfg(feature = "loop")]
            if let Some(ext) = source.extension() {
                let extf = if ext == "iso" { 1 } else { 0 } | if ext == "squashfs" { 2 } else { 0 };

                if extf != 0 {
                    fstype = if extf == 1 {
                        flags |= MountFlags::RDONLY;
                        FilesystemType::Manual("iso9660")
                    } else {
                        flags |= MountFlags::RDONLY;
                        FilesystemType::Manual("squashfs")
                    };
                }

                let new_loopback = loopdev::LoopControl::open()?.next_free()?;
                new_loopback.attach_file(source)?;
                let path = new_loopback.path().expect("loopback does not have path");
                c_source = Some(to_cstring(path.as_os_str().as_bytes())?);
                loop_path = Some(path);
                loopback = Some(new_loopback);
            }

            if c_source.is_none() {
                c_source = Some(to_cstring(source.as_os_str().as_bytes())?);
            }
        };

        let c_target = to_cstring(target.as_ref().as_os_str().as_bytes())?;
        let data = match data.map(|o| to_cstring(o.as_bytes())) {
            Some(Ok(string)) => Some(string),
            Some(Err(why)) => return Err(why),
            None => None,
        };

        let mut mount_data = MountData {
            c_source,
            c_target,
            flags,
            data,
        };

        let mut res = match fstype {
            FilesystemType::Auto(supported) => mount_data.automount(supported.dev_file_systems()),
            FilesystemType::Set(set) => mount_data.automount(set.iter().cloned()),
            FilesystemType::Manual(fstype) => mount_data.mount(fstype),
        };

        #[cfg(feature = "loop")]
        match res {
            Ok(ref mut mount) => {
                mount.loopback = loopback;
                mount.loop_path = loop_path;
            }
            Err(_) => {
                if let Some(loopback) = loopback {
                    let _ = loopback.detach();
                }
            }
        }

        res
    }

    /// If the device was associated with a loopback device, that device's path
    /// can be retrieved here.
    #[cfg(feature = "loop")]
    pub fn backing_loop_device(&self) -> Option<&Path> {
        self.loop_path.as_deref()
    }

    /// Describes the file system which this mount was mounted with.
    ///
    /// This is useful in the event that the mounted device was mounted automatically.
    pub fn get_fstype(&self) -> &str {
        &self.fstype
    }

    /// Return the path this mount was mounted on.
    pub fn target_path(&self) -> &Path {
        Path::new(OsStr::from_bytes(self.target.as_bytes()))
    }

    #[cfg(feature = "loop")]
    fn from_target_and_fstype(target: CString, fstype: String) -> Self {
        Mount {
            target,
            fstype,
            loopback: None,
            loop_path: None,
        }
    }

    #[cfg(not(feature = "loop"))]
    fn from_target_and_fstype(target: CString, fstype: String) -> Self {
        Mount { target, fstype }
    }
}

struct MountData {
    c_source: Option<CString>,
    c_target: CString,
    flags: MountFlags,
    data: Option<CString>,
}

impl MountData {
    fn mount(&mut self, fstype: &str) -> io::Result<Mount> {
        let c_fstype = to_cstring(fstype.as_bytes())?;
        match mount_(
            self.c_source.as_ref(),
            &self.c_target,
            &c_fstype,
            self.flags,
            self.data.as_ref(),
        ) {
            Ok(()) => Ok(Mount::from_target_and_fstype(
                self.c_target.clone(),
                fstype.to_owned(),
            )),
            Err(why) => Err(why),
        }
    }

    fn automount<'a, I: Iterator<Item = &'a str> + 'a>(mut self, iter: I) -> io::Result<Mount> {
        let mut res = Ok(());

        for fstype in iter {
            match self.mount(fstype) {
                mount @ Ok(_) => return mount,
                Err(why) => res = Err(why),
            }
        }

        match res {
            Ok(()) => Err(io::Error::new(
                io::ErrorKind::NotFound,
                "no supported file systems found",
            )),
            Err(why) => Err(why),
        }
    }
}

fn mount_(
    c_source: Option<&CString>,
    c_target: &CString,
    c_fstype: &CString,
    flags: MountFlags,
    c_data: Option<&CString>,
) -> io::Result<()> {
    let result = unsafe {
        mount(
            c_source.map_or_else(ptr::null, |s| s.as_ptr()),
            c_target.as_ptr(),
            c_fstype.as_ptr(),
            flags.bits(),
            c_data.map_or_else(ptr::null, |s| s.as_ptr()) as *const c_void,
        )
    };

    match result {
        0 => Ok(()),
        _err => Err(io::Error::last_os_error()),
    }
}

/// An abstraction that will ensure that temporary mounts are dropped in reverse.
pub struct Mounts(pub Vec<UnmountDrop<Mount>>);

impl Mounts {
    pub fn unmount(&mut self, lazy: bool) -> io::Result<()> {
        let flags = if lazy {
            UnmountFlags::DETACH
        } else {
            UnmountFlags::empty()
        };
        self.0
            .iter_mut()
            .rev()
            .try_for_each(|mount| mount.unmount(flags))
    }
}

impl Drop for Mounts {
    fn drop(&mut self) {
        for mount in self.0.drain(..).rev() {
            drop(mount);
        }
    }
}