diff --git a/.cargo/config b/.cargo/config new file mode 100755 index 00000000..c0f3d827 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = '.cargo/runner-x86_64-unknown-linux-gnu' diff --git a/.cargo/runner-x86_64-unknown-linux-gnu b/.cargo/runner-x86_64-unknown-linux-gnu new file mode 100755 index 00000000..8ca0ee5d --- /dev/null +++ b/.cargo/runner-x86_64-unknown-linux-gnu @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +binary_name=`basename $1` + +sudo -E --preserve-env=PATH $@ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2875c7be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +on: [push, pull_request] + +name: Continuous integration + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index da290449..4c585370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,9 @@ travis-ci = { repository = "serde-rs/serde" } errno = "0.2" libc = "0.2" +[build-dependencies] +bindgen = "0.58.1" + [dev-dependencies] tempfile = "3.1.0" lazy_static = "1.3.0" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..26eb038d --- /dev/null +++ b/build.rs @@ -0,0 +1,18 @@ +extern crate bindgen; + +use bindgen::Builder; +use std::{env::var, path::PathBuf}; + +fn main() { + let bindings = Builder::default() + .header_contents("wrapper.h", "#include ") + .derive_default(true) + .generate() + .expect("Could not generate bindings"); + + let mut bindings_path = PathBuf::from(var("OUT_DIR").unwrap()); + bindings_path.push("bindings.rs"); + bindings + .write_to_file(&bindings_path) + .expect("Could not write bindings to file"); +} diff --git a/losetup/src/main.rs b/losetup/src/main.rs index ce71ea09..3af1260c 100644 --- a/losetup/src/main.rs +++ b/losetup/src/main.rs @@ -2,9 +2,9 @@ extern crate clap; extern crate loopdev; +use loopdev::{LoopControl, LoopDevice}; use std::io::{self, Write}; use std::process::exit; -use loopdev::{LoopControl, LoopDevice}; fn find() -> io::Result<()> { let loopdev = LoopControl::open()?.next_free()?; @@ -13,16 +13,25 @@ fn find() -> io::Result<()> { } fn attach(matches: &clap::ArgMatches) -> io::Result<()> { - let quite = matches.is_present("quite"); + let quiet = matches.is_present("quiet"); let image = matches.value_of("image").unwrap(); let offset = value_t!(matches.value_of("offset"), u64).unwrap_or(0); - let sizelimit = value_t!(matches.value_of("sizelimit"), u64).unwrap_or(0); - let loopdev = match matches.value_of("loopdev") { + let size_limit = value_t!(matches.value_of("sizelimit"), u64).unwrap_or(0); + let read_only = matches.is_present("read-only"); + let autoclear = matches.is_present("autoclear"); + let mut loopdev = match matches.value_of("loopdev") { Some(loopdev) => LoopDevice::open(&loopdev)?, None => LoopControl::open().and_then(|lc| lc.next_free())?, }; - loopdev.attach_with_sizelimit(&image, offset, sizelimit)?; - if !quite { + loopdev + .with() + .offset(offset) + .size_limit(size_limit) + .read_only(read_only) + .autoclear(autoclear) + .attach(image)?; + + if !quiet { println!("{}", loopdev.path().unwrap().display()); } Ok(()) @@ -60,7 +69,9 @@ fn main() { (@arg loopdev: "the loop device to attach") (@arg offset: -o --offset +takes_value "the offset within the file to start at") (@arg sizelimit: -s --sizelimit +takes_value "the file is limited to this size") - (@arg quite: -q --quite "don't print the device name") + (@arg read_only: -r --read-only "set up a read-only loop device") + (@arg autoclear: -a --autoclear "set the autoclear flag") + (@arg quiet: -q --quiet "don't print the device name") ) (@subcommand detach => (about: "detach the loop device from the backing file") @@ -75,7 +86,8 @@ fn main() { (@arg free: -f --free "find free devices") (@arg used: -u --used "find used devices") ) - ).get_matches(); + ) + .get_matches(); let result = match matches.subcommand() { ("find", _) => find(), diff --git a/src/lib.rs b/src/lib.rs index 9f8a9b7e..ccd6fbb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! //! # Examples //! -//! ```rust +//! ```no_run //! use loopdev::LoopControl; //! let lc = LoopControl::open().unwrap(); //! let ld = lc.next_free().unwrap(); @@ -18,30 +18,36 @@ extern crate libc; -use std::fs::File; -use std::fs::OpenOptions; - +use bindings::{ + loop_info64, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_DIRECT_IO, LOOP_SET_FD, + LOOP_SET_STATUS64, +}; use libc::{c_int, ioctl}; -use std::default::Default; -use std::io; -use std::os::unix::prelude::*; -use std::path::{Path, PathBuf}; - -// TODO support missing operations -const LOOP_SET_FD: u16 = 0x4C00; -const LOOP_CLR_FD: u16 = 0x4C01; -const LOOP_SET_STATUS64: u16 = 0x4C04; -//const LOOP_GET_STATUS64: u16 = 0x4C05; -const LOOP_SET_CAPACITY: u16 = 0x4C07; -//const LOOP_SET_DIRECT_IO: u16 = 0x4C08; -//const LOOP_SET_BLOCK_SIZE: u16 = 0x4C09; - -//const LOOP_CTL_ADD: u16 = 0x4C80; -//const LOOP_CTL_REMOVE: u16 = 0x4C81; -const LOOP_CTL_GET_FREE: u16 = 0x4C82; +use std::fs::{File, Metadata, OpenOptions}; +use std::{ + default::Default, + io, + os::unix::prelude::*, + path::{Path, PathBuf}, +}; + +#[allow(non_camel_case_types)] +#[allow(dead_code)] +#[allow(non_snake_case)] +mod bindings { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +#[cfg(not(target_os = "android"))] +type IoctlRequest = libc::c_ulong; +#[cfg(target_os = "android")] +type IoctlRequest = libc::c_int; const LOOP_CONTROL: &str = "/dev/loop-control"; +#[cfg(not(target_os = "android"))] const LOOP_PREFIX: &str = "/dev/loop"; +#[cfg(target_os = "android")] +const LOOP_PREFIX: &str = "/dev/block/loop"; /// Interface to the loop control device: `/dev/loop-control`. #[derive(Debug)] @@ -64,7 +70,7 @@ impl LoopControl { /// /// # Examples /// - /// ```rust + /// ```no_run /// use loopdev::LoopControl; /// let lc = LoopControl::open().unwrap(); /// let ld = lc.next_free().unwrap(); @@ -72,12 +78,27 @@ impl LoopControl { /// ``` pub fn next_free(&self) -> io::Result { let dev_num = ioctl_to_error(unsafe { - ioctl(self.dev_file.as_raw_fd() as c_int, LOOP_CTL_GET_FREE.into()) + ioctl( + self.dev_file.as_raw_fd() as c_int, + LOOP_CTL_GET_FREE as IoctlRequest, + ) })?; LoopDevice::open(&format!("{}{}", LOOP_PREFIX, dev_num)) } } +impl AsRawFd for LoopControl { + fn as_raw_fd(&self) -> RawFd { + self.dev_file.as_raw_fd() + } +} + +impl IntoRawFd for LoopControl { + fn into_raw_fd(self) -> RawFd { + self.dev_file.into_raw_fd() + } +} + /// Interface to a loop device ie `/dev/loop0`. #[derive(Debug)] pub struct LoopDevice { @@ -105,7 +126,7 @@ impl LoopDevice { /// /// Attach the device to a file. /// - /// ```rust + /// ```no_run /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop3").unwrap(); /// ld.with().part_scan(true).attach("test.img").unwrap(); @@ -115,6 +136,7 @@ impl LoopDevice { AttachOptions { device: self, info: Default::default(), + direct_io: false, } } @@ -124,7 +146,7 @@ impl LoopDevice { note = "use `loop.with().offset(offset).attach(file)` instead" )] pub fn attach>(&self, backing_file: P, offset: u64) -> io::Result<()> { - let info = LoopInfo64 { + let info = loop_info64 { lo_offset: offset, ..Default::default() }; @@ -138,14 +160,14 @@ impl LoopDevice { /// /// Attach the device to a file. /// - /// ```rust + /// ```no_run /// use loopdev::LoopDevice; /// let ld = LoopDevice::open("/dev/loop4").unwrap(); /// ld.attach_file("test.img").unwrap(); /// # ld.detach().unwrap(); /// ``` pub fn attach_file>(&self, backing_file: P) -> io::Result<()> { - let info = LoopInfo64 { + let info = loop_info64 { ..Default::default() }; @@ -162,7 +184,7 @@ impl LoopDevice { backing_file: P, offset: u64, ) -> io::Result<()> { - let info = LoopInfo64 { + let info = loop_info64 { lo_offset: offset, ..Default::default() }; @@ -181,7 +203,7 @@ impl LoopDevice { offset: u64, size_limit: u64, ) -> io::Result<()> { - let info = LoopInfo64 { + let info = loop_info64 { lo_offset: offset, lo_sizelimit: size_limit, ..Default::default() @@ -190,38 +212,45 @@ impl LoopDevice { Self::attach_with_loop_info(self, backing_file, info) } - /// Attach the loop device to a file with loop_info. + /// Attach the loop device to a file with loop_info64. fn attach_with_loop_info( &self, // TODO should be mut? - but changing it is a breaking change backing_file: impl AsRef, - info: LoopInfo64, + info: loop_info64, ) -> io::Result<()> { let bf = OpenOptions::new() .read(true) .write(true) .open(backing_file)?; + self.attach_fd_with_loop_info(bf.as_raw_fd(), info) + } + /// Attach the loop device to a fd with loop_info64. + fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: loop_info64) -> io::Result<()> { // Attach the file ioctl_to_error(unsafe { ioctl( self.device.as_raw_fd() as c_int, - LOOP_SET_FD.into(), + LOOP_SET_FD as IoctlRequest, bf.as_raw_fd() as c_int, ) })?; - if let Err(err) = ioctl_to_error(unsafe { + let result = unsafe { ioctl( self.device.as_raw_fd() as c_int, - LOOP_SET_STATUS64.into(), + LOOP_SET_STATUS64 as IoctlRequest, &info, ) - }) { - // Ignore the error to preserve the original error - let _ = self.detach(); - return Err(err); + }; + match ioctl_to_error(result) { + Err(err) => { + // Ignore the error to preserve the original error + let _ = self.detach(); + Err(err) + } + Ok(_) => Ok(()), } - Ok(()) } /// Get the path of the loop device. @@ -237,6 +266,11 @@ impl LoopDevice { std::fs::read_link(&p).ok() } + /// Get the device metadata + pub fn metadata(&self) -> io::Result { + self.device.metadata() + } + /// Detach a loop device from its backing file. /// /// Note that the device won't fully detach until a short delay after the underling device file @@ -245,14 +279,20 @@ impl LoopDevice { /// /// # Examples /// - /// ```rust + /// ```no_run /// use loopdev::LoopDevice; /// let ld = LoopDevice::open("/dev/loop5").unwrap(); /// # ld.attach_file("test.img").unwrap(); /// ld.detach().unwrap(); /// ``` pub fn detach(&self) -> io::Result<()> { - ioctl_to_error(unsafe { ioctl(self.device.as_raw_fd() as c_int, LOOP_CLR_FD.into(), 0) })?; + ioctl_to_error(unsafe { + ioctl( + self.device.as_raw_fd() as c_int, + bindings::LOOP_CLR_FD as IoctlRequest, + 0, + ) + })?; Ok(()) } @@ -262,12 +302,24 @@ impl LoopDevice { ioctl_to_error(unsafe { ioctl( self.device.as_raw_fd() as c_int, - LOOP_SET_CAPACITY.into(), + LOOP_SET_CAPACITY as IoctlRequest, 0, ) })?; Ok(()) } + + // Enable or disable direct I/O for the backing file. + pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> { + ioctl_to_error(unsafe { + ioctl( + self.device.as_raw_fd() as c_int, + LOOP_SET_DIRECT_IO as IoctlRequest, + if direct_io { 1 } else { 0 }, + ) + })?; + Ok(()) + } } /// Used to set options when attaching a device. Created with [LoopDevice::with()]. @@ -276,7 +328,7 @@ impl LoopDevice { /// /// Enable partition scanning on attach: /// -/// ```rust +/// ```no_run /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop6").unwrap(); /// ld.with() @@ -288,7 +340,7 @@ impl LoopDevice { /// /// A 1MiB slice of the file located at 1KiB into the file. /// -/// ```rust +/// ```no_run /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop7").unwrap(); /// ld.with() @@ -300,7 +352,8 @@ impl LoopDevice { /// ``` pub struct AttachOptions<'d> { device: &'d mut LoopDevice, - info: LoopInfo64, + info: loop_info64, + direct_io: bool, } impl AttachOptions<'_> { @@ -316,6 +369,32 @@ impl AttachOptions<'_> { self } + /// Set read only flag + pub fn read_only(mut self, read_only: bool) -> Self { + if read_only { + self.info.lo_flags |= bindings::LO_FLAGS_READ_ONLY; + } else { + self.info.lo_flags &= !bindings::LO_FLAGS_READ_ONLY; + } + self + } + + /// Set autoclear flag + pub fn autoclear(mut self, read_only: bool) -> Self { + if read_only { + self.info.lo_flags |= bindings::LO_FLAGS_AUTOCLEAR; + } else { + self.info.lo_flags &= !bindings::LO_FLAGS_AUTOCLEAR; + } + self + } + + // Enable or disable direct I/O for the backing file. + pub fn set_direct_io(mut self, direct_io: bool) -> Self { + self.direct_io = direct_io; + self + } + /// Force the kernel to scan the partition table on a newly created loop device. Note that the /// partition table parsing depends on sector sizes. The default is sector size is 512 bytes pub fn part_scan(mut self, enable: bool) -> Self { @@ -329,45 +408,21 @@ impl AttachOptions<'_> { /// Attach the loop device to a file with the set options. pub fn attach(self, backing_file: impl AsRef) -> io::Result<()> { - self.device.attach_with_loop_info(backing_file, self.info) + self.device.attach_with_loop_info(backing_file, self.info)?; + if self.direct_io { + self.device.set_direct_io(self.direct_io)?; + } + Ok(()) } -} -// https://man7.org/linux/man-pages/man4/loop.4.html -#[repr(C)] -struct LoopInfo64 { - pub lo_device: u64, // ioctl r/o - pub lo_inode: u64, // ioctl r/o - pub lo_rdevice: u64, // ioctl r/o - pub lo_offset: u64, // - pub lo_sizelimit: u64, // bytes, 0 == max available - pub lo_number: u32, // ioctl r/o - pub lo_encrypt_type: u32, // - pub lo_encrypt_key_size: u32, // ioctl w/o - pub lo_flags: u32, // ioctl r/w (r/o before Linux 2.6.25) - pub lo_file_name: [u8; 64], // - pub lo_crypt_name: [u8; 64], // - pub lo_encrypt_key: [u8; 32], // ioctl w/o - pub lo_init: [u64; 2], // -} - -impl Default for LoopInfo64 { - fn default() -> Self { - Self { - lo_device: 0, - lo_inode: 0, - lo_rdevice: 0, - lo_offset: 0, - lo_sizelimit: 0, - lo_number: 0, - lo_encrypt_type: 0, - lo_encrypt_key_size: 0, - lo_flags: 0, - lo_file_name: [0; 64], - lo_crypt_name: [0; 64], - lo_encrypt_key: [0; 32], - lo_init: [0; 2], + /// Attach the loop device to an fd + pub fn attach_fd(self, backing_file_fd: impl AsRawFd) -> io::Result<()> { + self.device + .attach_fd_with_loop_info(backing_file_fd, self.info)?; + if self.direct_io { + self.device.set_direct_io(self.direct_io)?; } + Ok(()) } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9505672d..e41e2c71 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -7,7 +7,6 @@ extern crate serde; extern crate serde_json; use loopdev::{LoopControl, LoopDevice}; -use std::path::PathBuf; mod util; use util::{attach_file, create_backing_file, detach_all, list_device, setup}; @@ -17,15 +16,8 @@ fn get_next_free_device() { let _lock = setup(); let lc = LoopControl::open().expect("should be able to open the LoopControl device"); - let ld0 = lc - .next_free() + lc.next_free() .expect("should not error finding the next free loopback device"); - - assert_eq!( - ld0.path(), - Some(PathBuf::from("/dev/loop0")), - "should find the first loopback device" - ); } #[test] @@ -148,17 +140,14 @@ fn detach_a_backing_file_with_sizelimit_overflow() { fn detach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { let _lock = setup(); + let pre_num_devices = list_device(None).len(); + { let file = create_backing_file(file_size); - attach_file( - "/dev/loop0", - file.to_path_buf().to_str().unwrap(), - offset, - sizelimit, - ); + let ld = attach_file(file.to_path_buf().to_str().unwrap(), offset, sizelimit); - let ld0 = LoopDevice::open("/dev/loop0") - .expect("should be able to open the created loopback device"); + let ld0 = + LoopDevice::open(&ld).expect("should be able to open the created loopback device"); ld0.detach() .expect("should not error detaching the backing file from the loopdev"); @@ -171,8 +160,8 @@ fn detach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { assert_eq!( devices.len(), - 0, - "there should be no loopback devices mounted" + pre_num_devices, + "there should be no additional loopback devices mounted" ); detach_all(); } diff --git a/tests/util/mod.rs b/tests/util/mod.rs index f7cbd80e..988c0487 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -1,9 +1,13 @@ use libc::fallocate; +use loopdev::LoopControl; use serde::{Deserialize, Deserializer}; -use std::io; -use std::os::unix::io::AsRawFd; -use std::process::Command; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::{ + io, + os::unix::{io::AsRawFd, prelude::MetadataExt}, + path::PathBuf, + process::Command, + sync::{Arc, Mutex, MutexGuard}, +}; use tempfile::{NamedTempFile, TempPath}; @@ -30,22 +34,22 @@ pub fn setup() -> MutexGuard<'static, ()> { lock } -pub fn attach_file(loop_dev: &str, backing_file: &str, offset: u64, sizelimit: u64) { - if !Command::new("losetup") - .args(&[ - loop_dev, - backing_file, - "--offset", - &offset.to_string(), - "--sizelimit", - &sizelimit.to_string(), - ]) - .status() - .expect("failed to attach backing file to loop device") - .success() - { - panic!("failed to cleanup existing loop devices") - } +/// Attach `backing_file` and return path to loop device +pub fn attach_file(backing_file: &str, offset: u64, sizelimit: u64) -> PathBuf { + let lc = LoopControl::open().expect("should be able to open the LoopControl device"); + let mut ld = lc + .next_free() + .expect("should not error finding the next free loopback device"); + ld.with() + .offset(offset) + .size_limit(sizelimit) + .attach(&backing_file) + .expect("should not error attaching the backing file to the loopdev"); + let meta = ld.metadata().expect("should not error accessing metadata"); + let rdev = meta.rdev(); + let minor = ((rdev >> 12) & 0xFFFF_FF00) | (rdev & 0xFF); + + PathBuf::from(format!("/dev/loop{}", minor)) } pub fn detach_all() { @@ -67,9 +71,7 @@ pub fn list_device(dev_file: Option<&str>) -> Vec { if let Some(dev_file) = dev_file { output.arg(dev_file); } - let output = output - .output() - .expect("failed to cleanup existing loop devices"); + let output = output.output().expect("failed to list loop devices"); if output.stdout.is_empty() { Vec::new()