From 1c29075b01ba00ad5de64dc765174de35b462e46 Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:28:10 +0900 Subject: [PATCH] Fix ln -f handling when source and destination are the same entry (#8838) * fix(ln): enhance same-file detection with canonical paths Improved the `link` function in `ln.rs` to use canonical path resolution for accurate same-file detection when forcing overwrites, preventing incorrect errors for equivalent paths. Added tests to verify behavior for self-linking and hard link relinking scenarios. Co-authored-by: Sylvestre Ledru --- src/uu/ln/src/ln.rs | 12 ++++++++++- tests/by-util/test_ln.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index ba38236fa..a3fde8f4a 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -410,7 +410,17 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { } OverwriteMode::Force => { if !dst.is_symlink() && paths_refer_to_same_file(src, dst, true) { - return Err(LnError::SameFile(src.to_owned(), dst.to_owned()).into()); + // Even in force overwrite mode, verify we are not targeting the same entry and return a SameFile error if so + let same_entry = match ( + canonicalize(src, MissingHandling::Missing, ResolveMode::Physical), + canonicalize(dst, MissingHandling::Missing, ResolveMode::Physical), + ) { + (Ok(src), Ok(dst)) => src == dst, + _ => true, + }; + if same_entry { + return Err(LnError::SameFile(src.to_owned(), dst.to_owned()).into()); + } } if fs::remove_file(dst).is_ok() {} // In case of error, don't do anything diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 71f9b5716..d5a7bbfbb 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -793,6 +793,52 @@ fn test_symlink_remove_existing_same_src_and_dest() { assert_eq!(at.read("a"), "sample"); } +#[test] +fn test_force_same_file_detected_after_canonicalization() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("file", "hello"); + + ucmd.args(&["-f", "file", "./file"]) + .fails_with_code(1) + .stderr_contains("are the same file"); + + assert!(at.file_exists("file")); + assert_eq!(at.read("file"), "hello"); +} + +#[test] +#[cfg(not(target_os = "android"))] +fn test_force_ln_existing_hard_link_entry() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file", "hardlink\n"); + at.mkdir("dir"); + + scene.ucmd().args(&["file", "dir"]).succeeds().no_stderr(); + assert!(at.file_exists("dir/file")); + + scene + .ucmd() + .args(&["-f", "file", "dir"]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists("file")); + assert!(at.file_exists("dir/file")); + assert_eq!(at.read("file"), "hardlink\n"); + assert_eq!(at.read("dir/file"), "hardlink\n"); + + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let source_inode = at.metadata("file").ino(); + let target_inode = at.metadata("dir/file").ino(); + assert_eq!(source_inode, target_inode); + } +} + #[test] #[cfg(not(target_os = "android"))] fn test_ln_seen_file() {