From 2ee5b40c85d4ec1bc46f80ec065cbb4b4448198d Mon Sep 17 00:00:00 2001 From: decay Date: Sun, 6 Jul 2025 22:34:28 +0400 Subject: [PATCH 1/4] add functions to get original file name and loop device info --- src/lib.rs | 51 ++++++++++++++++++++--- src/loname.rs | 86 +++++++++++++++++++++++++++++++++++++++ tests/integration_test.rs | 64 ++++++++++++++++++++++++++--- 3 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 src/loname.rs diff --git a/src/lib.rs b/src/lib.rs index 0d2d463..7b561ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ use crate::bindings::{ loop_info64, LOOP_CLR_FD, LOOP_CTL_ADD, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_FD, LOOP_SET_STATUS64, LO_FLAGS_AUTOCLEAR, LO_FLAGS_PARTSCAN, LO_FLAGS_READ_ONLY, }; +use bindings::LOOP_GET_STATUS64; #[cfg(feature = "direct_io")] use bindings::LOOP_SET_DIRECT_IO; use libc::{c_int, ioctl}; @@ -52,6 +53,8 @@ use std::{ path::{Path, PathBuf}, }; +use loname::Name; + #[allow(non_camel_case_types)] #[allow(dead_code)] #[allow(non_snake_case)] @@ -59,6 +62,8 @@ mod bindings { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); } +mod loname; + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] type IoctlRequest = libc::c_ulong; #[cfg(any(target_os = "android", target_env = "musl"))] @@ -236,20 +241,22 @@ impl LoopDevice { /// for further details) or when calling the ioctl to attach the backing /// file to the device. 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) + Self::attach_with_loop_info(self, backing_file, loop_info64::default()) } /// 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, + mut info: loop_info64, ) -> io::Result<()> { let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0; + + // store backing file name in the info + let name = loname::Name::from_path(&backing_file).unwrap_or(Name::default()); + info.lo_file_name = name.0.clone(); + info.lo_crypt_name = name.0; + let bf = OpenOptions::new() .read(true) .write(write_access) @@ -292,6 +299,23 @@ impl LoopDevice { std::fs::read_link(&p).ok() } + /// Try to obtain the original file path used on mapping + /// The method ignores ioctl errors + /// + /// # Return + /// The path expected to be stored in the loop_info64. + /// If it's not there, method returns None, otherwise - the string stored + pub fn original_path(&self) -> Option { + self.info().ok().and_then(|info| { + let name = Name(info.lo_file_name).to_string(); + if name.is_empty() { + None + } else { + Some(PathBuf::from(name)) + } + }) + } + /// Get the device major number /// /// # Errors @@ -370,6 +394,21 @@ impl LoopDevice { Ok(()) } + /// Obtain loop_info64 struct for the loop device + /// # Return + /// Ok(loop_info64) - successfully obtained info + /// Err(std::io::Error) - error from ioctl + pub fn info(&self) -> Result { + let empty_info = Box::new(loop_info64::default()); + let fd = self.device.as_raw_fd(); + + unsafe { + let ptr = Box::into_raw(empty_info); + let ret_code = libc::ioctl(fd.as_raw_fd(), LOOP_GET_STATUS64 as IoctlRequest, ptr); + ioctl_to_error(ret_code).map(|_| *Box::from_raw(ptr)) + } + } + /// Enable or disable direct I/O for the backing file. /// /// # Errors diff --git a/src/loname.rs b/src/loname.rs new file mode 100644 index 0000000..0cbf21d --- /dev/null +++ b/src/loname.rs @@ -0,0 +1,86 @@ +//! Configuration structures for loop device + +use std::path::Path; + +use crate::bindings::LO_NAME_SIZE; + +/// Loop device name +#[repr(C)] +#[derive(Debug)] +pub struct Name(pub [libc::__u8; LO_NAME_SIZE as usize]); + +/// Allow to construct the config easily +impl Default for Name { + fn default() -> Self { + Self([0; LO_NAME_SIZE as usize]) + } +} + +/// Conversions simplifiers +impl Name { + pub fn from_path>(path: Y) -> Result { + let s = path.as_ref().as_os_str().as_encoded_bytes(); + if s.len() > LO_NAME_SIZE as usize { + return Err(format!( + "too many bytes in the provided loop dev source file path: {}, max {LO_NAME_SIZE}", + s.len() + )); + } + let mut data: [u8; 64] = [0; LO_NAME_SIZE as usize]; + for (idx, byte) in s.into_iter().enumerate() { + data[idx] = *byte; + } + Ok(Self(data)) + } +} +impl ToString for Name { + fn to_string(&self) -> String { + self.0 + .iter() + .filter_map(|ch| { + let ch: char = *ch as char; + if ch == '\0' { + None + } else { + Some(ch) + } + }) + .collect::() + } +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use super::Name; + + #[test] + fn test_name_empty() { + let name = Name::default(); + for num in name.0 { + assert_eq!(0, num); + } + } + + #[test] + fn test_name_from_to() { + let path_string = "/a/b/some-file/cool name"; + let path = PathBuf::from(&path_string); + let name = Name::from_path(path); + + assert_eq!(path_string, name.unwrap().to_string()); + } + + #[test] + fn test_name_too_long() { + let path_string = "/too-long/too-long/too-long/too-long/too-long/too-long/too-long--"; + let path = PathBuf::from(&path_string); + let name = Name::from_path(path); + + assert_eq!( + "too many bytes in the provided loop dev source file path: 65, max 64", + name.unwrap_err() + ) + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 84bd1cc..95e17fc 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,5 +1,5 @@ use loopdev::{LoopControl, LoopDevice}; -use std::path::PathBuf; +use std::{fs::File, os::fd::AsFd, path::PathBuf}; mod util; use crate::util::{ @@ -58,7 +58,7 @@ fn attach_a_backing_file_with_sizelimit_overflow() { fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { let _lock = setup(); - let (devices, ld0_path, file_path) = { + let (mut 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); @@ -84,23 +84,26 @@ fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { 1, "there should be only one loopback mounted device" ); + + let device = devices.pop().unwrap(); + assert_eq!( - devices[0].name.as_str(), + device.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(), + device.back_file.clone().unwrap().as_str(), file_path.to_str().unwrap(), "the backing file should match the given file" ); assert_eq!( - devices[0].offset, + device.offset, Some(offset), "the offset should match the requested offset" ); assert_eq!( - devices[0].size_limit, + device.size_limit, Some(sizelimit), "the sizelimit should match the requested sizelimit" ); @@ -220,3 +223,52 @@ fn add_a_loop_device() { assert!(lc.add(1).is_ok()); assert!(lc.add(1).is_err()); } + +#[test] +fn test_device_name() { + let _lock = setup(); + + let lc = LoopControl::open().expect("should be able to open the LoopControl device"); + + let file = create_backing_file(128 * 1024 * 1024); + let file_path = file.to_path_buf(); + let ld0 = lc + .next_free() + .expect("should not error finding the next free loopback device"); + + ld0.with() + .attach(&file) + .expect("should not error attaching the backing file to the loopdev"); + + file.close().expect("should delete the temp backing file"); + + assert_eq!( + file_path, + ld0.original_path() + .expect("expected a correct loop device name") + ); + + assert!(ld0.info().is_ok()); +} + +#[test] +fn test_device_name_absent() { + let _lock = setup(); + let file_size = 128 * 1024 * 1024; + + let lc = LoopControl::open().expect("should be able to open the LoopControl device"); + + let file = + File::open(create_backing_file(file_size)).expect("should be able to open our temp file"); + let ld0 = lc + .next_free() + .expect("should not error finding the next free loopback device"); + + ld0.with() + .attach_fd(file.as_fd()) + .expect("should not error attaching the backing file to the loopdev"); + + assert!(ld0.info().is_ok()); + + assert!(ld0.original_path().is_none()); +} From f69edcde7c3e993f569e1421b5a1460e8003d308 Mon Sep 17 00:00:00 2001 From: decay Date: Tue, 8 Jul 2025 10:53:30 +0400 Subject: [PATCH 2/4] fix clippy problems --- src/lib.rs | 4 ++-- src/loname.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7b561ea..9caed3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -253,8 +253,8 @@ impl LoopDevice { let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0; // store backing file name in the info - let name = loname::Name::from_path(&backing_file).unwrap_or(Name::default()); - info.lo_file_name = name.0.clone(); + let name = loname::Name::from_path(&backing_file).unwrap_or_default(); + info.lo_file_name = name.0; info.lo_crypt_name = name.0; let bf = OpenOptions::new() diff --git a/src/loname.rs b/src/loname.rs index 0cbf21d..a2e2175 100644 --- a/src/loname.rs +++ b/src/loname.rs @@ -27,12 +27,14 @@ impl Name { )); } let mut data: [u8; 64] = [0; LO_NAME_SIZE as usize]; - for (idx, byte) in s.into_iter().enumerate() { + for (idx, byte) in s.iter().enumerate() { data[idx] = *byte; } Ok(Self(data)) } } + +#[allow(clippy::to_string_trait_impl)] impl ToString for Name { fn to_string(&self) -> String { self.0 From c937dc55dc1472e740524c041d6b30cd62feb543 Mon Sep 17 00:00:00 2001 From: decay Date: Fri, 11 Jul 2025 10:49:31 +0400 Subject: [PATCH 3/4] move import to the existing block --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9caed3d..3ecd48d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,10 +38,10 @@ //! ld.detach().unwrap(); //! ``` use crate::bindings::{ - loop_info64, LOOP_CLR_FD, LOOP_CTL_ADD, LOOP_CTL_GET_FREE, LOOP_SET_CAPACITY, LOOP_SET_FD, - LOOP_SET_STATUS64, LO_FLAGS_AUTOCLEAR, LO_FLAGS_PARTSCAN, LO_FLAGS_READ_ONLY, + loop_info64, LOOP_CLR_FD, LOOP_CTL_ADD, LOOP_CTL_GET_FREE, LOOP_GET_STATUS64, + LOOP_SET_CAPACITY, LOOP_SET_FD, LOOP_SET_STATUS64, LO_FLAGS_AUTOCLEAR, LO_FLAGS_PARTSCAN, + LO_FLAGS_READ_ONLY, }; -use bindings::LOOP_GET_STATUS64; #[cfg(feature = "direct_io")] use bindings::LOOP_SET_DIRECT_IO; use libc::{c_int, ioctl}; From 35f7b9403cbbfaaf690ba238b079bb4b71f8e482 Mon Sep 17 00:00:00 2001 From: decay Date: Fri, 11 Jul 2025 10:53:35 +0400 Subject: [PATCH 4/4] remove redundant change --- tests/integration_test.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 95e17fc..59f37f8 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -58,7 +58,7 @@ fn attach_a_backing_file_with_sizelimit_overflow() { fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { let _lock = setup(); - let (mut devices, ld0_path, file_path) = { + 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); @@ -84,26 +84,23 @@ fn attach_a_backing_file(offset: u64, sizelimit: u64, file_size: i64) { 1, "there should be only one loopback mounted device" ); - - let device = devices.pop().unwrap(); - assert_eq!( - device.name.as_str(), + devices[0].name.as_str(), ld0_path.to_str().unwrap(), "the attached devices name should match the input name" ); assert_eq!( - device.back_file.clone().unwrap().as_str(), + devices[0].back_file.clone().unwrap().as_str(), file_path.to_str().unwrap(), "the backing file should match the given file" ); assert_eq!( - device.offset, + devices[0].offset, Some(offset), "the offset should match the requested offset" ); assert_eq!( - device.size_limit, + devices[0].size_limit, Some(sizelimit), "the sizelimit should match the requested sizelimit" );