loopdev-0.4.0/.cargo_vcs_info.json0000644000000001120000000000100124770ustar { "git": { "sha1": "79d471558e927c644111fe371fea30e1a9217722" } } loopdev-0.4.0/.gitignore000064400000000000000000000000770072674642500133210ustar 00000000000000target/ /Cargo.lock test.img disk.img *.rustfmt .vagrant *.log loopdev-0.4.0/.travis-deploy000075500000000000000000000006240072674642500141350ustar 00000000000000#!/bin/bash set -uo pipefail trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR IFS=$'\n\t' PKGID="$(cargo pkgid)" echo "Cargo version: ${PKGID##*#}" echo "Travis tag: ${TRAVIS_TAG}" if [[ "${PKGID##*#}" == "$TRAVIS_TAG" ]]; then cargo publish --token "$CARGO_TOKEN" else echo "Git tag '${TRAVIS_TAG}' does not match cargo version '${PKGID##*#}'" 1>&2 exit 1 fi loopdev-0.4.0/.travis.yml000064400000000000000000000026410072674642500134410ustar 00000000000000sudo: required language: rust addons: apt: packages: - libcurl4-openssl-dev - libelf-dev - libdw-dev rust: - nightly - beta - stable - 1.52.0 - 1.51.0 - 1.50.0 matrix: allow_failures: - rust: nightly before_script: - fallocate -l 128M disk.img - '[[ "$TRAVIS_RUST_VERSION" != "stable" ]] || rustup component add clippy-preview' env: global: - TRAVIS_CARGO_NIGHTLY_FEATURE="" - secure: "yxpJSNW4ASzz983BpHFHE3uoS+4eyB1YIZjZjtL+1/VLESMp5ZEx5nfYdcbg/yA45nsldERZaLEtyhhgbnSOofdzPsZjxrmF2B+ePheTg4GEvH4HGg1Gz8Phmkjd4PRt4w8ji0Qk6h0NruUMMImUBYLRAAB6z2iKQCaCftKhBtQDXtJ6XUe7xom4McooLsBYuJgY//Fjn/BbvClwn3RfpSWttEQJ6j8gQlMLxtLNxtQ8cf/NShkGmRyl4U0QqURBZqvR2LstnqVkYEVifJoWfnIn9qCKYTouj4tRxy3IUwOPFZ9/SvcN639zP8V4qy1vJ146GyfEDpN4mtFP23U6gBbnwVGaaPeGyVVCYYbPfFWbgSs0GUt2JCUTAJaHuIXCrUb8Tlcj3a5smd7ffwdoSK29arTkHF2j94GmTdDZLeGmLtOPz9GGHLnafmNyd8C2UW4psxapskEXaixAYg81R907dW4ZitDxKygnsBHHYzJoHP94cCqR+n6rad9z/YW0ZtMWgEenEkr4qht/jEMRz23a1kLEfmphCAPNzYI3jXFYaBq/sZHeoe/d5cSWxOyeUti9cLtlnpDOEi0S+M9Fczz/iH9EU3a0pQt1KGuOIzExcIV3WmpBErEaF+Xj0Vw7BfFt7yJAk0EAv1nCM8untqoFnmQQCYeiOP+XbhjHzjo=" script: - '[[ "$TRAVIS_RUST_VERSION" != "stable" ]] || cargo clippy -- -D warnings' - cargo build - sudo -E env "PATH=$PATH" cargo test --jobs 1 deploy: - provider: script skip_cleanup: true on: tags: true condition: "$TRAVIS_RUST_VERSION = stable" script: ./.travis-deploy loopdev-0.4.0/Cargo.toml0000644000000023220000000000100105020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] name = "loopdev" version = "0.4.0" authors = ["Michael Daffin "] description = "Setup and control loop devices" documentation = "https://docs.rs/loopdev" readme = "README.md" keywords = ["loop", "losetup"] license = "MIT" repository = "https://github.com/mdaffin/loopdev" [dependencies.errno] version = "0.2.8" [dependencies.libc] version = "0.2.105" [dev-dependencies.lazy_static] version = "1.4.0" [dev-dependencies.serde] version = "1.0.130" features = ["derive"] [dev-dependencies.serde_json] version = "1.0.68" [dev-dependencies.tempfile] version = "3.2.0" [build-dependencies.bindgen] version = "0.59.1" features = ["runtime"] default-features = false [features] direct_io = [] [badges.travis-ci] repository = "serde-rs/serde" loopdev-0.4.0/Cargo.toml.orig000064400000000000000000000012440072674642500142150ustar 00000000000000[package] name = "loopdev" description = "Setup and control loop devices" version = "0.4.0" authors = ["Michael Daffin "] license = "MIT" documentation = "https://docs.rs/loopdev" repository = "https://github.com/mdaffin/loopdev" readme = "README.md" keywords = ["loop", "losetup"] [badges] travis-ci = { repository = "serde-rs/serde" } [features] direct_io = [] [dependencies] errno = "0.2.8" libc = "0.2.105" [build-dependencies] bindgen = { version = "0.59.1", default-features = false, features = ["runtime"] } [dev-dependencies] tempfile = "3.2.0" lazy_static = "1.4.0" serde_json = "1.0.68" serde = { version = "1.0.130", features = ["derive"] } loopdev-0.4.0/LICENSE000064400000000000000000000020710072674642500123320ustar 00000000000000The MIT License (MIT) Copyright (c) 2016 Michael Daffin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. loopdev-0.4.0/README.md000064400000000000000000000030320072674642500126020ustar 00000000000000[![Build Status](https://travis-ci.com/mdaffin/loopdev.svg?branch=master)](https://app.travis-ci.com/github/mdaffin/loopdev) [![crates.io](https://img.shields.io/crates/v/loopdev.svg)](https://crates.io/crates/loopdev) # loopdev Setup and control loop devices. Provides rust interface with similar functionality to the Linux utility `losetup`. ## [Documentation](https://docs.rs/loopdev) ## Examples ```rust use loopdev::LoopControl; let lc = LoopControl::open().unwrap(); let ld = lc.next_free().unwrap(); println!("{}", ld.path().unwrap().display()); ld.attach_file("disk.img").unwrap(); // ... ld.detach().unwrap(); ``` ## Development ### Running The Tests Locally Unfortunately the tests require root only syscalls and thus must be run as root. There is little point in mocking out these syscalls as I want to test they actually function as expected and if they were to be mocked out then the tests would not really be testing anything useful. A vagrant file is provided that can be used to create an environment to safely run these tests locally as root. With [Vagrant] and [VirtualBox] installed you can do the following to run the tests. ```bash vagrant up vagrant ssh sudo -i cd /vagrant cargo test ``` Note that the tests are built with root privileges, but since vagrant maps this directory back to the host as your normal user there is minimal issues with this. At worst the vagrant box will become trashed and can be rebuilt in minutes. [vagrant]: https://www.vagrantup.com/docs/installation/ [virtualbox]: https://www.virtualbox.org/ loopdev-0.4.0/Vagrantfile000064400000000000000000000004470072674642500135170ustar 00000000000000Vagrant.configure("2") do |config| config.vm.box = "ubuntu/focal64" config.vm.provision "shell", inline: <<-EOS curl https://sh.rustup.rs -sSf | sh -s -- -y apt update && apt install -y gcc clang fallocate -l 128M /tmp/disk.img mv /tmp/disk.img /vagrant/ EOS end loopdev-0.4.0/build.rs000064400000000000000000000007760072674642500130040ustar 00000000000000extern 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"); } loopdev-0.4.0/src/lib.rs000064400000000000000000000270120072674642500132320ustar 00000000000000// Supress warnings generated by bindgen: https://github.com/rust-lang/rust-bindgen/issues/1651 #![allow(deref_nullptr)] #![allow(unknown_lints)] //! Setup and control loop devices. //! //! Provides rust interface with similar functionality to the Linux utility `losetup`. //! //! # Examples //! //! Default options: //! //! ```rust //! use loopdev::LoopControl; //! let lc = LoopControl::open().unwrap(); //! let ld = lc.next_free().unwrap(); //! //! println!("{}", ld.path().unwrap().display()); //! //! ld.attach_file("disk.img").unwrap(); //! // ... //! ld.detach().unwrap(); //! ``` //! //! Custom options: //! //! ```rust //! # use loopdev::LoopControl; //! # let lc = LoopControl::open().unwrap(); //! # let ld = lc.next_free().unwrap(); //! # //! ld.with() //! .part_scan(true) //! .offset(512 * 1024 * 1024) // 512 MiB //! .size_limit(1024 * 1024 * 1024) // 1GiB //! .attach("disk.img").unwrap(); //! // ... //! ld.detach().unwrap(); //! ``` extern crate libc; #[cfg(feature = "direct_io")] use bindings::LOOP_SET_DIRECT_IO; use bindings::{ loop_info64, LOOP_CLR_FD, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_FD, LOOP_SET_STATUS64, LO_FLAGS_AUTOCLEAR, LO_FLAGS_READ_ONLY, }; use libc::{c_int, ioctl}; use std::{ default::Default, fs::{File, OpenOptions}, 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(all(not(target_os = "android"), not(target_env = "musl")))] type IoctlRequest = libc::c_ulong; #[cfg(any(target_os = "android", target_env = "musl"))] 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)] pub struct LoopControl { dev_file: File, } impl LoopControl { /// Opens the loop control device. pub fn open() -> io::Result { Ok(Self { dev_file: OpenOptions::new() .read(true) .write(true) .open(LOOP_CONTROL)?, }) } /// Finds and opens the next available loop device. /// /// # Examples /// /// ```rust /// use loopdev::LoopControl; /// let lc = LoopControl::open().unwrap(); /// let ld = lc.next_free().unwrap(); /// println!("{}", ld.path().unwrap().display()); /// ``` 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 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 { device: File, } impl AsRawFd for LoopDevice { fn as_raw_fd(&self) -> RawFd { self.device.as_raw_fd() } } impl IntoRawFd for LoopDevice { fn into_raw_fd(self) -> RawFd { self.device.into_raw_fd() } } impl LoopDevice { /// Opens a loop device. pub fn open>(dev: P) -> io::Result { // TODO create dev if it does not exist and begins with LOOP_PREFIX Ok(Self { device: OpenOptions::new().read(true).write(true).open(dev)?, }) } /// Attach the loop device to a file with given options. /// /// # Examples /// /// Attach the device to a file. /// /// ```rust /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop3").unwrap(); /// ld.with().part_scan(true).attach("disk.img").unwrap(); /// # ld.detach().unwrap(); /// ``` pub fn with(&self) -> AttachOptions<'_> { AttachOptions { device: self, info: Default::default(), #[cfg(feature = "direct_io")] direct_io: false, } } /// Attach the loop device to a file that maps to the whole file. /// /// # Examples /// /// Attach the device to a file. /// /// ```rust /// use loopdev::LoopDevice; /// let ld = LoopDevice::open("/dev/loop4").unwrap(); /// ld.attach_file("disk.img").unwrap(); /// # ld.detach().unwrap(); /// ``` pub fn attach_file>(&self, backing_file: P) -> io::Result<()> { let info = loop_info64 { ..Default::default() }; Self::attach_with_loop_info(self, backing_file, 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: loop_info64, ) -> io::Result<()> { let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0; let bf = OpenOptions::new() .read(true) .write(write_access) .open(backing_file)?; self.attach_fd_with_loop_info(bf, info) } /// Attach the loop device to a fd with loop_info. 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 as IoctlRequest, bf.as_raw_fd() as c_int, ) })?; let result = unsafe { ioctl( self.device.as_raw_fd() as c_int, LOOP_SET_STATUS64 as IoctlRequest, &info, ) }; match ioctl_to_error(result) { Err(err) => { // Ignore the error to preserve the original error let _ = self.detach(); Err(err) } Ok(_) => Ok(()), } } /// Get the path of the loop device. pub fn path(&self) -> Option { let mut p = PathBuf::from("/proc/self/fd"); p.push(self.device.as_raw_fd().to_string()); std::fs::read_link(&p).ok() } /// Get the device major number pub fn major(&self) -> io::Result { self.device .metadata() .map(|m| unsafe { libc::major(m.rdev()) as u32 }) } /// Get the device major number pub fn minor(&self) -> io::Result { self.device .metadata() .map(|m| unsafe { libc::minor(m.rdev()) as u32 }) } /// 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 /// gets closed. This happens when LoopDev goes out of scope so you should ensure the LoopDev /// lives for a short a time as possible. /// /// # Examples /// /// ```rust /// use loopdev::LoopDevice; /// let ld = LoopDevice::open("/dev/loop5").unwrap(); /// # ld.attach_file("disk.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 as IoctlRequest, 0, ) })?; Ok(()) } /// Resize a live loop device. If the size of the backing file changes this can be called to /// inform the loop driver about the new size. pub fn set_capacity(&self) -> io::Result<()> { ioctl_to_error(unsafe { ioctl( self.device.as_raw_fd() as c_int, LOOP_SET_CAPACITY as IoctlRequest, 0, ) })?; Ok(()) } // Enable or disable direct I/O for the backing file. #[cfg(feature = "direct_io")] 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()]. /// /// # Examples /// /// Enable partition scanning on attach: /// /// ```rust /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop6").unwrap(); /// ld.with() /// .part_scan(true) /// .attach("disk.img") /// .unwrap(); /// # ld.detach().unwrap(); /// ``` /// /// A 1MiB slice of the file located at 1KiB into the file. /// /// ```rust /// use loopdev::LoopDevice; /// let mut ld = LoopDevice::open("/dev/loop7").unwrap(); /// ld.with() /// .offset(1024*1024) /// .size_limit(1024*1024*1024) /// .attach("disk.img") /// .unwrap(); /// # ld.detach().unwrap(); /// ``` pub struct AttachOptions<'d> { device: &'d LoopDevice, info: loop_info64, #[cfg(feature = "direct_io")] direct_io: bool, } impl AttachOptions<'_> { /// Offset in bytes from the start of the backing file the data will start at. pub fn offset(mut self, offset: u64) -> Self { self.info.lo_offset = offset; self } /// Maximum size of the data in bytes. pub fn size_limit(mut self, size_limit: u64) -> Self { self.info.lo_sizelimit = size_limit; self } /// Set read only flag pub fn read_only(mut self, read_only: bool) -> Self { if read_only { self.info.lo_flags |= LO_FLAGS_READ_ONLY; } else { self.info.lo_flags &= !LO_FLAGS_READ_ONLY; } self } /// Set autoclear flag pub fn autoclear(mut self, read_only: bool) -> Self { if read_only { self.info.lo_flags |= LO_FLAGS_AUTOCLEAR; } else { self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR; } self } // Enable or disable direct I/O for the backing file. #[cfg(feature = "direct_io")] 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 { if enable { self.info.lo_flags |= 1 << 4; } else { self.info.lo_flags &= u32::max_value() - (1 << 4); } self } /// 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)?; #[cfg(feature = "direct_io")] if self.direct_io { self.device.set_direct_io(self.direct_io)?; } Ok(()) } /// 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)?; #[cfg(feature = "direct_io")] if self.direct_io { self.device.set_direct_io(self.direct_io)?; } Ok(()) } } fn ioctl_to_error(ret: i32) -> io::Result { if ret < 0 { Err(io::Error::last_os_error()) } else { Ok(ret) } } loopdev-0.4.0/tests/integration_test.rs000064400000000000000000000120560072674642500164230ustar 00000000000000extern crate libc; extern crate loopdev; extern crate tempfile; #[macro_use] extern crate lazy_static; 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}; #[test] fn get_next_free_device() { let num_devices_at_start = list_device(None).len(); let _lock = setup(); let lc = LoopControl::open().expect("should be able to open the LoopControl device"); let ld0 = lc .next_free() .expect("should not error finding the next free loopback device"); assert_eq!( ld0.path(), Some(PathBuf::from(&format!( "/dev/loop{}", num_devices_at_start ))), "should find the first loopback device" ); } #[test] fn attach_a_backing_file_default() { attach_a_backing_file(0, 0, 128 * 1024 * 1024); } #[test] fn attach_a_backing_file_with_offset() { attach_a_backing_file(128 * 1024, 0, 128 * 1024 * 1024); } #[test] fn attach_a_backing_file_with_sizelimit() { attach_a_backing_file(0, 128 * 1024, 128 * 1024 * 1024); } #[test] fn attach_a_backing_file_with_offset_sizelimit() { attach_a_backing_file(128 * 1024, 128 * 1024, 128 * 1024 * 1024); } // This is also allowed by losetup, not sure what happens if you try to write to the file though. #[test] fn attach_a_backing_file_with_offset_overflow() { attach_a_backing_file(128 * 1024 * 1024 * 2, 0, 128 * 1024 * 1024); } // This is also allowed by losetup, not sure what happens if you try to write to the file though. #[test] fn attach_a_backing_file_with_sizelimit_overflow() { attach_a_backing_file(0, 128 * 1024 * 1024 * 2, 128 * 1024 * 1024); } fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { let _lock = setup(); let (devices, ld0_path, file_path) = { let lc = LoopControl::open().expect("should be able to open the LoopControl device"); let file = create_backing_file(file_size); let file_path = file.to_path_buf(); let ld0 = lc .next_free() .expect("should not error finding the next free loopback device"); ld0.with() .offset(offset) .size_limit(sizelimit) .attach(&file) .expect("should not error attaching the backing file to the loopdev"); let devices = list_device(Some(ld0.path().unwrap().to_str().unwrap())); file.close().expect("should delete the temp backing file"); (devices, ld0.path().unwrap(), file_path) }; assert_eq!( devices.len(), 1, "there should be only one loopback mounted device" ); assert_eq!( devices[0].name.as_str(), ld0_path.to_str().unwrap(), "the attached devices name should match the input name" ); assert_eq!( devices[0].back_file.clone().unwrap().as_str(), file_path.to_str().unwrap(), "the backing file should match the given file" ); assert_eq!( devices[0].offset, Some(offset), "the offset should match the requested offset" ); assert_eq!( devices[0].size_limit, Some(sizelimit), "the sizelimit should match the requested sizelimit" ); detach_all(); } #[test] fn detach_a_backing_file_default() { detach_a_backing_file(0, 0, 128 * 1024 * 1024); } #[test] fn detach_a_backing_file_with_offset() { detach_a_backing_file(128 * 1024, 0, 128 * 1024 * 1024); } #[test] fn detach_a_backing_file_with_sizelimit() { detach_a_backing_file(0, 128 * 1024, 128 * 1024 * 1024); } #[test] fn detach_a_backing_file_with_offset_sizelimit() { detach_a_backing_file(128 * 1024, 128 * 1024, 128 * 1024 * 1024); } // This is also allowed by losetup, not sure what happens if you try to write to the file though. #[test] fn detach_a_backing_file_with_offset_overflow() { detach_a_backing_file(128 * 1024 * 1024 * 2, 0, 128 * 1024 * 1024); } // This is also allowed by losetup, not sure what happens if you try to write to the file though. #[test] fn detach_a_backing_file_with_sizelimit_overflow() { detach_a_backing_file(0, 128 * 1024 * 1024 * 2, 128 * 1024 * 1024); } fn detach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { let num_devices_at_start = list_device(None).len(); let _lock = setup(); { let file = create_backing_file(file_size); attach_file( "/dev/loop3", file.to_path_buf().to_str().unwrap(), offset, sizelimit, ); let ld0 = LoopDevice::open("/dev/loop3") .expect("should be able to open the created loopback device"); ld0.detach() .expect("should not error detaching the backing file from the loopdev"); file.close().expect("should delete the temp backing file"); }; std::thread::sleep(std::time::Duration::from_millis(100)); assert_eq!( list_device(None).len(), num_devices_at_start, "there should be no loopback devices mounted" ); detach_all(); } loopdev-0.4.0/tests/util/mod.rs000064400000000000000000000065720072674642500146030ustar 00000000000000use libc::fallocate; use serde::{Deserialize, Deserializer}; use std::{ io, os::unix::io::AsRawFd, process::Command, sync::{Arc, Mutex, MutexGuard}, }; use tempfile::{NamedTempFile, TempPath}; // All tests use the same loopback device interface and so can tread on each others toes leading to // racy tests. So we need to lock all tests to ensure only one runs at a time. lazy_static! { static ref LOCK: Arc> = Arc::new(Mutex::new(())); } pub fn create_backing_file(size: i64) -> TempPath { let file = NamedTempFile::new().expect("should be able to create a temp file"); if unsafe { fallocate(file.as_raw_fd(), 0, 0, size) } < 0 { panic!( "should be able to allocate the tenp file: {}", io::Error::last_os_error() ); } file.into_temp_path() } pub fn setup() -> MutexGuard<'static, ()> { let lock = LOCK.lock().unwrap(); detach_all(); 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") } } pub fn detach_all() { std::thread::sleep(std::time::Duration::from_millis(10)); if !Command::new("losetup") .args(&["-D"]) .status() .expect("failed to cleanup existing loop devices") .success() { panic!("failed to cleanup existing loop devices") } std::thread::sleep(std::time::Duration::from_millis(10)); } pub fn list_device(dev_file: Option<&str>) -> Vec { let mut output = Command::new("losetup"); output.args(&["-J", "-l"]); if let Some(dev_file) = dev_file { output.arg(dev_file); } let output = output .output() .expect("failed to cleanup existing loop devices"); if output.stdout.is_empty() { Vec::new() } else { serde_json::from_slice::(&output.stdout) .unwrap() .loopdevices } } #[derive(Deserialize, Debug)] pub struct LoopDeviceOutput { pub name: String, #[serde(rename = "sizelimit")] #[serde(deserialize_with = "deserialize_optional_number_from_string")] pub size_limit: Option, #[serde(deserialize_with = "deserialize_optional_number_from_string")] pub offset: Option, #[serde(rename = "back-file")] //#[serde(deserialize_with = "deserialize_nullable_string")] pub back_file: Option, } #[derive(Deserialize, Debug)] pub struct ListOutput { pub loopdevices: Vec, } pub fn deserialize_optional_number_from_string<'de, D>( deserializer: D, ) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrInt { String(Option), Number(Option), } match StringOrInt::deserialize(deserializer)? { StringOrInt::String(None) | StringOrInt::Number(None) => Ok(None), StringOrInt::String(Some(s)) => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)), StringOrInt::Number(Some(i)) => Ok(Some(i)), } }