├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── examples ├── umount.rs └── mount.rs ├── LICENSE ├── src ├── fstype.rs ├── umount.rs ├── supported.rs ├── mount.rs ├── lib.rs ├── flags.rs └── builder.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.command": "clippy", 3 | "rust-analyzer.checkOnSave.extraArgs": ["--", "-W", "clippy::pedantic"] 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (2022-11-04) 2 | 3 | - Improvements to loopback device mounting support 4 | - `Mount::builder` is now the preferred way to configure mount options 5 | - `Mount::new` has been simplified to accept only a source and dest target 6 | - Which will attempt to automatically guess the filesystem type 7 | - Applied all suggestions from clippy's pedantic lints 8 | - Updated to Rust 2021 edition, with Rust 1.65.0 features 9 | - Updated all dependencies 10 | 11 | # 1.2.0 12 | 13 | Add ability to mount any file that contains a filesystem 14 | 15 | # 1.1.0 16 | 17 | Add `swapoff` and `Mounts` to API 18 | 19 | # 1.0.3 20 | 21 | Fix ISO and squashfs mounting 22 | 23 | # 1.0.2 24 | 25 | Fix the crate examples 26 | 27 | # 1.0.1 28 | 29 | Fix source type parameter in `Mount` 30 | 31 | # 1.0.0 32 | 33 | Intiail release 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sys-mount" 3 | version = "3.0.1" 4 | description = "High level FFI binding around the sys mount & umount2 calls" 5 | repository = "https://github.com/pop-os/sys-mount" 6 | authors = ["Michael Aaron Murphy "] 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["linux", "sys", "mount", "umount"] 9 | categories = ["external-ffi-bindings", "filesystem", "os::unix-apis"] 10 | edition = "2021" 11 | 12 | [badges] 13 | maintenance = { status = "passively-maintained" } 14 | 15 | [dependencies] 16 | bitflags = "2.4.1" 17 | libc = "0.2.139" 18 | loopdev = { package = "loopdev-3", version = "0.5.0", optional = true } 19 | smart-default = "0.7.1" 20 | thiserror = "1.0.38" 21 | tracing = "0.1.37" 22 | 23 | [dev-dependencies] 24 | clap = "4.1.4" 25 | 26 | [features] 27 | default = ["loop"] 28 | loop = ["loopdev"] 29 | -------------------------------------------------------------------------------- /examples/umount.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | extern crate clap; 5 | extern crate sys_mount; 6 | 7 | use clap::{Arg, Command}; 8 | use std::process::ExitCode; 9 | use sys_mount::{unmount, UnmountFlags}; 10 | 11 | fn main() -> ExitCode { 12 | let matches = Command::new("umount") 13 | .arg(Arg::new("lazy").short('l').long("lazy")) 14 | .arg(Arg::new("source").required(true)) 15 | .get_matches(); 16 | 17 | let src = matches.get_one::("source").unwrap(); 18 | 19 | let flags = if matches.get_flag("lazy") { 20 | UnmountFlags::DETACH 21 | } else { 22 | UnmountFlags::empty() 23 | }; 24 | 25 | let Err(why) = unmount(src, flags) else { 26 | return ExitCode::SUCCESS; 27 | }; 28 | 29 | eprintln!("failed to unmount {}: {}", src, why); 30 | ExitCode::FAILURE 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2022 System76 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/fstype.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use crate::supported::SupportedFilesystems; 5 | 6 | /// Defines how the file system type should be derived for a mount -- auto or manual 7 | #[derive(Clone, Copy, Debug)] 8 | pub enum FilesystemType<'a> { 9 | /// The automatic variant will iterate through a list of pre-generated supported 10 | /// file systems, and attempt to mount each one before giving up. 11 | Auto(&'a SupportedFilesystems), 12 | /// The caller can avoid costly trial-and-error iteration with this variant. 13 | Manual(&'a str), 14 | /// A specific set of file systems to attempt to mount with. 15 | Set(&'a [&'a str]), 16 | } 17 | 18 | impl<'a> From<&'a SupportedFilesystems> for FilesystemType<'a> { 19 | fn from(s: &'a SupportedFilesystems) -> Self { 20 | FilesystemType::Auto(s) 21 | } 22 | } 23 | 24 | impl<'a> From<&'a str> for FilesystemType<'a> { 25 | fn from(s: &'a str) -> Self { 26 | FilesystemType::Manual(s) 27 | } 28 | } 29 | 30 | impl<'a> From<&'a [&'a str]> for FilesystemType<'a> { 31 | fn from(s: &'a [&'a str]) -> Self { 32 | FilesystemType::Set(s) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/mount.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use clap::{Arg, Command}; 5 | use std::process::ExitCode; 6 | use sys_mount::{Mount, SupportedFilesystems}; 7 | 8 | fn main() -> ExitCode { 9 | let matches = Command::new("mount") 10 | .arg(Arg::new("source").required(true)) 11 | .arg(Arg::new("directory").required(true)) 12 | .get_matches(); 13 | 14 | let src = matches.get_one::("source").unwrap(); 15 | let dir = matches.get_one::("directory").unwrap(); 16 | 17 | // Fetch a listed of supported file systems on this system. This will be used 18 | // as the fstype to `Mount::new`, as the `Auto` mount parameter. 19 | let supported = match SupportedFilesystems::new() { 20 | Ok(supported) => supported, 21 | Err(why) => { 22 | eprintln!("failed to get supported file systems: {}", why); 23 | return ExitCode::FAILURE; 24 | } 25 | }; 26 | 27 | // The source block will be mounted to the target directory, and the fstype is likely 28 | // one of the supported file systems. 29 | match Mount::builder().fstype(&supported).mount(src, dir) { 30 | Ok(mount) => { 31 | println!("mounted {} ({}) to {}", src, mount.get_fstype(), dir); 32 | ExitCode::SUCCESS 33 | } 34 | Err(why) => { 35 | eprintln!("failed to mount {} to {}: {}", src, dir, why); 36 | ExitCode::FAILURE 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sys-mount 2 | 3 | ![Rust Compatibility](https://img.shields.io/badge/rust-1.24.1%20tested-green.svg) 4 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://gitlab.com/evertiro/maco/blob/master/LICENSE) 5 | 6 | High level FFI bindings to the `mount` and `umount2` system calls, for Rust. 7 | 8 | ## Examples 9 | 10 | ### Mount 11 | 12 | This is how the `mount` command could be written with this API. 13 | 14 | ```rust 15 | extern crate clap; 16 | extern crate sys_mount; 17 | 18 | use clap::{App, Arg}; 19 | use sys_mount::{Mount, MountFlags, SupportedFilesystems}; 20 | use std::process::exit; 21 | 22 | fn main() { 23 | let matches = App::new("mount") 24 | .arg(Arg::with_name("source").required(true)) 25 | .arg(Arg::with_name("directory").required(true)) 26 | .get_matches(); 27 | 28 | let src = matches.value_of("source").unwrap(); 29 | let dir = matches.value_of("directory").unwrap(); 30 | 31 | // Fetch a listed of supported file systems on this system. This will be used 32 | // as the fstype to `Mount::new`, as the `Auto` mount parameter. 33 | let supported = match SupportedFilesystems::new() { 34 | Ok(supported) => supported, 35 | Err(why) => { 36 | eprintln!("failed to get supported file systems: {}", why); 37 | exit(1); 38 | } 39 | }; 40 | 41 | // The source block will be mounted to the target directory, and the fstype is likely 42 | // one of the supported file systems. 43 | let result = Mount::builder() 44 | .fstype(FilesystemType::from(&supported)) 45 | .mount(src, dir); 46 | 47 | match result { 48 | Ok(mount) => { 49 | println!("mounted {} ({}) to {}", src, mount.get_fstype(), dir); 50 | } 51 | Err(why) => { 52 | eprintln!("failed to mount {} to {}: {}", src, dir, why); 53 | exit(1); 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ### Umount 60 | 61 | This is how the `umount` command could be implemented. 62 | 63 | ```rust 64 | extern crate clap; 65 | extern crate sys_mount; 66 | 67 | use clap::{App, Arg}; 68 | use sys_mount::{unmount, UnmountFlags}; 69 | use std::process::exit; 70 | 71 | fn main() { 72 | let matches = App::new("umount") 73 | .arg(Arg::with_name("lazy") 74 | .short("l") 75 | .long("lazy")) 76 | .arg(Arg::with_name("source").required(true)) 77 | .get_matches(); 78 | 79 | let src = matches.value_of("source").unwrap(); 80 | 81 | let flags = if matches.is_present("lazy") { 82 | UnmountFlags::DETACH 83 | } else { 84 | UnmountFlags::empty() 85 | }; 86 | 87 | match unmount(src, flags) { 88 | Ok(()) => (), 89 | Err(why) => { 90 | eprintln!("failed to unmount {}: {}", src, why); 91 | exit(1); 92 | } 93 | } 94 | } 95 | ``` -------------------------------------------------------------------------------- /src/umount.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use crate::UnmountFlags; 5 | use libc::{c_char, umount2}; 6 | use std::{ffi::CString, io, ops::Deref, os::unix::ffi::OsStrExt, path::Path, ptr}; 7 | 8 | /// Unmount trait which enables any type that implements it to be upgraded into an `UnmountDrop`. 9 | pub trait Unmount { 10 | /// Unmount this mount with the given `flags`. 11 | /// 12 | /// This will also detach the loopback device that the mount is assigned to, if 13 | /// it was associated with a loopback device. 14 | /// 15 | /// # Errors 16 | /// 17 | /// On failure to unmount 18 | fn unmount(&self, flags: UnmountFlags) -> io::Result<()>; 19 | 20 | /// Upgrades `Self` into an `UnmountDrop`, which will unmount the mount when it is dropped. 21 | fn into_unmount_drop(self, flags: UnmountFlags) -> UnmountDrop 22 | where 23 | Self: Sized, 24 | { 25 | UnmountDrop { mount: self, flags } 26 | } 27 | } 28 | 29 | /// Unmounts the underlying mounted device upon drop. 30 | pub struct UnmountDrop { 31 | pub(crate) mount: T, 32 | pub(crate) flags: UnmountFlags, 33 | } 34 | 35 | impl UnmountDrop { 36 | /// Modify the previously-set unmount flags. 37 | pub fn set_unmount_flags(&mut self, flags: UnmountFlags) { 38 | self.flags = flags; 39 | } 40 | } 41 | 42 | impl Deref for UnmountDrop { 43 | type Target = T; 44 | 45 | fn deref(&self) -> &T { 46 | &self.mount 47 | } 48 | } 49 | 50 | impl Drop for UnmountDrop { 51 | fn drop(&mut self) { 52 | let _res = self.mount.unmount(self.flags); 53 | } 54 | } 55 | 56 | /// Unmounts the device at `path` using the provided `UnmountFlags`. 57 | /// 58 | /// This will not detach a loopback device if the mount was attached to one. This behavior may 59 | /// change in the future, once the [loopdev](https://crates.io/crates/loopdev) crate supports 60 | /// querying loopback device info. 61 | /// 62 | /// # Errors 63 | /// 64 | /// - If the path is not a valid C String 65 | /// - Or the unmount function fails 66 | /// 67 | /// # Example 68 | /// 69 | /// ```rust,no_run 70 | /// extern crate sys_mount; 71 | /// 72 | /// use sys_mount::{unmount, UnmountFlags}; 73 | /// 74 | /// fn main() { 75 | /// // Unmount device at `/target/path` lazily. 76 | /// let result = unmount("/target/path", UnmountFlags::DETACH); 77 | /// } 78 | /// ``` 79 | pub fn unmount>(path: P, flags: UnmountFlags) -> io::Result<()> { 80 | let mount = CString::new(path.as_ref().as_os_str().as_bytes().to_owned()); 81 | let mount_ptr = mount 82 | .as_ref() 83 | .ok() 84 | .map_or(ptr::null(), |cstr| cstr.as_ptr()); 85 | 86 | unsafe { unmount_(mount_ptr, flags) } 87 | } 88 | 89 | #[inline] 90 | pub(crate) unsafe fn unmount_(mount_ptr: *const c_char, flags: UnmountFlags) -> io::Result<()> { 91 | match umount2(mount_ptr, flags.bits()) { 92 | 0 => Ok(()), 93 | _err => Err(io::Error::last_os_error()), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/supported.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use std::{ 5 | fs::File, 6 | io::{self, BufRead, BufReader}, 7 | }; 8 | 9 | /// Data structure for validating if a filesystem argument is valid, and used within 10 | /// automatic file system mounting. 11 | #[derive(Clone, Debug)] 12 | #[allow(clippy::module_name_repetitions)] 13 | pub struct SupportedFilesystems { 14 | nodev: Vec, 15 | fs: Vec, 16 | } 17 | 18 | impl SupportedFilesystems { 19 | /// Detects available supported file systems on the system 20 | /// 21 | /// # Errors 22 | /// 23 | /// - On failure to open `/proc/filesystems` 24 | pub fn new() -> io::Result { 25 | let mut fss = Vec::with_capacity(64); 26 | let mut nodevs = Vec::with_capacity(64); 27 | 28 | for line in BufReader::new(File::open("/proc/filesystems")?).lines() { 29 | let line = line?; 30 | let mut line = line.split_whitespace(); 31 | let (nodev, fs) = match (line.next(), line.next()) { 32 | (Some(fs), None) => (false, fs), 33 | (Some("nodev"), Some(fs)) => (true, fs), 34 | _ => continue, 35 | }; 36 | 37 | nodevs.push(nodev); 38 | fss.push(fs.to_owned()); 39 | } 40 | 41 | Ok(SupportedFilesystems { 42 | nodev: nodevs, 43 | fs: fss, 44 | }) 45 | } 46 | 47 | /// Check if a provided file system is valid on this system. 48 | /// 49 | /// ```rust 50 | /// extern crate sys_mount; 51 | /// 52 | /// use sys_mount::SupportedFilesystems; 53 | /// 54 | /// fn main() { 55 | /// let supports = SupportedFilesystems::new().unwrap(); 56 | /// println!("btrfs is {}", if supports.is_supported("btrfs") { 57 | /// "supported" 58 | /// } else { 59 | /// "not supported" 60 | /// }); 61 | /// } 62 | /// ``` 63 | #[must_use] 64 | pub fn is_supported(&self, fs: &str) -> bool { 65 | self.fs.iter().any(|s| s.as_str() == fs) 66 | } 67 | 68 | /// Iterate through file systems which are not associated with physical devices. 69 | #[must_use] 70 | pub fn nodev_file_systems<'a>(&'a self) -> Box + 'a> { 71 | // TODO: When we can, switch to `impl Iterator`. 72 | let iter = self.nodev.iter().enumerate().filter_map(move |(id, &x)| { 73 | if x { 74 | Some(self.fs[id].as_str()) 75 | } else { 76 | None 77 | } 78 | }); 79 | 80 | Box::new(iter) 81 | } 82 | 83 | /// Iterate through file systems which are associated with physical devices. 84 | #[must_use] 85 | pub fn dev_file_systems<'a>(&'a self) -> Box + 'a> { 86 | // TODO: When we can, switch to `impl Iterator`. 87 | let iter = self.nodev.iter().enumerate().filter_map(move |(id, &x)| { 88 | if x { 89 | None 90 | } else { 91 | Some(self.fs[id].as_str()) 92 | } 93 | }); 94 | 95 | Box::new(iter) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/mount.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use crate::umount::{unmount_, Unmount, UnmountDrop}; 5 | use crate::{MountBuilder, UnmountFlags}; 6 | use std::{ 7 | ffi::{CString, OsStr}, 8 | io, 9 | os::unix::ffi::OsStrExt, 10 | path::Path, 11 | }; 12 | 13 | /// Handle for managing a mounted file system. 14 | #[derive(Debug)] 15 | pub struct Mount { 16 | pub(crate) target: CString, 17 | pub(crate) fstype: String, 18 | #[cfg(feature = "loop")] 19 | pub(crate) loopback: Option, 20 | pub(crate) loop_path: Option, 21 | } 22 | 23 | impl Unmount for Mount { 24 | fn unmount(&self, flags: UnmountFlags) -> io::Result<()> { 25 | unsafe { 26 | unmount_(self.target.as_ptr(), flags)?; 27 | } 28 | 29 | #[cfg(feature = "loop")] 30 | if let Some(ref loopback) = self.loopback { 31 | loopback.detach()?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | } 37 | 38 | impl Mount { 39 | /// Creates a [`MountBuilder`] for configuring a new mount. 40 | /// 41 | /// ```no_run 42 | /// use sys_mount::*; 43 | /// 44 | /// fn main() -> std::io::Result<()> { 45 | /// let _mount = Mount::builder() 46 | /// .fstype("btrfs") 47 | /// .data("subvol=@home") 48 | /// .mount("/dev/sda1", "/home")?; 49 | /// Ok(()) 50 | /// } 51 | /// ``` 52 | #[inline] 53 | #[must_use] 54 | pub fn builder<'a>() -> MountBuilder<'a> { 55 | MountBuilder::default() 56 | } 57 | 58 | /// Mounts the source device to the target path. 59 | /// 60 | /// Attempts to automatically detect the filesystem of the source device. 61 | /// 62 | /// For more flexibility, use [`Mount::builder`] instead. 63 | /// 64 | /// # Errors 65 | /// 66 | /// Errors if supported filesystems cannot be detected, or the mount fails. 67 | #[inline] 68 | pub fn new(source: impl AsRef, target: impl AsRef) -> io::Result { 69 | let supported = crate::SupportedFilesystems::new()?; 70 | MountBuilder::default() 71 | .fstype(&supported) 72 | .mount(source, target) 73 | } 74 | 75 | /// If the device was associated with a loopback device, that device's path 76 | /// can be retrieved here. 77 | #[inline] 78 | #[must_use] 79 | pub fn backing_loop_device(&self) -> Option<&Path> { 80 | self.loop_path.as_deref() 81 | } 82 | 83 | /// Describes the file system which this mount was mounted with. 84 | /// 85 | /// This is useful in the event that the mounted device was mounted automatically. 86 | #[inline] 87 | #[must_use] 88 | pub fn get_fstype(&self) -> &str { 89 | &self.fstype 90 | } 91 | 92 | /// Return the path this mount was mounted on. 93 | #[inline] 94 | #[must_use] 95 | pub fn target_path(&self) -> &Path { 96 | Path::new(OsStr::from_bytes(self.target.as_bytes())) 97 | } 98 | 99 | #[inline] 100 | pub(crate) fn from_target_and_fstype(target: CString, fstype: String) -> Self { 101 | Mount { 102 | target, 103 | fstype, 104 | #[cfg(feature = "loop")] 105 | loopback: None, 106 | loop_path: None, 107 | } 108 | } 109 | } 110 | 111 | /// An abstraction that will ensure that temporary mounts are dropped in reverse. 112 | pub struct Mounts(pub Vec>); 113 | 114 | impl Mounts { 115 | /// Unmounts all mounts, with the option to do so lazily. 116 | /// 117 | /// # Errors 118 | /// 119 | /// Returns on the first error when unmounting. 120 | pub fn unmount(&mut self, lazy: bool) -> io::Result<()> { 121 | let flags = if lazy { 122 | UnmountFlags::DETACH 123 | } else { 124 | UnmountFlags::empty() 125 | }; 126 | self.0 127 | .iter_mut() 128 | .rev() 129 | .try_for_each(|mount| mount.unmount(flags)) 130 | } 131 | } 132 | 133 | impl Drop for Mounts { 134 | fn drop(&mut self) { 135 | for mount in self.0.drain(..).rev() { 136 | drop(mount); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | //! High level abstraction over the `mount` and `umount2` system calls. 5 | //! 6 | //! If the `loop` feature is enabled (default), additionally supports creating loopback devices 7 | //! automatically when mounting an iso or squashfs file. 8 | //! 9 | //! # Example 10 | //! 11 | //! ```rust,no_run 12 | //! extern crate sys_mount; 13 | //! 14 | //! use std::process::exit; 15 | //! use sys_mount::{ 16 | //! Mount, 17 | //! MountFlags, 18 | //! SupportedFilesystems, 19 | //! Unmount, 20 | //! UnmountFlags 21 | //! }; 22 | //! 23 | //! fn main() { 24 | //! // Fetch a list of supported file systems. 25 | //! // When mounting, a file system will be selected from this. 26 | //! let supported = SupportedFilesystems::new().unwrap(); 27 | //! 28 | //! // Attempt to mount the src device to the dest directory. 29 | //! let mount_result = Mount::builder() 30 | //! .fstype("btrfs") 31 | //! .data("subvol=@home") 32 | //! .mount("/dev/sda1", "/home"); 33 | //! 34 | //! match mount_result { 35 | //! Ok(mount) => { 36 | //! // Make the mount temporary, so that it will be unmounted on drop. 37 | //! let mount = mount.into_unmount_drop(UnmountFlags::DETACH); 38 | //! // Do thing with the mounted device. 39 | //! } 40 | //! Err(why) => { 41 | //! eprintln!("failed to mount device: {}", why); 42 | //! exit(1); 43 | //! } 44 | //! } 45 | //! } 46 | 47 | extern crate libc; 48 | #[macro_use] 49 | extern crate bitflags; 50 | #[macro_use] 51 | extern crate thiserror; 52 | 53 | mod builder; 54 | mod flags; 55 | mod fstype; 56 | mod mount; 57 | mod supported; 58 | mod umount; 59 | 60 | pub use self::{builder::*, flags::*, fstype::*, mount::*, supported::*, umount::*}; 61 | 62 | use libc::swapoff as c_swapoff; 63 | use std::{ 64 | ffi::CString, 65 | io::{self, Error, ErrorKind}, 66 | os::unix::ffi::OsStrExt, 67 | path::Path, 68 | }; 69 | 70 | #[derive(Debug, Error)] 71 | pub enum ScopedMountError { 72 | #[error("cannot get list of supported file systems")] 73 | Supported(#[source] io::Error), 74 | #[error("could not mount partition")] 75 | Mount(#[source] io::Error), 76 | } 77 | 78 | /// Mount a partition temporarily for the duration of the scoped block within. 79 | /// 80 | /// # Errors 81 | /// 82 | /// - Fails if the supported file systems cannot be found. 83 | /// - Or if it fails to unmount 84 | pub fn scoped_mount T>( 85 | source: &Path, 86 | mount_at: &Path, 87 | scope: S, 88 | ) -> Result { 89 | let supported = SupportedFilesystems::new().map_err(ScopedMountError::Supported)?; 90 | 91 | Mount::builder() 92 | .fstype(&supported) 93 | .mount(source, mount_at) 94 | .map_err(ScopedMountError::Mount)?; 95 | 96 | let result = scope(); 97 | 98 | if let Err(why) = unmount(mount_at, UnmountFlags::empty()) { 99 | tracing::warn!("{}: failed to unmount: {}", mount_at.display(), why); 100 | } 101 | 102 | Ok(result) 103 | } 104 | 105 | /// Unmounts a swap partition using `libc::swapoff` 106 | /// 107 | /// # Errors 108 | /// 109 | /// - If the destination path is not a valid C String 110 | /// - Or the swapoff function fails 111 | pub fn swapoff>(dest: P) -> io::Result<()> { 112 | let Ok(swap) = CString::new(dest.as_ref().as_os_str().as_bytes().to_owned()) else { 113 | return Err(Error::new( 114 | ErrorKind::Other, 115 | format!( 116 | "swap path is not a valid c string: '{}'", 117 | dest.as_ref().display() 118 | ) 119 | )) 120 | }; 121 | 122 | match unsafe { c_swapoff(swap.as_ptr()) } { 123 | 0 => Ok(()), 124 | 125 | _err => Err(Error::new( 126 | ErrorKind::Other, 127 | format!( 128 | "failed to swapoff {}: {}", 129 | dest.as_ref().display(), 130 | Error::last_os_error() 131 | ), 132 | )), 133 | } 134 | } 135 | 136 | #[inline] 137 | fn to_cstring(data: &[u8]) -> io::Result { 138 | CString::new(data).map_err(|why| { 139 | io::Error::new( 140 | io::ErrorKind::InvalidData, 141 | format!("failed to create `CString`: {}", why), 142 | ) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /src/flags.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use libc::{ 5 | c_int, c_ulong, MNT_DETACH, MNT_EXPIRE, MNT_FORCE, MS_BIND, MS_DIRSYNC, MS_MANDLOCK, MS_MOVE, 6 | MS_NOATIME, MS_NODEV, MS_NODIRATIME, MS_NOEXEC, MS_NOSUID, MS_RDONLY, MS_REC, MS_RELATIME, 7 | MS_REMOUNT, MS_SILENT, MS_STRICTATIME, MS_SYNCHRONOUS, O_NOFOLLOW, 8 | }; 9 | 10 | bitflags! { 11 | /// Flags which may be specified when mounting a file system. 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 13 | pub struct MountFlags: c_ulong { 14 | /// Perform a bind mount, making a file or a directory subtree visible at another 15 | /// point within a file system. Bind mounts may cross file system boundaries and 16 | /// span chroot(2) jails. The filesystemtype and data arguments are ignored. Up 17 | /// until Linux 2.6.26, mountflags was also ignored (the bind mount has the same 18 | /// mount options as the underlying mount point). 19 | const BIND = MS_BIND; 20 | 21 | /// Make directory changes on this file system synchronous.(This property can be 22 | /// obtained for individual directories or subtrees using chattr(1).) 23 | const DIRSYNC = MS_DIRSYNC; 24 | 25 | /// Permit mandatory locking on files in this file system. (Mandatory locking must 26 | /// still be enabled on a per-file basis, as described in fcntl(2).) 27 | const MANDLOCK = MS_MANDLOCK; 28 | 29 | /// Move a subtree. source specifies an existing mount point and target specifies 30 | /// the new location. The move is atomic: at no point is the subtree unmounted. 31 | /// The filesystemtype, mountflags, and data arguments are ignored. 32 | const MOVE = MS_MOVE; 33 | 34 | /// Do not update access times for (all types of) files on this file system. 35 | const NOATIME = MS_NOATIME; 36 | 37 | /// Do not allow access to devices (special files) on this file system. 38 | const NODEV = MS_NODEV; 39 | 40 | /// Do not update access times for directories on this file system. This flag provides 41 | /// a subset of the functionality provided by MS_NOATIME; that is, MS_NOATIME implies 42 | /// MS_NODIRATIME. 43 | const NODIRATIME = MS_NODIRATIME; 44 | 45 | /// Do not allow programs to be executed from this file system. 46 | const NOEXEC = MS_NOEXEC; 47 | 48 | /// Do not honor set-user-ID and set-group-ID bits when executing programs from this 49 | /// file system. 50 | const NOSUID = MS_NOSUID; 51 | 52 | /// Mount file system read-only. 53 | const RDONLY = MS_RDONLY; 54 | 55 | /// Used in conjunction with MS_BIND to create a recursive bind mount, and in 56 | /// conjunction with the propagation type flags to recursively change the propagation 57 | /// type of all of the mounts in a subtree. 58 | const REC = MS_REC; 59 | 60 | /// When a file on this file system is accessed, only update the file's last access 61 | /// time (atime) if the current value of atime is less than or equal to the file's 62 | /// last modification time (mtime) or last status change time (ctime). This option is 63 | /// useful for programs, such as mutt(1), that need to know when a file has been read 64 | /// since it was last modified. Since Linux 2.6.30, the kernel defaults to the behavior 65 | /// provided by this flag (unless MS_NOATIME was specified), and the MS_STRICTATIME 66 | /// flag is required to obtain traditional semantics. In addition, since Linux 2.6.30, 67 | /// the file's last access time is always updated if it is more than 1 day old. 68 | const RELATIME = MS_RELATIME; 69 | 70 | /// Remount an existing mount. This allows you to change the mountflags and data of an 71 | /// existing mount without having to unmount and remount the file system. target should 72 | /// be the same value specified in the initial mount() call; source and filesystemtype 73 | /// are ignored. 74 | /// 75 | /// The following mountflags can be changed: MS_RDONLY, MS_SYNCHRONOUS, MS_MANDLOCK; 76 | /// before kernel 2.6.16, the following could also be changed: MS_NOATIME and 77 | /// MS_NODIRATIME; and, additionally, before kernel 2.4.10, the following could also 78 | /// be changed: MS_NOSUID, MS_NODEV, MS_NOEXEC. 79 | const REMOUNT = MS_REMOUNT; 80 | 81 | /// Suppress the display of certain (printk()) warning messages in the kernel log. 82 | /// This flag supersedes the misnamed and obsolete MS_VERBOSE flag (available 83 | /// since Linux 2.4.12), which has the same meaning. 84 | const SILENT = MS_SILENT; 85 | 86 | /// Always update the last access time (atime) when files on this file system are 87 | /// accessed. (This was the default behavior before Linux 2.6.30.) Specifying this 88 | /// flag overrides the effect of setting the MS_NOATIME and MS_RELATIME flags. 89 | const STRICTATIME = MS_STRICTATIME; 90 | 91 | /// Make writes on this file system synchronous (as though the O_SYNC flag to 92 | /// open(2) was specified for all file opens to this file system). 93 | const SYNCHRONOUS = MS_SYNCHRONOUS; 94 | } 95 | } 96 | 97 | bitflags! { 98 | /// Flags which may be specified when unmounting a file system. 99 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 100 | pub struct UnmountFlags: c_int { 101 | /// Force unmount even if busy. This can cause data loss. (Only for NFS mounts.) 102 | const FORCE = MNT_FORCE; 103 | 104 | /// Perform a lazy unmount: make the mount point unavailable for new accesses, 105 | /// and actually perform the unmount when the mount point ceases to be busy. 106 | const DETACH = MNT_DETACH; 107 | 108 | /// Mark the mount point as expired. If a mount point is not currently in use, 109 | /// then an initial call to umount2() with this flag fails with the error EAGAIN, 110 | /// but marks the mount point as expired. The mount point remains expired as 111 | /// long as it isn't accessed by any process. A second umount2() call specifying 112 | /// MNT_EXPIRE unmounts an expired mount point. This flag cannot be specified with 113 | /// either MNT_FORCE or MNT_DETACH. 114 | const EXPIRE = MNT_EXPIRE; 115 | 116 | /// Don't dereference target if it is a symbolic link. This flag allows security 117 | /// problems to be avoided in set-user-ID-root programs that allow unprivileged 118 | /// users to unmount file systems. 119 | const NOFOLLOW = O_NOFOLLOW; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 System76 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use super::to_cstring; 5 | use crate::{ 6 | io, libc, CString, FilesystemType, Mount, MountFlags, OsStrExt, Path, SupportedFilesystems, 7 | Unmount, UnmountDrop, UnmountFlags, 8 | }; 9 | use libc::mount; 10 | use std::ptr; 11 | 12 | /// Builder API for mounting devices 13 | /// 14 | /// ```no_run 15 | /// use sys_mount::*; 16 | /// 17 | /// fn main() -> std::io::Result<()> { 18 | /// let _mount = Mount::builder() 19 | /// .fstype("btrfs") 20 | /// .data("subvol=@home") 21 | /// .mount("/dev/sda1", "/home")?; 22 | /// Ok(()) 23 | /// } 24 | /// ``` 25 | #[derive(Clone, Copy, smart_default::SmartDefault)] 26 | #[allow(clippy::module_name_repetitions)] 27 | pub struct MountBuilder<'a> { 28 | #[default(MountFlags::empty())] 29 | flags: MountFlags, 30 | fstype: Option>, 31 | #[cfg(feature = "loop")] 32 | loopback_offset: u64, 33 | #[cfg(feature = "loop")] 34 | explicit_loopback: bool, 35 | data: Option<&'a str>, 36 | } 37 | 38 | impl<'a> MountBuilder<'a> { 39 | /// Options to apply for the file system on mount. 40 | #[must_use] 41 | pub fn data(mut self, data: &'a str) -> Self { 42 | self.data = Some(data); 43 | self 44 | } 45 | 46 | /// The file system that is to be mounted. 47 | #[must_use] 48 | pub fn fstype(mut self, fs: impl Into>) -> Self { 49 | self.fstype = Some(fs.into()); 50 | self 51 | } 52 | 53 | /// Mount flags for the mount syscall. 54 | #[must_use] 55 | pub fn flags(mut self, flags: MountFlags) -> Self { 56 | self.flags = flags; 57 | self 58 | } 59 | 60 | /// Offset for the loopback device 61 | #[cfg(feature = "loop")] 62 | #[must_use] 63 | pub fn loopback_offset(mut self, offset: u64) -> Self { 64 | self.loopback_offset = offset; 65 | self 66 | } 67 | 68 | /// Use loopback even if not an iso or squashfs is being mounted 69 | #[cfg(feature = "loop")] 70 | #[must_use] 71 | pub fn explicit_loopback(mut self) -> Self { 72 | self.explicit_loopback = true; 73 | self 74 | } 75 | 76 | /// Mounts a file system at `source` to a `target` path in the system. 77 | /// 78 | /// ```rust,no_run 79 | /// use sys_mount::{ 80 | /// Mount, 81 | /// MountFlags, 82 | /// SupportedFilesystems 83 | /// }; 84 | /// 85 | /// // Fetch a list of supported file systems. 86 | /// // When mounting, a file system will be selected from this. 87 | /// let supported = SupportedFilesystems::new().unwrap(); 88 | /// 89 | /// // Attempt to mount the src device to the dest directory. 90 | /// let mount_result = Mount::builder() 91 | /// .fstype(&supported) 92 | /// .mount("/imaginary/block/device", "/tmp/location"); 93 | /// ``` 94 | /// # Notes 95 | /// 96 | /// The provided `source` device and `target` destinations must exist within the file system. 97 | /// 98 | /// If the `source` is a file with an extension, a loopback device will be created, and the 99 | /// file will be associated with the loopback device. If the extension is `iso` or `squashfs`, 100 | /// the filesystem type will be set accordingly, and the `MountFlags` will also be modified to 101 | /// ensure that the `MountFlags::RDONLY` flag is set before mounting. 102 | /// 103 | /// The `fstype` parameter accepts either a `&str` or `&SupportedFilesystem` as input. If the 104 | /// input is a `&str`, then a particular file system will be used to mount the `source` with. 105 | /// If the input is a `&SupportedFilesystems`, then the file system will be selected 106 | /// automatically from the list. 107 | /// 108 | /// The automatic variant of `fstype` works by attempting to mount the `source` with all 109 | /// supported device-based file systems until it succeeds, or fails after trying all 110 | /// possible options. 111 | /// 112 | /// # Errors 113 | /// 114 | /// - If a fstype is not defined and supported filesystems cannot be detected 115 | /// - If a loopback device cannot be created 116 | /// - If the source or target are not valid C strings 117 | /// - If mounting fails 118 | pub fn mount(self, source: impl AsRef, target: impl AsRef) -> io::Result { 119 | let MountBuilder { 120 | data, 121 | fstype, 122 | flags, 123 | #[cfg(feature = "loop")] 124 | loopback_offset, 125 | #[cfg(feature = "loop")] 126 | explicit_loopback, 127 | } = self; 128 | 129 | let supported; 130 | 131 | let fstype = if let Some(fstype) = fstype { 132 | fstype 133 | } else { 134 | supported = SupportedFilesystems::new()?; 135 | FilesystemType::Auto(&supported) 136 | }; 137 | 138 | let source = source.as_ref(); 139 | let mut c_source = None; 140 | 141 | #[cfg(feature = "loop")] 142 | let (mut flags, mut fstype, mut loopback, mut loop_path) = (flags, fstype, None, None); 143 | 144 | if !source.as_os_str().is_empty() { 145 | #[cfg(feature = "loop")] 146 | let mut create_loopback = |flags: &MountFlags| -> io::Result { 147 | let new_loopback = loopdev::LoopControl::open()?.next_free()?; 148 | new_loopback 149 | .with() 150 | .read_only(flags.contains(MountFlags::RDONLY)) 151 | .offset(loopback_offset) 152 | .attach(source)?; 153 | let path = new_loopback.path().expect("loopback does not have path"); 154 | c_source = Some(to_cstring(path.as_os_str().as_bytes())?); 155 | loop_path = Some(path); 156 | Ok(new_loopback) 157 | }; 158 | 159 | // Create a loopback device if an iso or squashfs is being mounted. 160 | #[cfg(feature = "loop")] 161 | if let Some(ext) = source.extension() { 162 | let extf = i32::from(ext == "iso") | if ext == "squashfs" { 2 } else { 0 }; 163 | 164 | if extf != 0 { 165 | fstype = if extf == 1 { 166 | flags |= MountFlags::RDONLY; 167 | FilesystemType::Manual("iso9660") 168 | } else { 169 | flags |= MountFlags::RDONLY; 170 | FilesystemType::Manual("squashfs") 171 | }; 172 | loopback = Some(create_loopback(&flags)?); 173 | } 174 | } 175 | 176 | #[cfg(feature = "loop")] 177 | if loopback.is_none() && explicit_loopback { 178 | loopback = Some(create_loopback(&flags)?); 179 | } 180 | 181 | if c_source.is_none() { 182 | c_source = Some(to_cstring(source.as_os_str().as_bytes())?); 183 | } 184 | }; 185 | 186 | let c_target = to_cstring(target.as_ref().as_os_str().as_bytes())?; 187 | let data = match data.map(|o| to_cstring(o.as_bytes())) { 188 | Some(Ok(string)) => Some(string), 189 | Some(Err(why)) => return Err(why), 190 | None => None, 191 | }; 192 | 193 | let mut mount_data = MountData { 194 | c_source, 195 | c_target, 196 | flags, 197 | data, 198 | }; 199 | 200 | let mut res = match fstype { 201 | FilesystemType::Auto(supported) => mount_data.automount(supported.dev_file_systems()), 202 | FilesystemType::Set(set) => mount_data.automount(set.iter().copied()), 203 | FilesystemType::Manual(fstype) => mount_data.mount(fstype), 204 | }; 205 | 206 | match res { 207 | Ok(ref mut _mount) => { 208 | #[cfg(feature = "loop")] 209 | { 210 | _mount.loopback = loopback; 211 | _mount.loop_path = loop_path; 212 | } 213 | } 214 | Err(_) => 215 | { 216 | #[cfg(feature = "loop")] 217 | if let Some(loopback) = loopback { 218 | let _res = loopback.detach(); 219 | } 220 | } 221 | } 222 | 223 | res 224 | } 225 | 226 | /// Perform a mount which auto-unmounts on drop. 227 | /// 228 | /// # Errors 229 | /// 230 | /// On failure to mount 231 | pub fn mount_autodrop( 232 | self, 233 | source: impl AsRef, 234 | target: impl AsRef, 235 | unmount_flags: UnmountFlags, 236 | ) -> io::Result> { 237 | self.mount(source, target) 238 | .map(|m| m.into_unmount_drop(unmount_flags)) 239 | } 240 | } 241 | 242 | struct MountData { 243 | c_source: Option, 244 | c_target: CString, 245 | flags: MountFlags, 246 | data: Option, 247 | } 248 | 249 | impl MountData { 250 | fn mount(&mut self, fstype: &str) -> io::Result { 251 | let c_fstype = to_cstring(fstype.as_bytes())?; 252 | match mount_( 253 | self.c_source.as_ref(), 254 | &self.c_target, 255 | &c_fstype, 256 | self.flags, 257 | self.data.as_ref(), 258 | ) { 259 | Ok(()) => Ok(Mount::from_target_and_fstype( 260 | self.c_target.clone(), 261 | fstype.to_owned(), 262 | )), 263 | Err(why) => Err(why), 264 | } 265 | } 266 | 267 | fn automount<'a, I: Iterator + 'a>(mut self, iter: I) -> io::Result { 268 | let mut res = Ok(()); 269 | 270 | for fstype in iter { 271 | match self.mount(fstype) { 272 | mount @ Ok(_) => return mount, 273 | Err(why) => res = Err(why), 274 | } 275 | } 276 | 277 | match res { 278 | Ok(()) => Err(io::Error::new( 279 | io::ErrorKind::NotFound, 280 | "no supported file systems found", 281 | )), 282 | Err(why) => Err(why), 283 | } 284 | } 285 | } 286 | 287 | fn mount_( 288 | c_source: Option<&CString>, 289 | c_target: &CString, 290 | c_fstype: &CString, 291 | flags: MountFlags, 292 | c_data: Option<&CString>, 293 | ) -> io::Result<()> { 294 | let result = unsafe { 295 | mount( 296 | c_source.map_or_else(ptr::null, |s| s.as_ptr()), 297 | c_target.as_ptr(), 298 | c_fstype.as_ptr(), 299 | flags.bits(), 300 | c_data 301 | .map_or_else(ptr::null, |s| s.as_ptr()) 302 | .cast::(), 303 | ) 304 | }; 305 | 306 | match result { 307 | 0 => Ok(()), 308 | _err => Err(io::Error::last_os_error()), 309 | } 310 | } 311 | --------------------------------------------------------------------------------