Skip to content

Commit 1c29075

Browse files
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 <[email protected]>
1 parent 3d3c21a commit 1c29075

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

src/uu/ln/src/ln.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,17 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> {
410410
}
411411
OverwriteMode::Force => {
412412
if !dst.is_symlink() && paths_refer_to_same_file(src, dst, true) {
413-
return Err(LnError::SameFile(src.to_owned(), dst.to_owned()).into());
413+
// Even in force overwrite mode, verify we are not targeting the same entry and return a SameFile error if so
414+
let same_entry = match (
415+
canonicalize(src, MissingHandling::Missing, ResolveMode::Physical),
416+
canonicalize(dst, MissingHandling::Missing, ResolveMode::Physical),
417+
) {
418+
(Ok(src), Ok(dst)) => src == dst,
419+
_ => true,
420+
};
421+
if same_entry {
422+
return Err(LnError::SameFile(src.to_owned(), dst.to_owned()).into());
423+
}
414424
}
415425
if fs::remove_file(dst).is_ok() {}
416426
// In case of error, don't do anything

tests/by-util/test_ln.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,52 @@ fn test_symlink_remove_existing_same_src_and_dest() {
793793
assert_eq!(at.read("a"), "sample");
794794
}
795795

796+
#[test]
797+
fn test_force_same_file_detected_after_canonicalization() {
798+
let (at, mut ucmd) = at_and_ucmd!();
799+
800+
at.write("file", "hello");
801+
802+
ucmd.args(&["-f", "file", "./file"])
803+
.fails_with_code(1)
804+
.stderr_contains("are the same file");
805+
806+
assert!(at.file_exists("file"));
807+
assert_eq!(at.read("file"), "hello");
808+
}
809+
810+
#[test]
811+
#[cfg(not(target_os = "android"))]
812+
fn test_force_ln_existing_hard_link_entry() {
813+
let scene = TestScenario::new(util_name!());
814+
let at = &scene.fixtures;
815+
816+
at.write("file", "hardlink\n");
817+
at.mkdir("dir");
818+
819+
scene.ucmd().args(&["file", "dir"]).succeeds().no_stderr();
820+
assert!(at.file_exists("dir/file"));
821+
822+
scene
823+
.ucmd()
824+
.args(&["-f", "file", "dir"])
825+
.succeeds()
826+
.no_stderr();
827+
828+
assert!(at.file_exists("file"));
829+
assert!(at.file_exists("dir/file"));
830+
assert_eq!(at.read("file"), "hardlink\n");
831+
assert_eq!(at.read("dir/file"), "hardlink\n");
832+
833+
#[cfg(unix)]
834+
{
835+
use std::os::unix::fs::MetadataExt;
836+
let source_inode = at.metadata("file").ino();
837+
let target_inode = at.metadata("dir/file").ino();
838+
assert_eq!(source_inode, target_inode);
839+
}
840+
}
841+
796842
#[test]
797843
#[cfg(not(target_os = "android"))]
798844
fn test_ln_seen_file() {

0 commit comments

Comments
 (0)