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 <sylvestre@debian.org>
This commit is contained in:
mattsu 2025-10-10 20:28:10 +09:00 committed by GitHub
parent 3d3c21afab
commit 1c29075b01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 1 deletions

View file

@ -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

View file

@ -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() {