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        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        #[allow(unused_unsafe)]
304        self.device
305            .metadata()
306            .map(|m| unsafe { libc::major(m.rdev()) })
307            .map(|m| m as u32)
308    }
309
310    /// Get the device major number
311    ///
312    /// # Errors
313    ///
314    /// This function needs to stat the backing file and can fail if there is
315    /// an IO error.
316    #[allow(clippy::unnecessary_cast)]
317    pub fn minor(&self) -> io::Result<u32> {
318        #[allow(unused_unsafe)]
319        self.device
320            .metadata()
321            .map(|m| unsafe { libc::minor(m.rdev()) })
322            .map(|m| m as u32)
323    }
324
325    /// Detach a loop device from its backing file.
326    ///
327    /// Note that the device won't fully detach until a short delay after the underling device file
328    /// gets closed. This happens when `LoopDev` goes out of scope so you should ensure the `LoopDev`
329    /// lives for a short a time as possible.
330    ///
331    /// # Examples
332    ///
333    /// ```no_run
334    /// use loopdev::LoopDevice;
335    /// let ld = LoopDevice::open("/dev/loop0").unwrap();
336    /// # ld.attach_file("disk.img").unwrap();
337    /// ld.detach().unwrap();
338    /// ```
339    ///
340    /// # Errors
341    ///
342    /// This function will return an error for various reasons when calling the
343    /// ioctl to detach the backing file from the device.
344    pub fn detach(&self) -> io::Result<()> {
345        ioctl_to_error(unsafe {
346            ioctl(
347                self.device.as_raw_fd() as c_int,
348                LOOP_CLR_FD as IoctlRequest,
349                0,
350            )
351        })?;
352        Ok(())
353    }
354
355    /// Resize a live loop device. If the size of the backing file changes this can be called to
356    /// inform the loop driver about the new size.
357    ///
358    /// # Errors
359    ///
360    /// This function will return an error for various reasons when calling the
361    /// ioctl to set the capacity of the device.
362    pub fn set_capacity(&self) -> io::Result<()> {
363        ioctl_to_error(unsafe {
364            ioctl(
365                self.device.as_raw_fd() as c_int,
366                LOOP_SET_CAPACITY as IoctlRequest,
367                0,
368            )
369        })?;
370        Ok(())
371    }
372
373    /// Enable or disable direct I/O for the backing file.
374    ///
375    /// # Errors
376    ///
377    /// This function will return an error for various reasons when calling the
378    /// ioctl to set the direct io flag for the device.
379    #[cfg(feature = "direct_io")]
380    pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
381        ioctl_to_error(unsafe {
382            ioctl(
383                self.device.as_raw_fd() as c_int,
384                LOOP_SET_DIRECT_IO as IoctlRequest,
385                if direct_io { 1 } else { 0 },
386            )
387        })?;
388        Ok(())
389    }
390}
391
392/// Used to set options when attaching a device. Created with [`LoopDevice::with`()].
393///
394/// # Examples
395///
396/// Enable partition scanning on attach:
397///
398/// ```no_run
399/// use loopdev::LoopDevice;
400/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
401/// ld.with()
402///     .part_scan(true)
403///     .attach("disk.img")
404///     .unwrap();
405/// # ld.detach().unwrap();
406/// ```
407///
408/// A 1MiB slice of the file located at 1KiB into the file.
409///
410/// ```no_run
411/// use loopdev::LoopDevice;
412/// let mut ld = LoopDevice::open("/dev/loop0").unwrap();
413/// ld.with()
414///     .offset(1024*1024)
415///     .size_limit(1024*1024*1024)
416///     .attach("disk.img")
417///     .unwrap();
418/// # ld.detach().unwrap();
419/// ```
420#[must_use]
421pub struct AttachOptions<'d> {
422    device: &'d LoopDevice,
423    info: loop_info64,
424    #[cfg(feature = "direct_io")]
425    direct_io: bool,
426}
427
428impl AttachOptions<'_> {
429    /// Offset in bytes from the start of the backing file the data will start at.
430    pub fn offset(mut self, offset: u64) -> Self {
431        self.info.lo_offset = offset;
432        self
433    }
434
435    /// Maximum size of the data in bytes.
436    pub fn size_limit(mut self, size_limit: u64) -> Self {
437        self.info.lo_sizelimit = size_limit;
438        self
439    }
440
441    /// Set read only flag
442    pub fn read_only(mut self, read_only: bool) -> Self {
443        if read_only {
444            self.info.lo_flags |= LO_FLAGS_READ_ONLY;
445        } else {
446            self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
447        }
448        self
449    }
450
451    /// Set autoclear flag
452    pub fn autoclear(mut self, autoclear: bool) -> Self {
453        if autoclear {
454            self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
455        } else {
456            self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
457        }
458        self
459    }
460
461    // Enable or disable direct I/O for the backing file.
462    #[cfg(feature = "direct_io")]
463    pub fn set_direct_io(mut self, direct_io: bool) -> Self {
464        self.direct_io = direct_io;
465        self
466    }
467
468    /// Force the kernel to scan the partition table on a newly created loop device. Note that the
469    /// partition table parsing depends on sector sizes. The default is sector size is 512 bytes
470    pub fn part_scan(mut self, enable: bool) -> Self {
471        if enable {
472            self.info.lo_flags |= LO_FLAGS_PARTSCAN;
473        } else {
474            self.info.lo_flags &= !LO_FLAGS_PARTSCAN;
475        }
476        self
477    }
478
479    /// Attach the loop device to a file with the set options.
480    ///
481    /// # Errors
482    ///
483    /// This function will return an error for various reasons. Either when
484    /// opening the backing file (see
485    /// [`OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html)
486    /// for further details) or when calling the ioctl to attach the backing
487    /// file to the device.
488    pub fn attach(self, backing_file: impl AsRef<Path>) -> io::Result<()> {
489        self.device.attach_with_loop_info(backing_file, self.info)?;
490        #[cfg(feature = "direct_io")]
491        if self.direct_io {
492            self.device.set_direct_io(self.direct_io)?;
493        }
494        Ok(())
495    }
496
497    /// Attach the loop device to an fd
498    ///
499    /// # Errors
500    ///
501    /// This function will return an error for various reasons when calling the
502    /// ioctl to attach the backing file to the device.
503    pub fn attach_fd(self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
504        self.device
505            .attach_fd_with_loop_info(backing_file_fd, self.info)?;
506        #[cfg(feature = "direct_io")]
507        if self.direct_io {
508            self.device.set_direct_io(self.direct_io)?;
509        }
510        Ok(())
511    }
512}
513
514fn ioctl_to_error(ret: i32) -> io::Result<i32> {
515    if ret < 0 {
516        Err(io::Error::last_os_error())
517    } else {
518        Ok(ret)
519    }
520}