diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 8b76aa73c..e53af03a2 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -12,6 +12,7 @@ use uucore::fs::{make_path_relative_to, paths_refer_to_same_file}; use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error}; use std::borrow::Cow; +use std::collections::HashSet; use std::error::Error; use std::ffi::OsString; use std::fmt::Display; @@ -295,6 +296,8 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) if !target_dir.is_dir() { return Err(LnError::TargetIsDirectory(target_dir.to_owned()).into()); } + // remember the linked destinations for further usage + let mut linked_destinations: HashSet = HashSet::with_capacity(files.len()); let mut all_successful = true; for srcpath in files { @@ -338,10 +341,20 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) } }; - if let Err(e) = link(srcpath, &targetpath, settings) { + if linked_destinations.contains(&targetpath) { + // If the target file was already created in this linked call, do not overwrite + show_error!( + "will not overwrite just-created '{}' with '{}'", + targetpath.display(), + srcpath.display() + ); + all_successful = false; + } else if let Err(e) = link(srcpath, &targetpath, settings) { show_error!("{}", e); all_successful = false; } + + linked_destinations.insert(targetpath.clone()); } if all_successful { Ok(()) diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index dc31f7261..b6453bf43 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use crate::common::util::TestScenario; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; use std::path::PathBuf; #[test] @@ -719,3 +721,44 @@ fn test_symlink_remove_existing_same_src_and_dest() { assert!(at.file_exists("a") && !at.symlink_exists("a")); assert_eq!(at.read("a"), "sample"); } + +#[test] +fn test_ln_seen_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("a"); + at.mkdir("b"); + at.mkdir("c"); + at.write("a/f", "a"); + at.write("b/f", "b"); + + ts.ucmd() + .arg("a/f") + .arg("b/f") + .arg("c") + .fails() + .stderr_contains("will not overwrite just-created 'c/f' with 'b/f'"); + + assert!(at.plus("c").join("f").exists()); + // b/f still exists + assert!(at.plus("b").join("f").exists()); + // a/f no longer exists + assert!(at.plus("a").join("f").exists()); + #[cfg(unix)] + { + // Check inode numbers + let inode_a_f = at.plus("a").join("f").metadata().unwrap().ino(); + let inode_b_f = at.plus("b").join("f").metadata().unwrap().ino(); + let inode_c_f = at.plus("c").join("f").metadata().unwrap().ino(); + + assert_eq!( + inode_a_f, inode_c_f, + "Inode numbers of a/f and c/f should be equal" + ); + assert_ne!( + inode_b_f, inode_c_f, + "Inode numbers of b/f and c/f should not be equal" + ); + } +}