Add functions to get original file name and loop device info#58
Add functions to get original file name and loop device info#58skorobogatydmitry wants to merge 9 commits intostratis-storage:masterfrom
Conversation
|
Congratulations! One of the builds has completed. 🍾 You can install the built RPMs by following these steps:
Please note that the RPMs should be used only in a testing environment. |
|
@skorobogatydmitry Thanks for PR. It looks like you hit some lints in your submitted code that you should fix. You should be able to force push. We have a Makefile target |
I hope the change is ok now. LMK if something else isn't fit. Some context on why I want to contribute. |
mulkieran
left a comment
There was a problem hiding this comment.
Two preliminary requests...please put the requested code up in a separate PR and I'll merge that immediately, then resume reviewing this PR.
src/lib.rs
Outdated
| 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; |
There was a problem hiding this comment.
Why not put this use in with the other collected uses from bindings on line 40-43?
|
@skorobogatydmitry This crate is the official successor of the original loopdev crate (https://rustsec.org/advisories/RUSTSEC-2023-0088.html). We took it over because it was no longer compiling and the original author was unresponsive. However, since we are not the original author, review will take a little bit longer than with a code base we are familiar with. You will have to bear with us. |
af56c22 to
35f7b94
Compare
| 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
mulkieran
left a comment
There was a problem hiding this comment.
Just one semantic question so far, still working...
| // store backing file name in the info | ||
| let name = loname::Name::from_path(&backing_file).unwrap_or_default(); | ||
| info.lo_file_name = name.0; | ||
| info.lo_crypt_name = name.0; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I'm personally okay with mimicking losetup if you are @mulkieran
| 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
LO_NAME_SIZE comes from C header and limits number of bytes, AFAIU. So the comparison should be right.
There was a problem hiding this comment.
Generally, I believe that .len() is correct. You may be thinking of .capacity().
|
@skorobogatydmitry Please rebase and force push when you get the chance. |
35f7b94 to
37146b5
Compare
Done. |
|
Build failures seem infrastructure related, we can ignore. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
src/loname.rs (1)
61-66: Test iter over zeroed array can use all() for brevityOptional tidy: reduces noise and improves readability.
Apply this diff:
- let name = Name::default(); - for num in name.0 { - assert_eq!(0, num); - } + let name = Name::default(); + assert!(name.0.iter().all(|&b| b == 0));tests/integration_test.rs (2)
225-249: Deterministically detach the loop device at test endRelying on the next test’s setup() to detach can hide ordering issues. Detach here to keep tests self-contained.
Apply this diff:
assert!(ld0.info().is_ok()); + ld0.detach() + .expect("should detach the backing file at the end of the test"); }
251-271: Detach in the fd-based test as wellMirror the previous test to avoid leaving state behind.
Apply this diff:
assert!(ld0.original_path().is_none()); + ld0.detach() + .expect("should detach the backing file at the end of the test"); }src/lib.rs (1)
251-259: Consider surfacing Name::from_path error instead of silently defaultingSilently zeroing lo_file_name on overflow hides input errors and surprises callers.
Option: return io::Error::new(InvalidInput, "...") from attach_with_loop_info when Name::from_path fails, or at least log at trace/debug.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/lib.rs(5 hunks)src/loname.rs(1 hunks)tests/integration_test.rs(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/integration_test.rs (2)
src/lib.rs (5)
path(320-324)open(93-100)open(195-200)original_path(332-341)info(425-434)tests/util/mod.rs (2)
setup(57-61)create_backing_file(19-27)
src/lib.rs (1)
src/loname.rs (2)
from_path(21-34)default(14-16)
🔇 Additional comments (1)
src/lib.rs (1)
255-259: Only set lo_file_name; drop lo_crypt_namePer earlier review, set lo_file_name only. lo_crypt_name’s semantics are unclear and not needed here.
Apply this diff:
- // store backing file name in the info - let name = loname::Name::from_path(&backing_file).unwrap_or_default(); - info.lo_file_name = name.0; - info.lo_crypt_name = name.0; + // store backing file name in the info + let name = loname::Name::from_path(&backing_file).unwrap_or_default(); + info.lo_file_name = name.0;
| /// 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<PathBuf> { | ||
| 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)) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
Lossless original_path: build PathBuf from raw bytes, not a UTF‑8 String
Current code can mangle non‑UTF‑8 paths. Use the bytes up to the first NUL and construct an OsString via OsStringExt.
Apply this diff:
- pub fn original_path(&self) -> Option<PathBuf> {
- 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))
- }
- })
- }
+ pub fn original_path(&self) -> Option<PathBuf> {
+ self.info().ok().and_then(|info| {
+ let raw = &info.lo_file_name;
+ let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
+ if end == 0 {
+ None
+ } else {
+ Some(PathBuf::from(
+ std::os::unix::ffi::OsStringExt::from_vec(raw[..end].to_vec()),
+ ))
+ }
+ })
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// 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<PathBuf> { | |
| 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)) | |
| } | |
| }) | |
| } | |
| pub fn original_path(&self) -> Option<PathBuf> { | |
| self.info().ok().and_then(|info| { | |
| let raw = &info.lo_file_name; | |
| let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len()); | |
| if end == 0 { | |
| None | |
| } else { | |
| Some(PathBuf::from( | |
| std::os::unix::ffi::OsStringExt::from_vec(raw[..end].to_vec()), | |
| )) | |
| } | |
| }) | |
| } |
🤖 Prompt for AI Agents
src/lib.rs around lines 326 to 341: current original_path converts the C-style
file name to a UTF-8 String which can mangle non‑UTF‑8 file names; instead,
extract the raw byte array from info.lo_file_name up to the first NUL byte,
convert that byte slice into an OsString (using OsStringExt::from_vec on Unix),
then build and return a PathBuf from that OsString (return None if the byte
slice is empty). Ensure you avoid any UTF-8 String conversion so paths are
preserved losslessly.
src/loname.rs
Outdated
| let mut data: [u8; 64] = [0; LO_NAME_SIZE as usize]; | ||
| for (idx, byte) in s.iter().enumerate() { | ||
| data[idx] = *byte; | ||
| } | ||
| Ok(Self(data)) |
There was a problem hiding this comment.
Avoid hard-coding 64; use LO_NAME_SIZE and copy_from_slice
Hard-coding the array length risks mismatch if LO_NAME_SIZE changes. Also, use copy_from_slice for clarity and bounds safety (you already guard len).
Apply this diff:
- let mut data: [u8; 64] = [0; LO_NAME_SIZE as usize];
- for (idx, byte) in s.iter().enumerate() {
- data[idx] = *byte;
- }
+ let mut data = [0u8; LO_NAME_SIZE as usize];
+ data[..s.len()].copy_from_slice(s);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut data: [u8; 64] = [0; LO_NAME_SIZE as usize]; | |
| for (idx, byte) in s.iter().enumerate() { | |
| data[idx] = *byte; | |
| } | |
| Ok(Self(data)) | |
| let mut data = [0u8; LO_NAME_SIZE as usize]; | |
| data[..s.len()].copy_from_slice(s); | |
| Ok(Self(data)) |
🤖 Prompt for AI Agents
In src/loname.rs around lines 29 to 33, the code hard-codes the array length as
64 and manually copies bytes; change the array initialization to use
LO_NAME_SIZE (e.g. let mut data: [u8; LO_NAME_SIZE as usize] = [0; LO_NAME_SIZE
as usize];) and replace the for-loop with a bounds-safe copy_from_slice call
(e.g. data[..s.len()].copy_from_slice(s)); keep the existing length guard and
ensure any casts to usize use LO_NAME_SIZE as usize.
jbaublitz
left a comment
There was a problem hiding this comment.
@mulkieran I've provided my review. @skorobogatydmitry I apologize that it's taken me so long to get around to this. Are you interested in resuming work on this?
| 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 { |
There was a problem hiding this comment.
Generally, I believe that .len() is correct. You may be thinking of .capacity().
src/lib.rs
Outdated
| 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)) |
There was a problem hiding this comment.
Is there any reason we're not just using .as_ptr() or .as_mut_ptr() here?
There was a problem hiding this comment.
No particular reason, I just used what I knew. Will change.
I also this thank using as_mut_ptr() eliminates one of the leakages pointed out in the AI review.
It's in nightly: https://doc.rust-lang.org/std/boxed/struct.Box.html#method.as_mut_ptr
I assume it's not ok to use, as the crate has rust 1.74.0 pinned.
There was a problem hiding this comment.
Okay, you're right. I believe it would still be better to use something like .as_ref() or .as_mut() as needed and then cast it using as to a pointer. I'm also a bit confused as to why we need the box at all. I'm not seeing a clear reason why we would need a heap allocation when at the end of the method, we copy the data out of the Box and pass it back by value as the return type. If we passed back the box I could maybe see why we might do that but as it stands, I think it would work just as well with a stack allocation.
There was a problem hiding this comment.
Good point, actually. Let me get rid of Box-es.
| // store backing file name in the info | ||
| let name = loname::Name::from_path(&backing_file).unwrap_or_default(); | ||
| info.lo_file_name = name.0; | ||
| info.lo_crypt_name = name.0; |
There was a problem hiding this comment.
I'm personally okay with mimicking losetup if you are @mulkieran
| 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(); |
There was a problem hiding this comment.
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.
|
I'm playing around with the way to produce a |
|
Congratulations! One of the builds has completed. 🍾 You can install the built RPMs by following these steps:
Please note that the RPMs should be used only in a testing environment. |
- cleanup code
- state possible errors on conversion, but ignore them
|
|
||
| impl TryInto<PathBuf> for Name { | ||
| type Error = NameToPathBufError; | ||
| fn try_into(self) -> Result<PathBuf, Self::Error> { |
There was a problem hiding this comment.
I'm exposing possible errors here, but the outer scope supresses it, as I think it's a meaningful action on that level.
| /// # 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> { |
There was a problem hiding this comment.
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
It could be useful to be able to get original file name and info of a loop device in case the original LoopDevice isn't available for any reason.
I have some more changes in mind like listing devices, applying
LoopConfiginstead of calling attach, etcTesting
All tests worked but
add_a_loop_device.I don't think it's related to the change.
Summary by CodeRabbit