Skip to content

Commit cc389e5

Browse files
mv: support moving folder containing symlinks to different filesystem (#8605)
Co-authored-by: Sylvestre Ledru <[email protected]>
1 parent 88051cb commit cc389e5

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

src/uu/mv/src/mv.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,13 @@ fn copy_dir_contents_recursive(
10991099
}
11001100
#[cfg(not(unix))]
11011101
{
1102-
fs::copy(&from_path, &to_path)?;
1102+
if from_path.is_symlink() {
1103+
// Copy a symlink file (no-follow).
1104+
rename_symlink_fallback(&from_path, &to_path)?;
1105+
} else {
1106+
// Copy a regular file.
1107+
fs::copy(&from_path, &to_path)?;
1108+
}
11031109
}
11041110

11051111
// Print verbose message for file
@@ -1142,14 +1148,19 @@ fn copy_file_with_hardlinks_helper(
11421148
return Ok(());
11431149
}
11441150

1145-
// Regular file copy
1146-
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1147-
{
1148-
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1149-
}
1150-
#[cfg(any(target_os = "macos", target_os = "redox"))]
1151-
{
1152-
fs::copy(from, to)?;
1151+
if from.is_symlink() {
1152+
// Copy a symlink file (no-follow).
1153+
rename_symlink_fallback(from, to)?;
1154+
} else {
1155+
// Copy a regular file.
1156+
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1157+
{
1158+
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1159+
}
1160+
#[cfg(any(target_os = "macos", target_os = "redox"))]
1161+
{
1162+
fs::copy(from, to)?;
1163+
}
11531164
}
11541165

11551166
Ok(())

tests/by-util/test_mv.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,58 @@ fn test_mv_symlink_into_target() {
623623
ucmd.arg("dir-link").arg("dir").succeeds();
624624
}
625625

626+
#[cfg(all(unix, not(target_os = "android")))]
627+
#[ignore = "requires sudo"]
628+
#[test]
629+
fn test_mv_broken_symlink_to_another_fs() {
630+
let scene = TestScenario::new(util_name!());
631+
632+
scene.fixtures.mkdir("foo");
633+
634+
let output = scene
635+
.cmd("sudo")
636+
.env("PATH", env!("PATH"))
637+
.args(&["-E", "--non-interactive", "ls"])
638+
.run();
639+
println!("test output: {output:?}");
640+
641+
let mount = scene
642+
.cmd("sudo")
643+
.env("PATH", env!("PATH"))
644+
.args(&[
645+
"-E",
646+
"--non-interactive",
647+
"mount",
648+
"none",
649+
"-t",
650+
"tmpfs",
651+
"foo",
652+
])
653+
.run();
654+
655+
if !mount.succeeded() {
656+
print!("Test skipped; requires root user");
657+
return;
658+
}
659+
660+
scene.fixtures.mkdir("bar");
661+
scene.fixtures.symlink_file("nonexistent", "bar/baz");
662+
663+
scene
664+
.ucmd()
665+
.arg("bar")
666+
.arg("foo")
667+
.succeeds()
668+
.no_stderr()
669+
.no_stdout();
670+
671+
scene
672+
.cmd("sudo")
673+
.env("PATH", env!("PATH"))
674+
.args(&["-E", "--non-interactive", "umount", "foo"])
675+
.succeeds();
676+
}
677+
626678
#[test]
627679
#[cfg(all(unix, not(target_os = "android")))]
628680
fn test_mv_hardlink_to_symlink() {

0 commit comments

Comments
 (0)