loopdev_erikh/
lib.rs

1// Supress warnings generated by bindgen: https://github.com/rust-lang/rust-bindgen/issues/1651
2#![allow(deref_nullptr)]
3#![allow(unknown_lints)]
4
5//! Setup and control loop devices.
6//!
7//! Provides rust interface with similar functionality to the Linux utility `losetup`.
8//!
9//! # Examples
10//!
11//! Default options:
12//!
13//! ```no_run
14//! use loopdev::LoopControl;
15//! let lc = LoopControl::open().unwrap();
16//! let ld = lc.next_free().unwrap();
17//!
18//! println!("{}", ld.path().unwrap().display());
19//!
20//! ld.attach_file("disk.img").unwrap();
21//! // ...
22//! ld.detach().unwrap();
23//! ```
24//!
25//! Custom options:
26//!
27//! ```no_run
28//! # use loopdev::LoopControl;
29//! # let lc = LoopControl::open().unwrap();
30//! # let ld = lc.next_free().unwrap();
31//! #
32//! ld.with()
33//!     .part_scan(true)
34//!     .offset(512 * 1024 * 1024) // 512 MiB
35//!     .size_limit(1024 * 1024 * 1024) // 1GiB
36//!     .attach("disk.img").unwrap();
37//! // ...
38//! ld.detach().unwrap();
39//! ```
40use crate::bindings::{
41    loop_info64, LOOP_CLR_FD, LOOP_CTL_ADD, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_FD,
42    LOOP_SET_STATUS64, LO_FLAGS_AUTOCLEAR, LO_FLAGS_PARTSCAN, LO_FLAGS_READ_ONLY,
43};
44#[cfg(feature = "direct_io")]
45use bindings::LOOP_SET_DIRECT_IO;
46use libc::{c_int, ioctl};
47use std::{
48    default::Default,
49    fs::{File, OpenOptions},
50    io,
51    os::unix::prelude::*,
52    path::{Path, PathBuf},
53};
54
55#[allow(non_camel_case_types)]
56#[allow(dead_code)]
57#[allow(non_snake_case)]
58mod bindings {
59    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
60}
61
62#[cfg(all(not(target_os = "android"), not(target_env = "musl")))]
63type IoctlRequest = libc::c_ulong;
64#[cfg(any(target_os = "android", target_env = "musl"))]
65type IoctlRequest = libc::c_int;
66
67const LOOP_CONTROL: &str = "/dev/loop-control";
68#[cfg(not(target_os = "android"))]
69const LOOP_PREFIX: &str = "/dev/loop";
70#[cfg(target_os = "android")]
71const LOOP_PREFIX: &str = "/dev/block/loop";
72
73/// Interface to the loop control device: `/dev/loop-control`.
74#[derive(Debug)]
75pub struct LoopControl {
76    dev_file: File,
77}
78
79impl LoopControl {
80    /// Opens the loop control device.
81    ///
82    /// # Errors
83    ///
84    /// This function will return an error for various reasons when opening
85    /// the loop control file `/dev/loop-control`. See
86    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
87    /// for further details.
88    pub fn open() -> io::Result<Self> {
89        Ok(Self {
90            dev_file: OpenOptions::new()
91                .read(true)
92                .write(true)
93                .open(LOOP_CONTROL)?,
94        })
95    }
96
97    /// Finds and opens the next available loop device.
98    ///
99    /// # Examples
100    ///
101    /// ```no_run
102    /// use loopdev::LoopControl;
103    /// let lc = LoopControl::open().unwrap();
104    /// let ld = lc.next_free().unwrap();
105    /// println!("{}", ld.path().unwrap().display());
106    /// ```
107    ///
108    /// # Errors
109    ///
110    /// This function will return an error for various reasons when opening
111    /// the loop device file `/dev/loopX`. See
112    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
113    /// for further details.
114    pub fn next_free(&self) -> io::Result<LoopDevice> {
115        let dev_num = ioctl_to_error(unsafe {
116            ioctl(
117                self.dev_file.as_raw_fd() as c_int,
118                LOOP_CTL_GET_FREE as IoctlRequest,
119            )
120        })?;
121        LoopDevice::open(format!("{}{}", LOOP_PREFIX, dev_num))
122    }
123
124    /// Add and opens a new loop device.
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use loopdev::LoopControl;
130    /// let lc = LoopControl::open().unwrap();
131    /// let ld = lc.add(1).unwrap();
132    /// println!("{}", ld.path().unwrap().display());
133    /// ```
134    ///
135    /// # Errors
136    ///
137    /// This funcitons will return an error when a loop device with the passed
138    /// number exists or opening the newly created device fails.
139    pub fn add(&self, n: u32) -> io::Result<LoopDevice> {
140        let dev_num = ioctl_to_error(unsafe {
141            ioctl(
142                self.dev_file.as_raw_fd() as c_int,
143                LOOP_CTL_ADD as IoctlRequest,
144                n as c_int,
145            )
146        })?;
147        LoopDevice::open(format!("{}{}", LOOP_PREFIX, dev_num))
148    }
149}
150
151impl AsRawFd for LoopControl {
152    fn as_raw_fd(&self) -> RawFd {
153        self.dev_file.as_raw_fd()
154    }
155}
156
157impl IntoRawFd for LoopControl {
158    fn into_raw_fd(self) -> RawFd {
159        self.dev_file.into_raw_fd()
160    }
161}
162
163/// Interface to a loop device ie `/dev/loop0`.
164#[derive(Debug)]
165pub struct LoopDevice {
166    device: File,
167}
168
169impl AsRawFd for LoopDevice {
170    fn as_raw_fd(&self) -> RawFd {
171        self.device.as_raw_fd()
172    }
173}
174
175impl IntoRawFd for LoopDevice {
176    fn into_raw_fd(self) -> RawFd {
177        self.device.into_raw_fd()
178    }
179}
180
181impl LoopDevice {
182    /// Opens a loop device.
183    ///
184    /// # Errors
185    ///
186    /// This function will return an error for various reasons when opening
187    /// the given loop device file. See
188    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
189    /// for further details.
190    pub fn open<P: AsRef<Path>>(dev: P) -> io::Result<Self> {
191        // TODO create dev if it does not exist and begins with LOOP_PREFIX
192        Ok(Self {
193            device: OpenOptions::new().read(true).write(true).open(dev)?,
194        })
195    }
196
197    /// Attach the loop device to a file with given options.
198    ///
199    /// # Examples
200    ///
201    /// Attach the device to a file.
202    ///
203    /// ```no_run
204    /// use loopdev::LoopDevice;
205    /// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
206    /// ld.with().part_scan(true).attach("disk.img").unwrap();
207    /// # ld.detach().unwrap();
208    /// ```
209    pub fn with(&self) -> AttachOptions<'_> {
210        AttachOptions {
211            device: self,
212            info: bindings::loop_info64::default(),
213            #[cfg(feature = "direct_io")]
214            direct_io: false,
215        }
216    }
217
218    /// Attach the loop device to a file that maps to the whole file.
219    ///
220    /// # Examples
221    ///
222    /// Attach the device to a file.
223    ///
224    /// ```no_run
225    /// use loopdev::LoopDevice;
226    /// let ld = LoopDevice::open("/dev/loop0").unwrap();
227    /// ld.attach_file("disk.img").unwrap();
228    /// # ld.detach().unwrap();
229    /// ```
230    ///
231    /// # Errors
232    ///
233    /// This function will return an error for various reasons. Either when
234    /// opening the backing file (see
235    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
236    /// for further details) or when calling the ioctl to attach the backing
237    /// file to the device.
238    pub fn attach_file<P: AsRef<Path>>(&self, backing_file: P) -> io::Result<()> {
239        let info = loop_info64 {
240            ..Default::default()
241        };
242
243        Self::attach_with_loop_info(self, backing_file, info)
244    }
245
246    /// Attach the loop device to a file with `loop_info64`.
247    fn attach_with_loop_info(
248        &self, // TODO should be mut? - but changing it is a breaking change
249        backing_file: impl AsRef<Path>,
250        info: loop_info64,
251    ) -> io::Result<()> {
252        let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0;
253        let bf = OpenOptions::new()
254            .read(true)
255            .write(write_access)
256            .open(backing_file)?;
257        self.attach_fd_with_loop_info(bf, info)
258    }
259
260    /// Attach the loop device to a fd with `loop_info`.
261    fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: loop_info64) -> io::Result<()> {
262        // Attach the file
263        ioctl_to_error(unsafe {
264            ioctl(
265                self.device.as_raw_fd() as c_int,
266                LOOP_SET_FD as IoctlRequest,
267                bf.as_raw_fd() as c_int,
268            )
269        })?;
270
271        let result = unsafe {
272            ioctl(
273                self.device.as_raw_fd() as c_int,
274                LOOP_SET_STATUS64 as IoctlRequest,
275                &info,
276            )
277        };
278        match ioctl_to_error(result) {
279            Err(err) => {
280                // Ignore the error to preserve the original error
281                let _detach_err = self.detach();
282                Err(err)
283            }
284            Ok(_) => Ok(()),
285        }
286    }
287
288    /// Get the path of the loop device.
289    pub fn path(&self) -> Option<PathBuf> {
290        let mut p = PathBuf::from("/proc/self/fd");
291        p.push(self.device.as_raw_fd().to_string());
292        std::fs::read_link(&p).ok()
293    }
294
295    /// Get the device major number
296    ///
297    /// # Errors
298    ///
299    /// This function needs to stat the backing file and can fail if there is
300    /// an IO error.
301    #[allow(clippy::unnecessary_cast)]
302    pub fn major(&self) -> io::Result<u32> {
303        self.device
304            .metadata()
305            .map(|m| unsafe { libc::major(m.rdev()) })
306            .map(|m| m as u32)
307    }
308
309    /// Get the device major number
310    ///
311    /// # Errors
312    ///
313    /// This function needs to stat the backing file and can fail if there is
314    /// an IO error.
315    #[allow(clippy::unnecessary_cast)]
316    pub fn minor(&self) -> io::Result<u32> {
317        self.device
318            .metadata()
319            .map(|m| unsafe { libc::minor(m.rdev()) })
320            .map(|m| m as u32)
321    }
322
323    /// Detach a loop device from its backing file.
324    ///
325    /// Note that the device won't fully detach until a short delay after the underling device file
326    /// gets closed. This happens when `LoopDev` goes out of scope so you should ensure the `LoopDev`
327    /// lives for a short a time as possible.
328    ///
329    /// # Examples
330    ///
331    /// ```no_run
332    /// use loopdev::LoopDevice;
333    /// let ld = LoopDevice::open("/dev/loop0").unwrap();
334    /// # ld.attach_file("disk.img").unwrap();
335    /// ld.detach().unwrap();
336    /// ```
337    ///
338    /// # Errors
339    ///
340    /// This function will return an error for various reasons when calling the
341    /// ioctl to detach the backing file from the device.
342    pub fn detach(&self) -> io::Result<()> {
343        ioctl_to_error(unsafe {
344            ioctl(
345                self.device.as_raw_fd() as c_int,
346                LOOP_CLR_FD as IoctlRequest,
347                0,
348            )
349        })?;
350        Ok(())
351    }
352
353    /// Resize a live loop device. If the size of the backing file changes this can be called to
354    /// inform the loop driver about the new size.
355    ///
356    /// # Errors
357    ///
358    /// This function will return an error for various reasons when calling the
359    /// ioctl to set the capacity of the device.
360    pub fn set_capacity(&self) -> io::Result<()> {
361        ioctl_to_error(unsafe {
362            ioctl(
363                self.device.as_raw_fd() as c_int,
364                LOOP_SET_CAPACITY as IoctlRequest,
365                0,
366            )
367        })?;
368        Ok(())
369    }
370
371    /// Enable or disable direct I/O for the backing file.
372    ///
373    /// # Errors
374    ///
375    /// This function will return an error for various reasons when calling the
376    /// ioctl to set the direct io flag for the device.
377    #[cfg(feature = "direct_io")]
378    pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
379        ioctl_to_error(unsafe {
380            ioctl(
381                self.device.as_raw_fd() as c_int,
382                LOOP_SET_DIRECT_IO as IoctlRequest,
383                if direct_io { 1 } else { 0 },
384            )
385        })?;
386        Ok(())
387    }
388}
389
390/// Used to set options when attaching a device. Created with [`LoopDevice::with`()].
391///
392/// # Examples
393///
394/// Enable partition scanning on attach:
395///
396/// ```no_run
397/// use loopdev::LoopDevice;
398/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
399/// ld.with()
400///     .part_scan(true)
401///     .attach("disk.img")
402///     .unwrap();
403/// # ld.detach().unwrap();
404/// ```
405///
406/// A 1MiB slice of the file located at 1KiB into the file.
407///
408/// ```no_run
409/// use loopdev::LoopDevice;
410/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
411/// ld.with()
412///     .offset(1024*1024)
413///     .size_limit(1024*1024*1024)
414///     .attach("disk.img")
415///     .unwrap();
416/// # ld.detach().unwrap();
417/// ```
418#[must_use]
419pub struct AttachOptions<'d> {
420    device: &'d LoopDevice,
421    info: loop_info64,
422    #[cfg(feature = "direct_io")]
423    direct_io: bool,
424}
425
426impl AttachOptions<'_> {
427    /// Offset in bytes from the start of the backing file the data will start at.
428    pub fn offset(mut self, offset: u64) -> Self {
429        self.info.lo_offset = offset;
430        self
431    }
432
433    /// Maximum size of the data in bytes.
434    pub fn size_limit(mut self, size_limit: u64) -> Self {
435        self.info.lo_sizelimit = size_limit;
436        self
437    }
438
439    /// Set read only flag
440    pub fn read_only(mut self, read_only: bool) -> Self {
441        if read_only {
442            self.info.lo_flags |= LO_FLAGS_READ_ONLY;
443        } else {
444            self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
445        }
446        self
447    }
448
449    /// Set autoclear flag
450    pub fn autoclear(mut self, autoclear: bool) -> Self {
451        if autoclear {
452            self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
453        } else {
454            self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
455        }
456        self
457    }
458
459    // Enable or disable direct I/O for the backing file.
460    #[cfg(feature = "direct_io")]
461    pub fn set_direct_io(mut self, direct_io: bool) -> Self {
462        self.direct_io = direct_io;
463        self
464    }
465
466    /// Force the kernel to scan the partition table on a newly created loop device. Note that the
467    /// partition table parsing depends on sector sizes. The default is sector size is 512 bytes
468    pub fn part_scan(mut self, enable: bool) -> Self {
469        if enable {
470            self.info.lo_flags |= LO_FLAGS_PARTSCAN;
471        } else {
472            self.info.lo_flags &= !LO_FLAGS_PARTSCAN;
473        }
474        self
475    }
476
477    /// Attach the loop device to a file with the set options.
478    ///
479    /// # Errors
480    ///
481    /// This function will return an error for various reasons. Either when
482    /// opening the backing file (see
483    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
484    /// for further details) or when calling the ioctl to attach the backing
485    /// file to the device.
486    pub fn attach(self, backing_file: impl AsRef<Path>) -> io::Result<()> {
487        self.device.attach_with_loop_info(backing_file, self.info)?;
488        #[cfg(feature = "direct_io")]
489        if self.direct_io {
490            self.device.set_direct_io(self.direct_io)?;
491        }
492        Ok(())
493    }
494
495    /// Attach the loop device to an fd
496    ///
497    /// # Errors
498    ///
499    /// This function will return an error for various reasons when calling the
500    /// ioctl to attach the backing file to the device.
501    pub fn attach_fd(self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
502        self.device
503            .attach_fd_with_loop_info(backing_file_fd, self.info)?;
504        #[cfg(feature = "direct_io")]
505        if self.direct_io {
506            self.device.set_direct_io(self.direct_io)?;
507        }
508        Ok(())
509    }
510}
511
512fn ioctl_to_error(ret: i32) -> io::Result<i32> {
513    if ret < 0 {
514        Err(io::Error::last_os_error())
515    } else {
516        Ok(ret)
517    }
518}