loopdev/
lib.rs

1// Suppress 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 function 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        Self::attach_with_loop_info(self, backing_file, loop_info64::default())
240    }
241
242    /// Attach the loop device to a file with `loop_info64`.
243    fn attach_with_loop_info(
244        &self, // TODO should be mut? - but changing it is a breaking change
245        backing_file: impl AsRef<Path>,
246        info: loop_info64,
247    ) -> io::Result<()> {
248        let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0;
249        let bf = OpenOptions::new()
250            .read(true)
251            .write(write_access)
252            .open(backing_file)?;
253        self.attach_fd_with_loop_info(bf, info)
254    }
255
256    /// Attach the loop device to a fd with `loop_info`.
257    fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: loop_info64) -> io::Result<()> {
258        // Attach the file
259        self.attach_fd(bf.as_raw_fd())?;
260
261        let result = unsafe {
262            ioctl(
263                self.device.as_raw_fd() as c_int,
264                LOOP_SET_STATUS64 as IoctlRequest,
265                &info,
266            )
267        };
268        match ioctl_to_error(result) {
269            Err(err) => {
270                // Ignore the error to preserve the original error
271                let _detach_err = self.detach();
272                Err(err)
273            }
274            Ok(_) => Ok(()),
275        }
276    }
277
278    /// Attach the loop device to a file descriptor of an already opened file
279    /// # Examples
280    ///
281    /// Attach the device to a file descriptor.
282    ///
283    /// ```no_run
284    /// use std::fs::OpenOptions;
285    /// use loopdev::LoopDevice;
286    /// let file = OpenOptions::new().read(true).write(true).open("disk.img").unwrap();
287    /// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
288    /// ld.attach_fd(file).unwrap();
289    /// # ld.detach().unwrap();
290    /// ```
291    ///
292    /// # Errors
293    ///
294    /// This file descriptor needs to be already opened and can fail otherwise
295    /// with an IO error.
296    pub fn attach_fd(&self, fd: impl AsRawFd) -> io::Result<()> {
297        ioctl_to_error(unsafe {
298            ioctl(
299                self.device.as_raw_fd() as c_int,
300                LOOP_SET_FD as IoctlRequest,
301                fd.as_raw_fd() as c_int,
302            )
303        })?;
304
305        Ok(())
306    }
307
308    /// Get the path of the loop device.
309    pub fn path(&self) -> Option<PathBuf> {
310        let mut p = PathBuf::from("/proc/self/fd");
311        p.push(self.device.as_raw_fd().to_string());
312        std::fs::read_link(&p).ok()
313    }
314
315    /// Get the device major number
316    ///
317    /// # Errors
318    ///
319    /// This function needs to stat the backing file and can fail if there is
320    /// an IO error.
321    #[allow(clippy::unnecessary_cast)]
322    pub fn major(&self) -> io::Result<u32> {
323        #[allow(unused_unsafe)]
324        self.device
325            .metadata()
326            .map(|m| unsafe { libc::major(m.rdev()) })
327            .map(|m| m as u32)
328    }
329
330    /// Get the device major number
331    ///
332    /// # Errors
333    ///
334    /// This function needs to stat the backing file and can fail if there is
335    /// an IO error.
336    #[allow(clippy::unnecessary_cast)]
337    pub fn minor(&self) -> io::Result<u32> {
338        #[allow(unused_unsafe)]
339        self.device
340            .metadata()
341            .map(|m| unsafe { libc::minor(m.rdev()) })
342            .map(|m| m as u32)
343    }
344
345    /// Detach a loop device from its backing file.
346    ///
347    /// Note that the device won't fully detach until a short delay after the underling device file
348    /// gets closed. This happens when `LoopDev` goes out of scope so you should ensure the `LoopDev`
349    /// lives for a short a time as possible.
350    ///
351    /// # Examples
352    ///
353    /// ```no_run
354    /// use loopdev::LoopDevice;
355    /// let ld = LoopDevice::open("/dev/loop0").unwrap();
356    /// # ld.attach_file("disk.img").unwrap();
357    /// ld.detach().unwrap();
358    /// ```
359    ///
360    /// # Errors
361    ///
362    /// This function will return an error for various reasons when calling the
363    /// ioctl to detach the backing file from the device.
364    pub fn detach(&self) -> io::Result<()> {
365        ioctl_to_error(unsafe {
366            ioctl(
367                self.device.as_raw_fd() as c_int,
368                LOOP_CLR_FD as IoctlRequest,
369                0,
370            )
371        })?;
372        Ok(())
373    }
374
375    /// Resize a live loop device. If the size of the backing file changes this can be called to
376    /// inform the loop driver about the new size.
377    ///
378    /// # Errors
379    ///
380    /// This function will return an error for various reasons when calling the
381    /// ioctl to set the capacity of the device.
382    pub fn set_capacity(&self) -> io::Result<()> {
383        ioctl_to_error(unsafe {
384            ioctl(
385                self.device.as_raw_fd() as c_int,
386                LOOP_SET_CAPACITY as IoctlRequest,
387                0,
388            )
389        })?;
390        Ok(())
391    }
392
393    /// Enable or disable direct I/O for the backing file.
394    ///
395    /// # Errors
396    ///
397    /// This function will return an error for various reasons when calling the
398    /// ioctl to set the direct io flag for the device.
399    #[cfg(feature = "direct_io")]
400    pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
401        ioctl_to_error(unsafe {
402            ioctl(
403                self.device.as_raw_fd() as c_int,
404                LOOP_SET_DIRECT_IO as IoctlRequest,
405                if direct_io { 1 } else { 0 },
406            )
407        })?;
408        Ok(())
409    }
410}
411
412/// Used to set options when attaching a device. Created with [`LoopDevice::with`()].
413///
414/// # Examples
415///
416/// Enable partition scanning on attach:
417///
418/// ```no_run
419/// use loopdev::LoopDevice;
420/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
421/// ld.with()
422///     .part_scan(true)
423///     .attach("disk.img")
424///     .unwrap();
425/// # ld.detach().unwrap();
426/// ```
427///
428/// A 1MiB slice of the file located at 1KiB into the file.
429///
430/// ```no_run
431/// use loopdev::LoopDevice;
432/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
433/// ld.with()
434///     .offset(1024*1024)
435///     .size_limit(1024*1024*1024)
436///     .attach("disk.img")
437///     .unwrap();
438/// # ld.detach().unwrap();
439/// ```
440#[must_use]
441pub struct AttachOptions<'d> {
442    device: &'d LoopDevice,
443    info: loop_info64,
444    #[cfg(feature = "direct_io")]
445    direct_io: bool,
446}
447
448impl AttachOptions<'_> {
449    /// Offset in bytes from the start of the backing file the data will start at.
450    pub fn offset(mut self, offset: u64) -> Self {
451        self.info.lo_offset = offset;
452        self
453    }
454
455    /// Maximum size of the data in bytes.
456    pub fn size_limit(mut self, size_limit: u64) -> Self {
457        self.info.lo_sizelimit = size_limit;
458        self
459    }
460
461    /// Set read only flag
462    pub fn read_only(mut self, read_only: bool) -> Self {
463        if read_only {
464            self.info.lo_flags |= LO_FLAGS_READ_ONLY;
465        } else {
466            self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
467        }
468        self
469    }
470
471    /// Set autoclear flag
472    pub fn autoclear(mut self, autoclear: bool) -> Self {
473        if autoclear {
474            self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
475        } else {
476            self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
477        }
478        self
479    }
480
481    // Enable or disable direct I/O for the backing file.
482    #[cfg(feature = "direct_io")]
483    pub fn set_direct_io(mut self, direct_io: bool) -> Self {
484        self.direct_io = direct_io;
485        self
486    }
487
488    /// Force the kernel to scan the partition table on a newly created loop device. Note that the
489    /// partition table parsing depends on sector sizes. The default is sector size is 512 bytes
490    pub fn part_scan(mut self, enable: bool) -> Self {
491        if enable {
492            self.info.lo_flags |= LO_FLAGS_PARTSCAN;
493        } else {
494            self.info.lo_flags &= !LO_FLAGS_PARTSCAN;
495        }
496        self
497    }
498
499    /// Attach the loop device to a file with the set options.
500    ///
501    /// # Errors
502    ///
503    /// This function will return an error for various reasons. Either when
504    /// opening the backing file (see
505    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
506    /// for further details) or when calling the ioctl to attach the backing
507    /// file to the device.
508    pub fn attach(self, backing_file: impl AsRef<Path>) -> io::Result<()> {
509        self.device.attach_with_loop_info(backing_file, self.info)?;
510        #[cfg(feature = "direct_io")]
511        if self.direct_io {
512            self.device.set_direct_io(self.direct_io)?;
513        }
514        Ok(())
515    }
516
517    /// Attach the loop device to an fd
518    ///
519    /// # Errors
520    ///
521    /// This function will return an error for various reasons when calling the
522    /// ioctl to attach the backing file to the device.
523    pub fn attach_fd(self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
524        self.device
525            .attach_fd_with_loop_info(backing_file_fd, self.info)?;
526        #[cfg(feature = "direct_io")]
527        if self.direct_io {
528            self.device.set_direct_io(self.direct_io)?;
529        }
530        Ok(())
531    }
532}
533
534fn ioctl_to_error(ret: i32) -> io::Result<i32> {
535    if ret < 0 {
536        Err(io::Error::last_os_error())
537    } else {
538        Ok(ret)
539    }
540}