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}