Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
//! 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,
};
#[cfg(feature = "direct_io")]
use bindings::LOOP_SET_DIRECT_IO;
Expand All @@ -52,13 +53,17 @@ use std::{
path::{Path, PathBuf},
};

use loname::Name;

#[allow(non_camel_case_types)]
#[allow(dead_code)]
#[allow(non_snake_case)]
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"))]
Expand Down Expand Up @@ -243,9 +248,15 @@ impl LoopDevice {
fn attach_with_loop_info(
&self, // TODO should be mut? - but changing it is a breaking change
backing_file: impl AsRef<Path>,
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_default();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the purpose of this but I'm unsure why you need the special Name struct for marshalling and unmarshalling. I feel that it would be better to use existing Path and PathBuf methods, as_os_str() or something. Can you tell me why the concern w/ null terminators in the to_string method? Thanks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern was compatibility with C code.
I found that kernel actually takes care of the terminator here.

I did a brief check that name setup through as_os_str is correctly displayed by losetup -l, but that's not a reliable source, because ...

losetup actually leverages cat /sys/block/loop0/loop/backing_file on my system. It isn't limited at 64 chars and a way more straight-forward path.

I think I need to get rid of lo_file_name logic but keep info around for future use.

Do you think it's reasonable ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the full name from /sys/block/loop0/loop/backing_file isn't that simple, as it requires to know the loop0 part, which, in its turn, requires an info call. The only real advantage is that it allows to get the full name in all cases.
LMK if you want to see the version with as_os_str anyways

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would be for removing some of the more complicated handling that @mulkieran mentioned. I can see something like a length check potentially being necessary, but I believe that if we're doing FFI, it's better to use standard ways of null-encoding the string. Custom logic with null handling has caused me a lot of pain in the past.

Copy link
Author

@skorobogatydmitry skorobogatydmitry Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the point of removing the custom logic for handling \0, let me find a way to make it with OsString.
However, I would rather keep the naming helper separated from the rest of the code so it doesn't pollute the lib.rs, which is nearing the limit of a good code unit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended-up with casting &[u8] -> CStr (because it's actually a CStr) -> OsStr (because it's needed to make a PathBuf) -> PathBuf.
So, there's no Tostring anymore, although the code is even more complicated.

info.lo_file_name = name.0;
info.lo_crypt_name = name.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider only setting lo_file_name and not lo_crypt_name here? If not, are you using io_crypt_name? I've made some preliminary inquiries and it seems not entirely clear what it is for.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider only setting lo_file_name and not lo_crypt_name here?
Sure, will do.

I don't use lo_crypt_name, but I found that losetup sets both and they're semantically similar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm personally okay with mimicking losetup if you are @mulkieran


let bf = OpenOptions::new()
.read(true)
.write(write_access)
Expand Down Expand Up @@ -312,6 +323,19 @@ impl LoopDevice {
std::fs::read_link(&p).ok()
}

/// Try to obtain the original file path used on mapping
/// The path is expected to be stored in the loop_info64.
/// The method ignores ioctl errors on requesting info.
///
/// # Return
/// None - path field is empty, not a valid path or ioctl error was encountered
/// Some(PathBuf) - otherwise
pub fn original_path(&self) -> Option<PathBuf> {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this has to be replaced by this:

        self.info().ok().and_then(|info| {
            self.path()
                .and_then(|dev_loop_path| {
                    dev_loop_path.file_name().and_then(|dev_name| {
                        fs::read_to_string(
                            PathBuf::from("/sys/block")
                                .join(dev_name)
                                .join("loop/backing_file"),
                        )
                        .ok()
                    })
                })
                .map(|string| PathBuf::from(string))
                .or(Name(info.lo_file_name).try_into().ok())
        })

but would like to see a thumb-up on this version

self.info()
.ok()
.and_then(|info| Name(info.lo_file_name).try_into().ok())
}

/// Get the device major number
///
/// # Errors
Expand Down Expand Up @@ -390,6 +414,24 @@ 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<loop_info64, std::io::Error> {
let mut loop_info = loop_info64::default();

let ret_code = unsafe {
libc::ioctl(
self.device.as_raw_fd(),
LOOP_GET_STATUS64 as IoctlRequest,
&mut loop_info,
)
};

ioctl_to_error(ret_code).map(|_| loop_info)
}

/// Enable or disable direct I/O for the backing file.
///
/// # Errors
Expand Down
123 changes: 123 additions & 0 deletions src/loname.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Configuration structures for loop device

use std::{
ffi::CStr,
path::{Path, PathBuf},
};

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<Y: AsRef<Path>>(path: &Y) -> Result<Self, String> {
let s = path.as_ref().as_os_str().as_encoded_bytes();
if s.len() > LO_NAME_SIZE as usize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that this use of .len() is not correct, because it means the bytes the implementation has allocated, not the actual length of the characters required to encode the path. I'm going to ask another person to look at this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LO_NAME_SIZE comes from C header and limits number of bytes, AFAIU. So the comparison should be right.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, I believe that .len() is correct. You may be thinking of .capacity().

return Err(format!(
"too many bytes in the provided loop dev source file path: {}, max {LO_NAME_SIZE}",
s.len()
));
}
let mut data = [0; LO_NAME_SIZE as usize];
for (idx, byte) in s.iter().enumerate() {
data[idx] = *byte;
}
Ok(Self(data))
}
}

impl TryInto<PathBuf> for Name {
type Error = NameToPathBufError;
fn try_into(self) -> Result<PathBuf, Self::Error> {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm exposing possible errors here, but the outer scope supresses it, as I think it's a meaningful action on that level.

// cut trailing \0
let c_str = CStr::from_bytes_until_nul(&self.0)
.map_err(|_e| Self::Error::FromBytesUntilNulError)?;
// UTF-8 decode
let string = c_str.to_str().map_err(|_e| Self::Error::CStrToStr)?;
if string.is_empty() {
Err(Self::Error::EmptyName)
} else {
Ok(PathBuf::from(string))
}
}
}

#[derive(Debug)]
pub enum NameToPathBufError {
EmptyName,
FromBytesUntilNulError,
CStrToStr,
}

#[cfg(test)]
mod test {
use std::path::PathBuf;

use super::*;

#[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 = PathBuf::from("/a/b/some-file/かっこいい имя");
let name = Name::from_path(&path);

assert_eq!(
path,
<Name as TryInto<PathBuf>>::try_into(name.unwrap()).expect("path should not be empty")
);
}

#[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()
)
}

#[test]
fn test_name_is_empty() {
let name = Name::default();

assert!(matches!(
<Name as TryInto<PathBuf>>::try_into(name),
Err(NameToPathBufError::EmptyName)
));
}

#[test]
fn test_name_non_utf8() {
let name = Name([
'H' as u8, 'e' as u8, 'l' as u8, 'l' as u8, 'o' as u8, 159, 146, 150, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);

assert!(matches!(
<Name as TryInto<PathBuf>>::try_into(name),
Err(NameToPathBufError::CStrToStr)
));
}
}
51 changes: 50 additions & 1 deletion tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -220,3 +220,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());
}