sys_mount/
builder.rs

1// Copyright 2018-2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::to_cstring;
5use crate::{
6    io, libc, CString, FilesystemType, Mount, MountFlags, OsStrExt, Path, SupportedFilesystems,
7    Unmount, UnmountDrop, UnmountFlags,
8};
9use libc::mount;
10use 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)]
27pub struct MountBuilder<'a> {
28    #[default(MountFlags::empty())]
29    flags: MountFlags,
30    fstype: Option<FilesystemType<'a>>,
31    #[cfg(feature = "loop")]
32    loopback_offset: u64,
33    #[cfg(feature = "loop")]
34    explicit_loopback: bool,
35    data: Option<&'a str>,
36}
37
38impl<'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<FilesystemType<'a>>) -> 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<Path>, target: impl AsRef<Path>) -> io::Result<Mount> {
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<loopdev::LoopDevice> {
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                }
173
174                loopback = Some(create_loopback(&flags)?);
175            }
176
177            #[cfg(feature = "loop")]
178            if loopback.is_none() && explicit_loopback {
179                loopback = Some(create_loopback(&flags)?);
180            }
181
182            if c_source.is_none() {
183                c_source = Some(to_cstring(source.as_os_str().as_bytes())?);
184            }
185        };
186
187        let c_target = to_cstring(target.as_ref().as_os_str().as_bytes())?;
188        let data = match data.map(|o| to_cstring(o.as_bytes())) {
189            Some(Ok(string)) => Some(string),
190            Some(Err(why)) => return Err(why),
191            None => None,
192        };
193
194        let mut mount_data = MountData {
195            c_source,
196            c_target,
197            flags,
198            data,
199        };
200
201        let mut res = match fstype {
202            FilesystemType::Auto(supported) => mount_data.automount(supported.dev_file_systems()),
203            FilesystemType::Set(set) => mount_data.automount(set.iter().copied()),
204            FilesystemType::Manual(fstype) => mount_data.mount(fstype),
205        };
206
207        match res {
208            Ok(ref mut _mount) => {
209                #[cfg(feature = "loop")]
210                {
211                    _mount.loopback = loopback;
212                    _mount.loop_path = loop_path;
213                }
214            }
215            Err(_) =>
216            {
217                #[cfg(feature = "loop")]
218                if let Some(loopback) = loopback {
219                    let _res = loopback.detach();
220                }
221            }
222        }
223
224        res
225    }
226
227    /// Perform a mount which auto-unmounts on drop.
228    ///
229    /// # Errors
230    ///
231    /// On failure to mount
232    pub fn mount_autodrop(
233        self,
234        source: impl AsRef<Path>,
235        target: impl AsRef<Path>,
236        unmount_flags: UnmountFlags,
237    ) -> io::Result<UnmountDrop<Mount>> {
238        self.mount(source, target)
239            .map(|m| m.into_unmount_drop(unmount_flags))
240    }
241}
242
243struct MountData {
244    c_source: Option<CString>,
245    c_target: CString,
246    flags: MountFlags,
247    data: Option<CString>,
248}
249
250impl MountData {
251    fn mount(&mut self, fstype: &str) -> io::Result<Mount> {
252        let c_fstype = to_cstring(fstype.as_bytes())?;
253        match mount_(
254            self.c_source.as_ref(),
255            &self.c_target,
256            &c_fstype,
257            self.flags,
258            self.data.as_ref(),
259        ) {
260            Ok(()) => Ok(Mount::from_target_and_fstype(
261                self.c_target.clone(),
262                fstype.to_owned(),
263            )),
264            Err(why) => Err(why),
265        }
266    }
267
268    fn automount<'a, I: Iterator<Item = &'a str> + 'a>(mut self, iter: I) -> io::Result<Mount> {
269        let mut res = Ok(());
270
271        for fstype in iter {
272            match self.mount(fstype) {
273                mount @ Ok(_) => return mount,
274                Err(why) => res = Err(why),
275            }
276        }
277
278        match res {
279            Ok(()) => Err(io::Error::new(
280                io::ErrorKind::NotFound,
281                "no supported file systems found",
282            )),
283            Err(why) => Err(why),
284        }
285    }
286}
287
288fn mount_(
289    c_source: Option<&CString>,
290    c_target: &CString,
291    c_fstype: &CString,
292    flags: MountFlags,
293    c_data: Option<&CString>,
294) -> io::Result<()> {
295    let result = unsafe {
296        mount(
297            c_source.map_or_else(ptr::null, |s| s.as_ptr()),
298            c_target.as_ptr(),
299            c_fstype.as_ptr(),
300            flags.bits(),
301            c_data
302                .map_or_else(ptr::null, |s| s.as_ptr())
303                .cast::<libc::c_void>(),
304        )
305    };
306
307    match result {
308        0 => Ok(()),
309        _err => Err(io::Error::last_os_error()),
310    }
311}