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}