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