diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 742ac5c4a2e..fb9d5c60bc1 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -402,6 +402,17 @@ fn relative_path<'a>(src: &'a Path, dst: &Path) -> Cow<'a, Path> { src.into() } +/// Decide whether `src` and `dst` are actually the same directory entry. +fn is_same_entry(src: &Path, dst: &Path) -> bool { + match ( + canonicalize(src, MissingHandling::Missing, ResolveMode::Physical), + canonicalize(dst, MissingHandling::Missing, ResolveMode::Physical), + ) { + (Ok(src), Ok(dst)) => src == dst, + _ => true, + } +} + #[allow(clippy::cognitive_complexity)] fn link(src: &Path, dst: &Path, settings: &Settings) -> LnResult<()> { let mut backup_path = None; @@ -415,7 +426,7 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> LnResult<()> { backup_path = backup_control::get_backup_path(settings.backup, dst, &settings.suffix); if settings.backup == BackupMode::Existing && !settings.symbolic { // when ln --backup f f, it should detect that it is the same file - if paths_refer_to_same_file(src, dst, true) { + if paths_refer_to_same_file(src, dst, true) && is_same_entry(src, dst) { return Err(LnError::SameFile(src.to_owned(), dst.to_owned())); } } @@ -438,18 +449,12 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> LnResult<()> { // In case of error, don't do anything } OverwriteMode::Force => { - if !dst.is_symlink() && paths_refer_to_same_file(src, dst, true) { + if !dst.is_symlink() + && paths_refer_to_same_file(src, dst, true) + && is_same_entry(src, dst) + { // 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())); - } + return Err(LnError::SameFile(src.to_owned(), dst.to_owned())); } let _ = fs::remove_file(dst); // 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 7702b8b9de1..f9f2276eef6 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -5,6 +5,8 @@ #![allow(clippy::similar_names)] use std::path::PathBuf; +#[cfg(all(unix, not(target_os = "android")))] +use std::time::Duration; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; @@ -840,6 +842,38 @@ fn test_backup_same_file() { .stderr_contains("n: 'file1' and './file1' are the same file"); } +#[test] +#[cfg(not(target_os = "android"))] +fn test_backup_existing_hard_linked_under_different_name() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.hard_link("a", "b"); + + ucmd.args(&["--backup", "a", "b"]).succeeds().no_stderr(); + + assert!(at.file_exists("a")); + assert!(at.file_exists("b")); + assert!(at.file_exists("b~")); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_backup_existing_hard_linked_target_is_fifo() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.hard_link("a", "b"); + at.mkfifo("b~"); + + ucmd.args(&["--backup", "a", "b"]) + .timeout(Duration::from_secs(10)) + .succeeds() + .no_stderr(); + + assert!(at.file_exists("a")); + assert!(at.file_exists("b")); + assert!(!at.is_fifo("b~")); +} + #[test] fn test_backup_force() { let scene = TestScenario::new(util_name!());