-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mv: gnu test case part-hardlink
fix
#6632
base: main
Are you sure you want to change the base?
Conversation
GNU testsuite comparison:
|
Bravo
|
f32325e
to
1da64ed
Compare
GNU testsuite comparison:
|
1 similar comment
GNU testsuite comparison:
|
1da64ed
to
2c7bee1
Compare
GNU testsuite comparison:
|
GNU testsuite comparison:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for tackling these issues! However, combining these three behavioral changes seems like a bad idea, given that each of them have delicate consequences.
It seems that get_dir_content
is the entirely wrong tool to approach mv
, and it causes incorrect behavior.
Finally, it seems you fell into a common trap / bad pattern: Guessing a potential error condition (and not detecting others), then doing an action (and badly handling most error conditions.) This causes unhelpful error messages and unnecessary syscalls.
src/uu/mv/src/mv.rs
Outdated
} | ||
// We fall back to copying and then removing. | ||
else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move the comment into the else
clause. Currently, it looks misleading, as if the if
is now complete.
src/uu/mv/src/mv.rs
Outdated
if !from.exists() { | ||
if let Some(msg) = from.to_str() { | ||
let msg = format!("Path \"{}\" does not exist or you don't have access!", msg); | ||
return Err(FsXError::new(FsXErrorKind::NotFound, &msg)); | ||
} | ||
return Err(FsXError::new( | ||
FsXErrorKind::NotFound, | ||
"Path does not exist or you don't have access!", | ||
)); | ||
} | ||
|
||
let dir_content = get_dir_content(from)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This misses some error conditions, which leads to the following confusing behavior, both on main and on this PR:
$ rm -rf /tmp/foobar/ && cargo run -q --features mv mv -v /root/ /tmp/foobar
mv: Permission denied
[$? = 1]
Which permission was denied? When accessing which file? Is the permission problem with the source or with the destination? I hope you can see how this error message might be confusing when there's many files being moved/copied.
GNU offers a much more user-friendly error message:
$ rm -rf /tmp/foobar/ && mv -v /root/ /tmp/foobar
created directory '/tmp/foobar'
mv: cannot access '/root/': Permission denied
[$? = 1]
The same holds with the error message on line 770: It doesn't even mention the filename.
Please instead handle errors on the actions that cause them. In this case, handling the error when it happens (instead of trying to guess) completely eliminates the need for the from.exists()
call.
src/uu/mv/src/mv.rs
Outdated
if let Some(file_name) = from.file_name().and_then(|file_name| file_name.to_str()) { | ||
file_name.to_string() | ||
} else { | ||
return Err(FsXError::new( | ||
FsXErrorKind::InvalidFileName, | ||
"Invalid file name", | ||
)); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File names are not necessarily valid UTF-8. This code would raise an error if such a file name is encountered. Does that mean non-UTF8-filenames cause an error when enabling progress bars?
We already don't have good handling for non-UTF-8 filenames, but let's try to avoid making the situation worse.
tests/by-util/test_mv.rs
Outdated
|
||
// Ensure that the folder structure is preserved, files are copied, and their contents remain intact. | ||
assert_eq!( | ||
read_to_string(other_fs_tempdir.path().join("a/b/d"),).expect("Unable to read other_fs_file"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there a spurious comma?
read_to_string(other_fs_tempdir.path().join("a/b/d"),).expect("Unable to read other_fs_file"), | |
read_to_string(other_fs_tempdir.path().join("a/b/d")).expect("Unable to read other_fs_file"), |
tests/by-util/test_mv.rs
Outdated
"d" | ||
); | ||
assert_eq!( | ||
read_to_string(other_fs_tempdir.path().join("a/b/c/e"),).expect("Unable to read other_fs_file"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
src/uu/mv/src/mv.rs
Outdated
let result_file_move = if let Some(new_source) = moved_files.get(file_info) { | ||
fs::hard_link(new_source, to)?; | ||
Ok(file_info.file_size()) | ||
} else if let Some(progress_bar) = progress_bar { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you write code for the progress-bar case, then please add a test that exercises the progress-bar case.
src/uu/mv/src/mv.rs
Outdated
)); | ||
} | ||
|
||
let dir_content = get_dir_content(from)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_dir_content
does not return sufficient information. GNU mv copies/moves as many files/directories as it can, even if it first encounter some files where it has insufficient permissions:
$ mv -v source/ /tmp
created directory '/tmp/source'
created directory '/tmp/source/d1'
mv: cannot access 'source/d1': Permission denied
created directory '/tmp/source/d3'
mv: cannot access 'source/d3': Permission denied
copied 'source/f2' -> '/tmp/source/f2'
[$? = 1]
$ ls source/ # source has NOT been deleted!
d1 d3 f2
$ ls /tmp/source/
d1 d3 f2
However, get_dir_content
bails completely if there are any permission issues:
$ cargo run -q --features mv mv -v source/ /tmp
mv: cannot move 'source/' to '/tmp/source': Permission denied
[$? = 1]
$ ls /tmp/source/
ls: cannot access '/tmp/source/': No such file or directory
[$? = 2]
And the final nail in the coffin is that get_dir_content
insists to first read the entire filesystem structure, which uses up potentially vast amounts of memory, and gives ample opportunity for all kinds of TOCTOU races: https://docs.rs/fs_extra/latest/fs_extra/dir/struct.DirContent.html
Therefore, we should avoid using get_dir_content
entirely. I'm not even sure it can be used correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right. Instead of using get_dir_content
, we could follow the approach used in uu_cp
, which involves walking through the directory, copying files, and creating folders as we go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, I meant to set the "Request changes" flag. See above for my reasoning. In particular, get_dir_content
must go.
May I suggest to split the tree changes into two or three PRs?
4b9e64e
to
666f8cb
Compare
GNU testsuite comparison:
|
666f8cb
to
119ce64
Compare
GNU testsuite comparison:
|
119ce64
to
e8c9ffc
Compare
GNU testsuite comparison:
|
3754a06
to
338ed28
Compare
@BenWiederhake I cleaned up this PR. Now, it focuses solely on the directory copying part. I'll work on verbose output and error reporting next. For now, could you let me know if this approach looks okay? |
GNU testsuite comparison:
|
GNU testsuite comparison:
|
GNU testsuite comparison:
|
GNU testsuite comparison:
|
GNU testsuite comparison:
|
Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
102a27d
to
dd06b70
Compare
fix for #6631
behaviours changed
no-target-directory
was specified and the destination ended with a/
This patch resolves that issue.