loopdev/
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//! ```rust
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//! ```rust
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//! ```
40
41extern crate libc;
42
43#[cfg(feature = "direct_io")]
44use bindings::LOOP_SET_DIRECT_IO;
45use bindings::{
46    loop_info64, LOOP_CLR_FD, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_FD, LOOP_SET_STATUS64,
47    LO_FLAGS_AUTOCLEAR, LO_FLAGS_READ_ONLY,
48};
49use libc::{c_int, ioctl};
50use std::{
51    default::Default,
52    fs::{File, OpenOptions},
53    io,
54    os::unix::prelude::*,
55    path::{Path, PathBuf},
56};
57
58#[allow(non_camel_case_types)]
59#[allow(dead_code)]
60#[allow(non_snake_case)]
61mod bindings {
62    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
63}
64
65#[cfg(all(not(target_os = "android"), not(target_env = "musl")))]
66type IoctlRequest = libc::c_ulong;
67#[cfg(any(target_os = "android", target_env = "musl"))]
68type IoctlRequest = libc::c_int;
69
70const LOOP_CONTROL: &str = "/dev/loop-control";
71#[cfg(not(target_os = "android"))]
72const LOOP_PREFIX: &str = "/dev/loop";
73#[cfg(target_os = "android")]
74const LOOP_PREFIX: &str = "/dev/block/loop";
75
76/// Interface to the loop control device: `/dev/loop-control`.
77#[derive(Debug)]
78pub struct LoopControl {
79    dev_file: File,
80}
81
82impl LoopControl {
83    /// Opens the loop control device.
84    pub fn open() -> io::Result<Self> {
85        Ok(Self {
86            dev_file: OpenOptions::new()
87                .read(true)
88                .write(true)
89                .open(LOOP_CONTROL)?,
90        })
91    }
92
93    /// Finds and opens the next available loop device.
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use loopdev::LoopControl;
99    /// let lc = LoopControl::open().unwrap();
100    /// let ld = lc.next_free().unwrap();
101    /// println!("{}", ld.path().unwrap().display());
102    /// ```
103    pub fn next_free(&self) -> io::Result<LoopDevice> {
104        let dev_num = ioctl_to_error(unsafe {
105            ioctl(
106                self.dev_file.as_raw_fd() as c_int,
107                LOOP_CTL_GET_FREE as IoctlRequest,
108            )
109        })?;
110        LoopDevice::open(&format!("{}{}", LOOP_PREFIX, dev_num))
111    }
112}
113
114impl AsRawFd for LoopControl {
115    fn as_raw_fd(&self) -> RawFd {
116        self.dev_file.as_raw_fd()
117    }
118}
119
120impl IntoRawFd for LoopControl {
121    fn into_raw_fd(self) -> RawFd {
122        self.dev_file.into_raw_fd()
123    }
124}
125
126/// Interface to a loop device ie `/dev/loop0`.
127#[derive(Debug)]
128pub struct LoopDevice {
129    device: File,
130}
131
132impl AsRawFd for LoopDevice {
133    fn as_raw_fd(&self) -> RawFd {
134        self.device.as_raw_fd()
135    }
136}
137
138impl IntoRawFd for LoopDevice {
139    fn into_raw_fd(self) -> RawFd {
140        self.device.into_raw_fd()
141    }
142}
143
144impl LoopDevice {
145    /// Opens a loop device.
146    pub fn open<P: AsRef<Path>>(dev: P) -> io::Result<Self> {
147        // TODO create dev if it does not exist and begins with LOOP_PREFIX
148        Ok(Self {
149            device: OpenOptions::new().read(true).write(true).open(dev)?,
150        })
151    }
152
153    /// Attach the loop device to a file with given options.
154    ///
155    /// # Examples
156    ///
157    /// Attach the device to a file.
158    ///
159    /// ```rust
160    /// use loopdev::LoopDevice;
161    /// let mut ld = LoopDevice::open("/dev/loop3").unwrap();
162    /// ld.with().part_scan(true).attach("disk.img").unwrap();
163    /// # ld.detach().unwrap();
164    /// ```
165    pub fn with(&self) -> AttachOptions<'_> {
166        AttachOptions {
167            device: self,
168            info: Default::default(),
169            #[cfg(feature = "direct_io")]
170            direct_io: false,
171        }
172    }
173
174    /// Attach the loop device to a file that maps to the whole file.
175    ///
176    /// # Examples
177    ///
178    /// Attach the device to a file.
179    ///
180    /// ```rust
181    /// use loopdev::LoopDevice;
182    /// let ld = LoopDevice::open("/dev/loop4").unwrap();
183    /// ld.attach_file("disk.img").unwrap();
184    /// # ld.detach().unwrap();
185    /// ```
186    pub fn attach_file<P: AsRef<Path>>(&self, backing_file: P) -> io::Result<()> {
187        let info = loop_info64 {
188            ..Default::default()
189        };
190
191        Self::attach_with_loop_info(self, backing_file, info)
192    }
193
194    /// Attach the loop device to a file with loop_info64.
195    fn attach_with_loop_info(
196        &self, // TODO should be mut? - but changing it is a breaking change
197        backing_file: impl AsRef<Path>,
198        info: loop_info64,
199    ) -> io::Result<()> {
200        let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0;
201        let bf = OpenOptions::new()
202            .read(true)
203            .write(write_access)
204            .open(backing_file)?;
205        self.attach_fd_with_loop_info(bf, info)
206    }
207
208    /// Attach the loop device to a fd with loop_info.
209    fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: loop_info64) -> io::Result<()> {
210        // Attach the file
211        ioctl_to_error(unsafe {
212            ioctl(
213                self.device.as_raw_fd() as c_int,
214                LOOP_SET_FD as IoctlRequest,
215                bf.as_raw_fd() as c_int,
216            )
217        })?;
218
219        let result = unsafe {
220            ioctl(
221                self.device.as_raw_fd() as c_int,
222                LOOP_SET_STATUS64 as IoctlRequest,
223                &info,
224            )
225        };
226        match ioctl_to_error(result) {
227            Err(err) => {
228                // Ignore the error to preserve the original error
229                let _ = self.detach();
230                Err(err)
231            }
232            Ok(_) => Ok(()),
233        }
234    }
235
236    /// Get the path of the loop device.
237    pub fn path(&self) -> Option<PathBuf> {
238        let mut p = PathBuf::from("/proc/self/fd");
239        p.push(self.device.as_raw_fd().to_string());
240        std::fs::read_link(&p).ok()
241    }
242
243    /// Get the device major number
244    pub fn major(&self) -> io::Result<u32> {
245        self.device
246            .metadata()
247            .map(|m| unsafe { libc::major(m.rdev()) as u32 })
248    }
249
250    /// Get the device major number
251    pub fn minor(&self) -> io::Result<u32> {
252        self.device
253            .metadata()
254            .map(|m| unsafe { libc::minor(m.rdev()) as u32 })
255    }
256
257    /// Detach a loop device from its backing file.
258    ///
259    /// Note that the device won't fully detach until a short delay after the underling device file
260    /// gets closed. This happens when LoopDev goes out of scope so you should ensure the LoopDev
261    /// lives for a short a time as possible.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use loopdev::LoopDevice;
267    /// let ld = LoopDevice::open("/dev/loop5").unwrap();
268    /// # ld.attach_file("disk.img").unwrap();
269    /// ld.detach().unwrap();
270    /// ```
271    pub fn detach(&self) -> io::Result<()> {
272        ioctl_to_error(unsafe {
273            ioctl(
274                self.device.as_raw_fd() as c_int,
275                LOOP_CLR_FD as IoctlRequest,
276                0,
277            )
278        })?;
279        Ok(())
280    }
281
282    /// Resize a live loop device. If the size of the backing file changes this can be called to
283    /// inform the loop driver about the new size.
284    pub fn set_capacity(&self) -> io::Result<()> {
285        ioctl_to_error(unsafe {
286            ioctl(
287                self.device.as_raw_fd() as c_int,
288                LOOP_SET_CAPACITY as IoctlRequest,
289                0,
290            )
291        })?;
292        Ok(())
293    }
294
295    // Enable or disable direct I/O for the backing file.
296    #[cfg(feature = "direct_io")]
297    pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
298        ioctl_to_error(unsafe {
299            ioctl(
300                self.device.as_raw_fd() as c_int,
301                LOOP_SET_DIRECT_IO as IoctlRequest,
302                if direct_io { 1 } else { 0 },
303            )
304        })?;
305        Ok(())
306    }
307}
308
309/// Used to set options when attaching a device. Created with [LoopDevice::with()].
310///
311/// # Examples
312///
313/// Enable partition scanning on attach:
314///
315/// ```rust
316/// use loopdev::LoopDevice;
317/// let mut ld = LoopDevice::open("/dev/loop6").unwrap();
318/// ld.with()
319///     .part_scan(true)
320///     .attach("disk.img")
321///     .unwrap();
322/// # ld.detach().unwrap();
323/// ```
324///
325/// A 1MiB slice of the file located at 1KiB into the file.
326///
327/// ```rust
328/// use loopdev::LoopDevice;
329/// let mut ld = LoopDevice::open("/dev/loop7").unwrap();
330/// ld.with()
331///     .offset(1024*1024)
332///     .size_limit(1024*1024*1024)
333///     .attach("disk.img")
334///     .unwrap();
335/// # ld.detach().unwrap();
336/// ```
337pub struct AttachOptions<'d> {
338    device: &'d LoopDevice,
339    info: loop_info64,
340    #[cfg(feature = "direct_io")]
341    direct_io: bool,
342}
343
344impl AttachOptions<'_> {
345    /// Offset in bytes from the start of the backing file the data will start at.
346    pub fn offset(mut self, offset: u64) -> Self {
347        self.info.lo_offset = offset;
348        self
349    }
350
351    /// Maximum size of the data in bytes.
352    pub fn size_limit(mut self, size_limit: u64) -> Self {
353        self.info.lo_sizelimit = size_limit;
354        self
355    }
356
357    /// Set read only flag
358    pub fn read_only(mut self, read_only: bool) -> Self {
359        if read_only {
360            self.info.lo_flags |= LO_FLAGS_READ_ONLY;
361        } else {
362            self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
363        }
364        self
365    }
366
367    /// Set autoclear flag
368    pub fn autoclear(mut self, read_only: bool) -> Self {
369        if read_only {
370            self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
371        } else {
372            self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
373        }
374        self
375    }
376
377    // Enable or disable direct I/O for the backing file.
378    #[cfg(feature = "direct_io")]
379    pub fn set_direct_io(mut self, direct_io: bool) -> Self {
380        self.direct_io = direct_io;
381        self
382    }
383
384    /// Force the kernel to scan the partition table on a newly created loop device. Note that the
385    /// partition table parsing depends on sector sizes. The default is sector size is 512 bytes
386    pub fn part_scan(mut self, enable: bool) -> Self {
387        if enable {
388            self.info.lo_flags |= 1 << 4;
389        } else {
390            self.info.lo_flags &= u32::max_value() - (1 << 4);
391        }
392        self
393    }
394
395    /// Attach the loop device to a file with the set options.
396    pub fn attach(self, backing_file: impl AsRef<Path>) -> io::Result<()> {
397        self.device.attach_with_loop_info(backing_file, self.info)?;
398        #[cfg(feature = "direct_io")]
399        if self.direct_io {
400            self.device.set_direct_io(self.direct_io)?;
401        }
402        Ok(())
403    }
404
405    /// Attach the loop device to an fd
406    pub fn attach_fd(self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
407        self.device
408            .attach_fd_with_loop_info(backing_file_fd, self.info)?;
409        #[cfg(feature = "direct_io")]
410        if self.direct_io {
411            self.device.set_direct_io(self.direct_io)?;
412        }
413        Ok(())
414    }
415}
416
417fn ioctl_to_error(ret: i32) -> io::Result<i32> {
418    if ret < 0 {
419        Err(io::Error::last_os_error())
420    } else {
421        Ok(ret)
422    }
423}