diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 9664ba66f..4050dd146 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -918,6 +918,23 @@ fn preserve_hardlinks( for hard_link in hard_links.iter() { if hard_link.1 == inode { + // Consider the following files: + // + // * `src/f` - a regular file + // * `src/link` - a hard link to `src/f` + // * `dest/src/f` - a different regular file + // + // In this scenario, if we do `cp -a src/ dest/`, it is + // possible that the order of traversal causes `src/link` + // to get copied first (to `dest/src/link`). In that case, + // in order to make sure `dest/src/link` is a hard link to + // `dest/src/f` and `dest/src/f` has the contents of + // `src/f`, we delete the existing file to allow the hard + // linking. + if file_or_link_exists(dest) && file_or_link_exists(Path::new(&hard_link.0)) { + std::fs::remove_file(dest)?; + } + std::fs::hard_link(hard_link.0.clone(), dest).unwrap(); *found_hard_link = true; } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 9ced1d130..e1e07878b 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2363,3 +2363,32 @@ fn test_reflink_never_sparse_always() { assert_eq!(src_metadata.blocks(), dest_metadata.blocks()); assert_eq!(dest_metadata.len(), 1024 * 1024); } + +/// Test for preserving attributes of a hard link in a directory. +#[test] +fn test_preserve_hardlink_attributes_in_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + + // The source directory tree. + at.mkdir("src"); + at.touch("src/f"); + at.hard_link("src/f", "src/link"); + + // The destination directory tree. + // + // The file `f` already exists, but the `link` does not. + at.mkdir_all("dest/src"); + at.touch("dest/src/f"); + + ucmd.args(&["-a", "src", "dest"]).succeeds().no_output(); + + // The hard link should now appear in the destination directory tree. + // + // A hard link should have the same inode as the target file. + at.file_exists("dest/src/link"); + #[cfg(unix)] + assert_eq!( + at.metadata("dest/src/f").ino(), + at.metadata("dest/src/link").ino() + ); +} diff --git a/tests/common/util.rs b/tests/common/util.rs index 47de18dce..e233d525c 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -228,6 +228,11 @@ impl CmdResult { self } + /// Assert that there is output to neither stderr nor stdout. + pub fn no_output(&self) -> &Self { + self.no_stdout().no_stderr() + } + /// asserts that the command resulted in stdout stream output that equals the /// passed in value, trailing whitespace are kept to force strict comparison (#1235) /// stdout_only is a better choice unless stderr may or will be non-empty