From a01ee7667d3b656273cad0f4ba5ea40f7b4a5fa4 Mon Sep 17 00:00:00 2001 From: yuankunzhang Date: Wed, 10 Sep 2025 23:12:25 +0800 Subject: [PATCH] mv: support moving folder containing symlinks to different filesystem --- src/uu/mv/src/mv.rs | 29 +++++++++++++++------- tests/by-util/test_mv.rs | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 723875f615f..b2a9552b7e0 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1094,7 +1094,13 @@ fn copy_dir_contents_recursive( } #[cfg(not(unix))] { - fs::copy(&from_path, &to_path)?; + if from_path.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(&from_path, &to_path)?; + } else { + // Copy a regular file. + fs::copy(&from_path, &to_path)?; + } } // Print verbose message for file @@ -1137,14 +1143,19 @@ fn copy_file_with_hardlinks_helper( return Ok(()); } - // Regular file copy - #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - { - fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; - } - #[cfg(any(target_os = "macos", target_os = "redox"))] - { - fs::copy(from, to)?; + if from.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(from, to)?; + } else { + // Copy a regular file. + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + { + fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; + } + #[cfg(any(target_os = "macos", target_os = "redox"))] + { + fs::copy(from, to)?; + } } Ok(()) diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index f28fc8c28a6..9c07a6468af 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -621,6 +621,58 @@ fn test_mv_symlink_into_target() { ucmd.arg("dir-link").arg("dir").succeeds(); } +#[cfg(all(unix, not(target_os = "android")))] +#[ignore = "requires sudo"] +#[test] +fn test_mv_broken_symlink_to_another_fs() { + let scene = TestScenario::new(util_name!()); + + scene.fixtures.mkdir("foo"); + + let output = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "ls"]) + .run(); + println!("test output: {output:?}"); + + let mount = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&[ + "-E", + "--non-interactive", + "mount", + "none", + "-t", + "tmpfs", + "foo", + ]) + .run(); + + if !mount.succeeded() { + print!("Test skipped; requires root user"); + return; + } + + scene.fixtures.mkdir("bar"); + scene.fixtures.symlink_file("nonexistent", "bar/baz"); + + scene + .ucmd() + .arg("bar") + .arg("foo") + .succeeds() + .no_stderr() + .no_stdout(); + + scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "umount", "foo"]) + .succeeds(); +} + #[test] #[cfg(all(unix, not(target_os = "android")))] fn test_mv_hardlink_to_symlink() {