├── .gitignore ├── Cross.toml ├── .cargo └── config.toml ├── losetup ├── Cargo.toml ├── src │ └── main.rs └── Cargo.lock ├── Vagrantfile ├── .travis-deploy ├── Cargo.toml ├── LICENSE ├── .travis.yml ├── README.md ├── .github └── workflows │ └── ci.yml ├── tests ├── util │ └── mod.rs └── integration_test.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /Cargo.lock 3 | test.img 4 | disk.img 5 | *.rustfmt 6 | .vagrant 7 | *.log 8 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | pre-build = [ 3 | # Bindgen dependencies 4 | "apt-get update && apt-get install --assume-yes --no-install-recommends libclang-dev", 5 | ] 6 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu.env] 2 | BINDGEN_EXTRA_CLANG_ARGS = "-I/usr/aarch64-linux-gnu/include" 3 | 4 | [target.aarch64-unknown-linux-musl.env] 5 | BINDGEN_EXTRA_CLANG_ARGS="-I/usr/local/aarch64-linux-musl/include" -------------------------------------------------------------------------------- /losetup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Michael Daffin "] 3 | name = "losetup" 4 | version = "0.2.0" 5 | description = "Setup and control loopback devices" 6 | 7 | [dependencies] 8 | clap = "2.34.0" 9 | 10 | [dependencies.loopdev] 11 | optional = false 12 | path = ".." 13 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "archlinux/archlinux" 3 | config.vm.provision "shell", 4 | inline: <<-EOS 5 | set -eo 6 | pacman -Syu --noconfirm rustup base-devel clang 7 | rustup default stable 8 | fallocate -l 128M /tmp/disk.img 9 | mv /tmp/disk.img /vagrant/ 10 | EOS 11 | end 12 | -------------------------------------------------------------------------------- /.travis-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -uo pipefail 3 | trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR 4 | IFS=$'\n\t' 5 | 6 | PKGID="$(cargo pkgid)" 7 | echo "Cargo version: ${PKGID##*#}" 8 | echo "Travis tag: ${TRAVIS_TAG}" 9 | 10 | if [[ "${PKGID##*#}" == "$TRAVIS_TAG" ]]; then 11 | cargo publish --token "$CARGO_TOKEN" 12 | else 13 | echo "Git tag '${TRAVIS_TAG}' does not match cargo version '${PKGID##*#}'" 1>&2 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loopdev" 3 | description = "Setup and control loop devices" 4 | version = "0.5.0" 5 | authors = ["Michael Daffin "] 6 | license = "MIT" 7 | documentation = "https://docs.rs/loopdev" 8 | repository = "https://github.com/mdaffin/loopdev" 9 | readme = "README.md" 10 | keywords = ["loop", "losetup"] 11 | edition = "2021" 12 | 13 | [badges] 14 | build = { status = "https://github.com/mdaffin/loopdev/actions/workflows/ci.yml/badge.svg" } 15 | 16 | [features] 17 | direct_io = [] 18 | 19 | [dependencies] 20 | errno = "0.2.8" 21 | libc = "0.2.105" 22 | 23 | [build-dependencies] 24 | bindgen = { version = "0.63.0", default-features = false, features = ["runtime"] } 25 | 26 | [dev-dependencies] 27 | glob = "0.3.0" 28 | gpt = "3.0.0" 29 | lazy_static = "1.4.0" 30 | serde = { version = "1.0.130", features = ["derive"] } 31 | serde_json = "1.0.68" 32 | tempfile = "3.2.0" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael Daffin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: rust 3 | addons: 4 | apt: 5 | packages: 6 | - libcurl4-openssl-dev 7 | - libelf-dev 8 | - libdw-dev 9 | 10 | rust: 11 | - nightly 12 | - beta 13 | - stable 14 | - 1.52.0 15 | - 1.51.0 16 | - 1.50.0 17 | 18 | matrix: 19 | allow_failures: 20 | - rust: nightly 21 | 22 | before_script: 23 | - fallocate -l 128M disk.img 24 | - '[[ "$TRAVIS_RUST_VERSION" != "stable" ]] || rustup component add clippy-preview' 25 | 26 | env: 27 | global: 28 | - TRAVIS_CARGO_NIGHTLY_FEATURE="" 29 | - secure: "yxpJSNW4ASzz983BpHFHE3uoS+4eyB1YIZjZjtL+1/VLESMp5ZEx5nfYdcbg/yA45nsldERZaLEtyhhgbnSOofdzPsZjxrmF2B+ePheTg4GEvH4HGg1Gz8Phmkjd4PRt4w8ji0Qk6h0NruUMMImUBYLRAAB6z2iKQCaCftKhBtQDXtJ6XUe7xom4McooLsBYuJgY//Fjn/BbvClwn3RfpSWttEQJ6j8gQlMLxtLNxtQ8cf/NShkGmRyl4U0QqURBZqvR2LstnqVkYEVifJoWfnIn9qCKYTouj4tRxy3IUwOPFZ9/SvcN639zP8V4qy1vJ146GyfEDpN4mtFP23U6gBbnwVGaaPeGyVVCYYbPfFWbgSs0GUt2JCUTAJaHuIXCrUb8Tlcj3a5smd7ffwdoSK29arTkHF2j94GmTdDZLeGmLtOPz9GGHLnafmNyd8C2UW4psxapskEXaixAYg81R907dW4ZitDxKygnsBHHYzJoHP94cCqR+n6rad9z/YW0ZtMWgEenEkr4qht/jEMRz23a1kLEfmphCAPNzYI3jXFYaBq/sZHeoe/d5cSWxOyeUti9cLtlnpDOEi0S+M9Fczz/iH9EU3a0pQt1KGuOIzExcIV3WmpBErEaF+Xj0Vw7BfFt7yJAk0EAv1nCM8untqoFnmQQCYeiOP+XbhjHzjo=" 30 | 31 | script: 32 | - '[[ "$TRAVIS_RUST_VERSION" != "stable" ]] || cargo clippy -- -D warnings' 33 | - cargo build 34 | - sudo -E env "PATH=$PATH" cargo test --jobs 1 35 | 36 | deploy: 37 | - provider: script 38 | skip_cleanup: true 39 | on: 40 | tags: true 41 | condition: "$TRAVIS_RUST_VERSION = stable" 42 | script: ./.travis-deploy 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/mdaffin/loopdev/actions/workflows/ci.yml/badge.svg)](https://github.com/mdaffin/loopdev/actions/workflows/ci.yml) 2 | [![crates.io](https://img.shields.io/crates/v/loopdev.svg)](https://crates.io/crates/loopdev) 3 | 4 | # loopdev 5 | 6 | Setup and control loop devices. 7 | 8 | Provides rust interface with similar functionality to the Linux utility `losetup`. 9 | 10 | ## [Documentation](https://docs.rs/loopdev) 11 | 12 | ## Examples 13 | 14 | ```rust 15 | use loopdev::LoopControl; 16 | let lc = LoopControl::open().unwrap(); 17 | let ld = lc.next_free().unwrap(); 18 | 19 | println!("{}", ld.path().unwrap().display()); 20 | 21 | ld.attach_file("disk.img").unwrap(); 22 | // ... 23 | ld.detach().unwrap(); 24 | ``` 25 | 26 | ## Development 27 | 28 | ### Running The Tests Locally 29 | 30 | Unfortunately the tests require root only syscalls and thus must be run as root. 31 | There is little point in mocking out these syscalls as I want to test they 32 | actually function as expected and if they were to be mocked out then the tests 33 | would not really be testing anything useful. 34 | 35 | A vagrant file is provided that can be used to create an environment to safely 36 | run these tests locally as root. With [Vagrant] and [VirtualBox] installed you 37 | can do the following to run the tests. 38 | 39 | ```bash 40 | vagrant up 41 | vagrant ssh 42 | sudo -i 43 | cd /vagrant 44 | cargo test 45 | ``` 46 | 47 | Note that the tests are built with root privileges, but since vagrant maps this 48 | directory back to the host as your normal user there is minimal issues with 49 | this. At worst the vagrant box will become trashed and can be rebuilt in 50 | minutes. 51 | 52 | [vagrant]: https://www.vagrantup.com/docs/installation/ 53 | [virtualbox]: https://www.virtualbox.org/ 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: loopdev 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | target: 12 | - aarch64-linux-android 13 | - aarch64-unknown-linux-gnu 14 | - aarch64-unknown-linux-musl 15 | - x86_64-unknown-linux-gnu 16 | - x86_64-unknown-linux-musl 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v3 20 | 21 | - name: Install stable toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | target: ${{ matrix.target }} 27 | override: true 28 | - name: Cross 29 | run: cargo install --git https://github.com/cross-rs/cross.git --rev bb3df1b cross 30 | 31 | - name: Run cargo check 32 | uses: actions-rs/cargo@v1 33 | with: 34 | use-cross: true 35 | command: check 36 | args: --target=${{ matrix.target }} 37 | 38 | lints: 39 | name: Lints 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout sources 43 | uses: actions/checkout@v3 44 | 45 | - name: Install stable toolchain 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | override: true 51 | components: rustfmt, clippy 52 | 53 | - name: Install bindgen dependencies 54 | run: sudo apt-get install llvm-dev libclang-dev clang 55 | 56 | - name: Run cargo fmt 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: fmt 60 | args: --all -- --check 61 | 62 | - name: Run cargo clippy 63 | uses: actions-rs/cargo@v1 64 | with: 65 | command: clippy 66 | args: -- -D warnings 67 | -------------------------------------------------------------------------------- /losetup/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate loopdev; 4 | 5 | use loopdev::{LoopControl, LoopDevice}; 6 | use std::io::{self, Write}; 7 | use std::process::exit; 8 | 9 | fn find() -> io::Result<()> { 10 | println!( 11 | "{}", 12 | LoopControl::open()?.next_free()?.path().unwrap().display() 13 | ); 14 | Ok(()) 15 | } 16 | 17 | fn attach(matches: &clap::ArgMatches) -> io::Result<()> { 18 | let quiet = matches.is_present("quiet"); 19 | let image = matches.value_of("image").unwrap(); 20 | let offset = value_t!(matches.value_of("offset"), u64).unwrap_or(0); 21 | let size_limit = value_t!(matches.value_of("sizelimit"), u64).unwrap_or(0); 22 | let read_only = matches.is_present("read_only"); 23 | let auto_clear = matches.is_present("auto_clear"); 24 | let part_scan = matches.is_present("part_scan"); 25 | let loopdev = match matches.value_of("loopdev") { 26 | Some(loopdev) => LoopDevice::open(&loopdev)?, 27 | None => LoopControl::open().and_then(|lc| lc.next_free())?, 28 | }; 29 | loopdev 30 | .with() 31 | .offset(offset) 32 | .size_limit(size_limit) 33 | .read_only(read_only) 34 | .autoclear(auto_clear) 35 | .part_scan(part_scan) 36 | .attach(image)?; 37 | 38 | if !quiet { 39 | println!("{}", loopdev.path().unwrap().display()); 40 | } 41 | Ok(()) 42 | } 43 | 44 | fn detach(matches: &clap::ArgMatches) -> io::Result<()> { 45 | let loopdev = matches.value_of("file").unwrap(); 46 | LoopDevice::open(loopdev)?.detach() 47 | } 48 | 49 | fn set_capacity(matches: &clap::ArgMatches) -> io::Result<()> { 50 | let loopdev = matches.value_of("file").unwrap(); 51 | LoopDevice::open(loopdev)?.set_capacity() 52 | } 53 | 54 | fn list(matches: Option<&clap::ArgMatches>) -> io::Result<()> { 55 | let (_free, _used) = match matches { 56 | Some(matches) => (matches.is_present("free"), matches.is_present("used")), 57 | None => (false, false), 58 | }; 59 | unimplemented!(); 60 | } 61 | 62 | fn main() { 63 | let matches = clap_app!(losetup => 64 | (version: crate_version!()) 65 | (author: crate_authors!()) 66 | (about: crate_description!()) 67 | (@subcommand find => 68 | (about: "find the next free loop device") 69 | ) 70 | (@subcommand attach => 71 | (about: "attach the loop device to a backing file") 72 | (@arg image: +required "the backing file to attach") 73 | (@arg loopdev: "the loop device to attach") 74 | (@arg offset: -o --offset +takes_value "the offset within the file to start at") 75 | (@arg sizelimit: -s --sizelimit +takes_value "the file is limited to this size") 76 | (@arg read_only: -r --readonly "set up a read-only loop device") 77 | (@arg auto_clear: -a --autoclear "set the autoclear flag") 78 | (@arg part_scan: -p --partscan "set the part-scan flag") 79 | (@arg quiet: -q --quiet "don't print the device name") 80 | ) 81 | (@subcommand detach => 82 | (about: "detach the loop device from the backing file") 83 | (@arg file: +required "The file to detach") 84 | ) 85 | (@subcommand setcapacity => 86 | (about: "inform the loop driver of a change in size of the backing file") 87 | (@arg file: +required "The file to set the capacity of") 88 | ) 89 | (@subcommand list => 90 | (about: "list the available loop devices") 91 | (@arg free: -f --free "find free devices") 92 | (@arg used: -u --used "find used devices") 93 | ) 94 | ) 95 | .get_matches(); 96 | 97 | let result = match matches.subcommand() { 98 | ("find", _) => find(), 99 | ("attach", Some(matches)) => attach(matches), 100 | ("detach", Some(matches)) => detach(matches), 101 | ("setcapacity", Some(matches)) => set_capacity(matches), 102 | (_, matches) => list(matches), 103 | }; 104 | 105 | if let Err(err) = result { 106 | writeln!(&mut std::io::stderr(), "{}", err).unwrap(); 107 | exit(1); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | use libc::fallocate; 2 | use serde::{Deserialize, Deserializer}; 3 | use std::{ 4 | fs::OpenOptions, 5 | io, 6 | os::unix::io::AsRawFd, 7 | path::Path, 8 | process::Command, 9 | sync::{Arc, Mutex, MutexGuard}, 10 | }; 11 | 12 | use tempfile::{NamedTempFile, TempPath}; 13 | 14 | // All tests use the same loopback device interface and so can tread on each others toes leading to 15 | // racy tests. So we need to lock all tests to ensure only one runs at a time. 16 | lazy_static::lazy_static! { 17 | static ref LOCK: Arc> = Arc::new(Mutex::new(())); 18 | } 19 | 20 | pub fn create_backing_file(size: i64) -> TempPath { 21 | let file = NamedTempFile::new().expect("should be able to create a temp file"); 22 | assert!( 23 | unsafe { fallocate(file.as_raw_fd(), 0, 0, size) } >= 0, 24 | "should be able to allocate the temp file: {}", 25 | io::Error::last_os_error() 26 | ); 27 | file.into_temp_path() 28 | } 29 | 30 | pub fn partition_backing_file(backing_file: impl AsRef, size: u64) { 31 | gpt::mbr::ProtectiveMBR::new() 32 | .overwrite_lba0(&mut OpenOptions::new().write(true).open(&backing_file).unwrap()) 33 | .expect("failed to write MBR"); 34 | 35 | let mut disk = gpt::GptConfig::new() 36 | .initialized(false) 37 | .writable(true) 38 | .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) 39 | .open(backing_file) 40 | .expect("could not open backing file"); 41 | 42 | disk.update_partitions(std::collections::BTreeMap::::new()) 43 | .expect("coult not initialize blank partition table"); 44 | 45 | disk.add_partition( 46 | "Linux filesystem", 47 | size, 48 | gpt::partition_types::LINUX_FS, 49 | 0, 50 | None, 51 | ) 52 | .expect("could not create partition"); 53 | 54 | disk.write() 55 | .expect("could not write partition table to backing file"); 56 | } 57 | 58 | pub fn setup() -> MutexGuard<'static, ()> { 59 | let lock = LOCK.lock().unwrap(); 60 | detach_all(); 61 | lock 62 | } 63 | 64 | pub fn attach_file(loop_dev: &str, backing_file: &str, offset: u64, sizelimit: u64) { 65 | if !Command::new("losetup") 66 | .args([ 67 | loop_dev, 68 | backing_file, 69 | "--offset", 70 | &offset.to_string(), 71 | "--sizelimit", 72 | &sizelimit.to_string(), 73 | ]) 74 | .status() 75 | .expect("failed to attach backing file to loop device") 76 | .success() 77 | { 78 | panic!("failed to cleanup existing loop devices") 79 | } 80 | } 81 | 82 | pub fn detach_all() { 83 | std::thread::sleep(std::time::Duration::from_millis(10)); 84 | if !Command::new("losetup") 85 | .args(["-D"]) 86 | .status() 87 | .expect("failed to cleanup existing loop devices") 88 | .success() 89 | { 90 | panic!("failed to cleanup existing loop devices") 91 | } 92 | std::thread::sleep(std::time::Duration::from_millis(10)); 93 | } 94 | 95 | pub fn list_device(dev_file: Option<&str>) -> Vec { 96 | let mut output = Command::new("losetup"); 97 | output.args(["-J", "-l"]); 98 | if let Some(dev_file) = dev_file { 99 | output.arg(dev_file); 100 | } 101 | let output = output 102 | .output() 103 | .expect("failed to cleanup existing loop devices"); 104 | 105 | if output.stdout.is_empty() { 106 | Vec::new() 107 | } else { 108 | serde_json::from_slice::(&output.stdout) 109 | .unwrap() 110 | .loopdevices 111 | } 112 | } 113 | 114 | #[derive(Deserialize, Debug)] 115 | pub struct LoopDeviceOutput { 116 | pub name: String, 117 | #[serde(rename = "sizelimit")] 118 | #[serde(deserialize_with = "deserialize_optional_number_from_string")] 119 | pub size_limit: Option, 120 | #[serde(deserialize_with = "deserialize_optional_number_from_string")] 121 | pub offset: Option, 122 | #[serde(rename = "back-file")] 123 | //#[serde(deserialize_with = "deserialize_nullable_string")] 124 | pub back_file: Option, 125 | } 126 | 127 | #[derive(Deserialize, Debug)] 128 | pub struct ListOutput { 129 | pub loopdevices: Vec, 130 | } 131 | 132 | pub fn deserialize_optional_number_from_string<'de, D>( 133 | deserializer: D, 134 | ) -> Result, D::Error> 135 | where 136 | D: Deserializer<'de>, 137 | { 138 | #[derive(Deserialize)] 139 | #[serde(untagged)] 140 | enum StringOrInt { 141 | String(Option), 142 | Number(Option), 143 | } 144 | 145 | match StringOrInt::deserialize(deserializer)? { 146 | StringOrInt::String(None) | StringOrInt::Number(None) => Ok(None), 147 | StringOrInt::String(Some(s)) => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)), 148 | StringOrInt::Number(Some(i)) => Ok(Some(i)), 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use loopdev::{LoopControl, LoopDevice}; 2 | use std::path::PathBuf; 3 | 4 | mod util; 5 | use crate::util::{ 6 | attach_file, create_backing_file, detach_all, list_device, partition_backing_file, setup, 7 | }; 8 | 9 | #[test] 10 | fn get_next_free_device() { 11 | let num_devices_at_start = list_device(None).len(); 12 | let _lock = setup(); 13 | 14 | let lc = LoopControl::open().expect("should be able to open the LoopControl device"); 15 | let ld0 = lc 16 | .next_free() 17 | .expect("should not error finding the next free loopback device"); 18 | 19 | assert_eq!( 20 | ld0.path(), 21 | Some(PathBuf::from(&format!("/dev/loop{}", num_devices_at_start))), 22 | "should find the first loopback device" 23 | ); 24 | } 25 | 26 | #[test] 27 | fn attach_a_backing_file_default() { 28 | attach_a_backing_file(0, 0, 128 * 1024 * 1024); 29 | } 30 | 31 | #[test] 32 | fn attach_a_backing_file_with_offset() { 33 | attach_a_backing_file(128 * 1024, 0, 128 * 1024 * 1024); 34 | } 35 | 36 | #[test] 37 | fn attach_a_backing_file_with_sizelimit() { 38 | attach_a_backing_file(0, 128 * 1024, 128 * 1024 * 1024); 39 | } 40 | 41 | #[test] 42 | fn attach_a_backing_file_with_offset_sizelimit() { 43 | attach_a_backing_file(128 * 1024, 128 * 1024, 128 * 1024 * 1024); 44 | } 45 | 46 | // This is also allowed by losetup, not sure what happens if you try to write to the file though. 47 | #[test] 48 | fn attach_a_backing_file_with_offset_overflow() { 49 | attach_a_backing_file(128 * 1024 * 1024 * 2, 0, 128 * 1024 * 1024); 50 | } 51 | 52 | // This is also allowed by losetup, not sure what happens if you try to write to the file though. 53 | #[test] 54 | fn attach_a_backing_file_with_sizelimit_overflow() { 55 | attach_a_backing_file(0, 128 * 1024 * 1024 * 2, 128 * 1024 * 1024); 56 | } 57 | 58 | fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { 59 | let _lock = setup(); 60 | 61 | let (devices, ld0_path, file_path) = { 62 | let lc = LoopControl::open().expect("should be able to open the LoopControl device"); 63 | 64 | let file = create_backing_file(file_size); 65 | let file_path = file.to_path_buf(); 66 | let ld0 = lc 67 | .next_free() 68 | .expect("should not error finding the next free loopback device"); 69 | 70 | ld0.with() 71 | .offset(offset) 72 | .size_limit(sizelimit) 73 | .attach(&file) 74 | .expect("should not error attaching the backing file to the loopdev"); 75 | 76 | let devices = list_device(Some(ld0.path().unwrap().to_str().unwrap())); 77 | file.close().expect("should delete the temp backing file"); 78 | 79 | (devices, ld0.path().unwrap(), file_path) 80 | }; 81 | 82 | assert_eq!( 83 | devices.len(), 84 | 1, 85 | "there should be only one loopback mounted device" 86 | ); 87 | assert_eq!( 88 | devices[0].name.as_str(), 89 | ld0_path.to_str().unwrap(), 90 | "the attached devices name should match the input name" 91 | ); 92 | assert_eq!( 93 | devices[0].back_file.clone().unwrap().as_str(), 94 | file_path.to_str().unwrap(), 95 | "the backing file should match the given file" 96 | ); 97 | assert_eq!( 98 | devices[0].offset, 99 | Some(offset), 100 | "the offset should match the requested offset" 101 | ); 102 | assert_eq!( 103 | devices[0].size_limit, 104 | Some(sizelimit), 105 | "the sizelimit should match the requested sizelimit" 106 | ); 107 | 108 | detach_all(); 109 | } 110 | 111 | #[test] 112 | fn detach_a_backing_file_default() { 113 | detach_a_backing_file(0, 0, 128 * 1024 * 1024); 114 | } 115 | 116 | #[test] 117 | fn detach_a_backing_file_with_offset() { 118 | detach_a_backing_file(128 * 1024, 0, 128 * 1024 * 1024); 119 | } 120 | 121 | #[test] 122 | fn detach_a_backing_file_with_sizelimit() { 123 | detach_a_backing_file(0, 128 * 1024, 128 * 1024 * 1024); 124 | } 125 | 126 | #[test] 127 | fn detach_a_backing_file_with_offset_sizelimit() { 128 | detach_a_backing_file(128 * 1024, 128 * 1024, 128 * 1024 * 1024); 129 | } 130 | 131 | // This is also allowed by losetup, not sure what happens if you try to write to the file though. 132 | #[test] 133 | fn detach_a_backing_file_with_offset_overflow() { 134 | detach_a_backing_file(128 * 1024 * 1024 * 2, 0, 128 * 1024 * 1024); 135 | } 136 | 137 | // This is also allowed by losetup, not sure what happens if you try to write to the file though. 138 | #[test] 139 | fn detach_a_backing_file_with_sizelimit_overflow() { 140 | detach_a_backing_file(0, 128 * 1024 * 1024 * 2, 128 * 1024 * 1024); 141 | } 142 | 143 | fn detach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { 144 | let num_devices_at_start = list_device(None).len(); 145 | let _lock = setup(); 146 | 147 | { 148 | let file = create_backing_file(file_size); 149 | attach_file( 150 | "/dev/loop5", 151 | file.to_path_buf().to_str().unwrap(), 152 | offset, 153 | sizelimit, 154 | ); 155 | 156 | let ld0 = LoopDevice::open("/dev/loop5") 157 | .expect("should be able to open the created loopback device"); 158 | 159 | ld0.detach() 160 | .expect("should not error detaching the backing file from the loopdev"); 161 | 162 | file.close().expect("should delete the temp backing file"); 163 | }; 164 | 165 | std::thread::sleep(std::time::Duration::from_millis(100)); 166 | 167 | assert_eq!( 168 | list_device(None).len(), 169 | num_devices_at_start, 170 | "there should be no loopback devices mounted" 171 | ); 172 | detach_all(); 173 | } 174 | 175 | #[test] 176 | fn attach_a_backing_file_with_part_scan_default() { 177 | attach_a_backing_file_with_part_scan(1024 * 1024); 178 | } 179 | 180 | fn attach_a_backing_file_with_part_scan(file_size: i64) { 181 | let _lock = setup(); 182 | 183 | let partitions = { 184 | let lc = LoopControl::open().expect("should be able to open the LoopControl device"); 185 | 186 | let file = create_backing_file(file_size); 187 | partition_backing_file(&file, 1024); 188 | 189 | let ld0 = lc 190 | .next_free() 191 | .expect("should not error finding the next free loopback device"); 192 | 193 | ld0.with() 194 | .part_scan(true) 195 | .attach(&file) 196 | .expect("should not error attaching the backing file to the loopdev"); 197 | let devices = list_device(Some(ld0.path().unwrap().to_str().unwrap())); 198 | let partitions = glob::glob(&format!("{}p*", devices[0].name)) 199 | .unwrap() 200 | .map(|entry| entry.unwrap().display().to_string()) 201 | .collect::>(); 202 | 203 | file.close().expect("should delete the temp backing file"); 204 | 205 | partitions 206 | }; 207 | 208 | assert_eq!( 209 | partitions.len(), 210 | 1, 211 | "there should be only one partition for the device" 212 | ); 213 | } 214 | 215 | #[test] 216 | fn add_a_loop_device() { 217 | let _lock = setup(); 218 | 219 | let lc = LoopControl::open().expect("should be able to open the LoopControl device"); 220 | assert!(lc.add(1).is_ok()); 221 | assert!(lc.add(1).is_err()); 222 | } 223 | -------------------------------------------------------------------------------- /losetup/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "bindgen" 27 | version = "0.60.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" 30 | dependencies = [ 31 | "bitflags", 32 | "cexpr", 33 | "clang-sys", 34 | "lazy_static", 35 | "lazycell", 36 | "peeking_take_while", 37 | "proc-macro2", 38 | "quote", 39 | "regex", 40 | "rustc-hash", 41 | "shlex", 42 | ] 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "1.3.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 49 | 50 | [[package]] 51 | name = "cc" 52 | version = "1.0.74" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" 55 | 56 | [[package]] 57 | name = "cexpr" 58 | version = "0.6.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 61 | dependencies = [ 62 | "nom", 63 | ] 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "clang-sys" 73 | version = "1.4.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" 76 | dependencies = [ 77 | "glob", 78 | "libc", 79 | "libloading", 80 | ] 81 | 82 | [[package]] 83 | name = "clap" 84 | version = "2.34.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 87 | dependencies = [ 88 | "ansi_term", 89 | "atty", 90 | "bitflags", 91 | "strsim", 92 | "textwrap", 93 | "unicode-width", 94 | "vec_map", 95 | ] 96 | 97 | [[package]] 98 | name = "errno" 99 | version = "0.2.8" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 102 | dependencies = [ 103 | "errno-dragonfly", 104 | "libc", 105 | "winapi", 106 | ] 107 | 108 | [[package]] 109 | name = "errno-dragonfly" 110 | version = "0.1.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 113 | dependencies = [ 114 | "cc", 115 | "libc", 116 | ] 117 | 118 | [[package]] 119 | name = "glob" 120 | version = "0.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 123 | 124 | [[package]] 125 | name = "hermit-abi" 126 | version = "0.1.19" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 129 | dependencies = [ 130 | "libc", 131 | ] 132 | 133 | [[package]] 134 | name = "lazy_static" 135 | version = "1.4.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 138 | 139 | [[package]] 140 | name = "lazycell" 141 | version = "1.3.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 144 | 145 | [[package]] 146 | name = "libc" 147 | version = "0.2.137" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 150 | 151 | [[package]] 152 | name = "libloading" 153 | version = "0.7.3" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" 156 | dependencies = [ 157 | "cfg-if", 158 | "winapi", 159 | ] 160 | 161 | [[package]] 162 | name = "loopdev" 163 | version = "0.5.0" 164 | dependencies = [ 165 | "bindgen", 166 | "errno", 167 | "libc", 168 | ] 169 | 170 | [[package]] 171 | name = "losetup" 172 | version = "0.2.0" 173 | dependencies = [ 174 | "clap", 175 | "loopdev", 176 | ] 177 | 178 | [[package]] 179 | name = "memchr" 180 | version = "2.5.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 183 | 184 | [[package]] 185 | name = "minimal-lexical" 186 | version = "0.2.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 189 | 190 | [[package]] 191 | name = "nom" 192 | version = "7.1.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 195 | dependencies = [ 196 | "memchr", 197 | "minimal-lexical", 198 | ] 199 | 200 | [[package]] 201 | name = "peeking_take_while" 202 | version = "0.1.2" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 205 | 206 | [[package]] 207 | name = "proc-macro2" 208 | version = "1.0.47" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 211 | dependencies = [ 212 | "unicode-ident", 213 | ] 214 | 215 | [[package]] 216 | name = "quote" 217 | version = "1.0.21" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 220 | dependencies = [ 221 | "proc-macro2", 222 | ] 223 | 224 | [[package]] 225 | name = "regex" 226 | version = "1.6.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 229 | dependencies = [ 230 | "regex-syntax", 231 | ] 232 | 233 | [[package]] 234 | name = "regex-syntax" 235 | version = "0.6.27" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 238 | 239 | [[package]] 240 | name = "rustc-hash" 241 | version = "1.1.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 244 | 245 | [[package]] 246 | name = "shlex" 247 | version = "1.1.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" 250 | 251 | [[package]] 252 | name = "strsim" 253 | version = "0.8.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 256 | 257 | [[package]] 258 | name = "textwrap" 259 | version = "0.11.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 262 | dependencies = [ 263 | "unicode-width", 264 | ] 265 | 266 | [[package]] 267 | name = "unicode-ident" 268 | version = "1.0.5" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 271 | 272 | [[package]] 273 | name = "unicode-width" 274 | version = "0.1.10" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 277 | 278 | [[package]] 279 | name = "vec_map" 280 | version = "0.8.2" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 283 | 284 | [[package]] 285 | name = "winapi" 286 | version = "0.3.9" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 289 | dependencies = [ 290 | "winapi-i686-pc-windows-gnu", 291 | "winapi-x86_64-pc-windows-gnu", 292 | ] 293 | 294 | [[package]] 295 | name = "winapi-i686-pc-windows-gnu" 296 | version = "0.4.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 299 | 300 | [[package]] 301 | name = "winapi-x86_64-pc-windows-gnu" 302 | version = "0.4.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 305 | -------------------------------------------------------------------------------- /src/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 | //! ``` 40 | use 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")] 45 | use bindings::LOOP_SET_DIRECT_IO; 46 | use libc::{c_int, ioctl}; 47 | use 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)] 58 | mod bindings { 59 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 60 | } 61 | 62 | #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] 63 | type IoctlRequest = libc::c_ulong; 64 | #[cfg(any(target_os = "android", target_env = "musl"))] 65 | type IoctlRequest = libc::c_int; 66 | 67 | const LOOP_CONTROL: &str = "/dev/loop-control"; 68 | #[cfg(not(target_os = "android"))] 69 | const LOOP_PREFIX: &str = "/dev/loop"; 70 | #[cfg(target_os = "android")] 71 | const LOOP_PREFIX: &str = "/dev/block/loop"; 72 | 73 | /// Interface to the loop control device: `/dev/loop-control`. 74 | #[derive(Debug)] 75 | pub struct LoopControl { 76 | dev_file: File, 77 | } 78 | 79 | impl 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 { 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 { 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 { 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 | 151 | impl AsRawFd for LoopControl { 152 | fn as_raw_fd(&self) -> RawFd { 153 | self.dev_file.as_raw_fd() 154 | } 155 | } 156 | 157 | impl 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)] 165 | pub struct LoopDevice { 166 | device: File, 167 | } 168 | 169 | impl AsRawFd for LoopDevice { 170 | fn as_raw_fd(&self) -> RawFd { 171 | self.device.as_raw_fd() 172 | } 173 | } 174 | 175 | impl IntoRawFd for LoopDevice { 176 | fn into_raw_fd(self) -> RawFd { 177 | self.device.into_raw_fd() 178 | } 179 | } 180 | 181 | impl 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>(dev: P) -> io::Result { 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>(&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, 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 { 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 { 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 { 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] 419 | pub struct AttachOptions<'d> { 420 | device: &'d LoopDevice, 421 | info: loop_info64, 422 | #[cfg(feature = "direct_io")] 423 | direct_io: bool, 424 | } 425 | 426 | impl 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) -> 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 | 512 | fn ioctl_to_error(ret: i32) -> io::Result { 513 | if ret < 0 { 514 | Err(io::Error::last_os_error()) 515 | } else { 516 | Ok(ret) 517 | } 518 | } 519 | --------------------------------------------------------------------------------