From c3a1fdfb7b5c05d228bd220bc3c783dc59e3a6c9 Mon Sep 17 00:00:00 2001 From: id3v1669 Date: Sat, 30 Aug 2025 08:47:04 +0800 Subject: [PATCH 1/2] mv: treat symlinks as files --- src/uu/mv/src/mv.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index b89290149..b737b88a0 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -375,8 +375,8 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> }); } - let target_is_dir = target.is_dir(); - let source_is_dir = source.is_dir(); + let target_is_dir = target.is_dir() && !target.is_symlink(); + let source_is_dir = source.is_dir() && !source.is_symlink(); if path_ends_with_terminator(target) && (!target_is_dir && !source_is_dir) @@ -415,7 +415,7 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> } else { move_files_into_dir(&[source.to_path_buf()], target, opts) } - } else if target.exists() && source.is_dir() { + } else if target.exists() && source_is_dir { match opts.overwrite { OverwriteMode::NoClobber => return Ok(()), OverwriteMode::Interactive => { @@ -747,7 +747,7 @@ fn rename( } // "to" may no longer exist if it was backed up - if to.exists() && to.is_dir() { + if to.exists() && to.is_dir() && !to.is_symlink() { // normalize behavior between *nix and windows if from.is_dir() { if is_empty_dir(to) { From c53b5dbce54acf7ea539ffa102d8a56d23d91e65 Mon Sep 17 00:00:00 2001 From: id3v1669 Date: Sat, 30 Aug 2025 15:58:50 +0800 Subject: [PATCH 2/2] mv: symlinks as files, tests --- tests/by-util/test_mv.rs | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 23d7315fb..7741fa8ca 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -398,6 +398,78 @@ fn test_mv_replace_file() { assert!(at.file_exists(file_b)); } +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_replace_symlink_with_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + at.mkdir("b"); + at.touch("a/empty_file_a"); + at.touch("b/empty_file_b"); + + at.symlink_dir("a", "symlink_a"); + at.symlink_dir("b", "symlink_b"); + + assert_eq!(at.read("symlink_a/empty_file_a"), ""); + + ucmd.arg("-T") + .arg("symlink_b") + .arg("symlink_a") + .succeeds() + .no_stderr(); + + assert!(at.file_exists("symlink_a/empty_file_b")); + assert!(!at.file_exists("symlink_a/empty_file_a")); + assert!(!at.symlink_exists("symlink_b")); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_replace_symlink_with_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + at.mkdir("b"); + at.touch("a/empty_file_a"); + at.touch("b/empty_file_b"); + + at.symlink_dir("a", "symlink"); + + ucmd.arg("-T") + .arg("b") + .arg("symlink") + .fails() + .stderr_contains("cannot overwrite non-directory") + .stderr_contains("with directory"); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_replace_symlink_with_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + at.touch("a/empty_file_a"); + at.touch("empty_file_b"); + + at.symlink_dir("a", "symlink"); + + assert!(at.file_exists("symlink/empty_file_a")); + + ucmd.arg("-T") + .arg("empty_file_b") + .arg("symlink") + .succeeds() + .no_stderr(); + + assert!(at.file_exists("symlink")); + assert!(!at.is_symlink("symlink")); + assert!(!at.file_exists("empty_file_b")); + assert!(at.dir_exists("a")); + assert!(at.file_exists("a/empty_file_a")); +} + #[test] fn test_mv_force_replace_file() { let (at, mut ucmd) = at_and_ucmd!();