Skip to main content

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                    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<Path>,
234        target: impl AsRef<Path>,
235        unmount_flags: UnmountFlags,
236    ) -> io::Result<UnmountDrop<Mount>> {
237        self.mount(source, target)
238            .map(|m| m.into_unmount_drop(unmount_flags))
239    }
240}
241
242struct MountData {
243    c_source: Option<CString>,
244    c_target: CString,
245    flags: MountFlags,
246    data: Option<CString>,
247}
248
249impl MountData {
250    fn mount(&mut self, fstype: &str) -> io::Result<Mount> {
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<Item = &'a str> + 'a>(mut self, iter: I) -> io::Result<Mount> {
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
287fn 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::<libc::c_void>(),
303        )
304    };
305
306    match result {
307        0 => Ok(()),
308        _err => Err(io::Error::last_os_error()),
309    }
310}