diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index 46ca3329450..1ee3c64ace0 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -215,18 +215,16 @@ impl HardlinkGroupScanner { fn scan_single_path(&mut self, path: &Path) -> io::Result<()> { use std::os::unix::fs::MetadataExt; - if path.is_dir() { + let metadata = path.symlink_metadata()?; + if metadata.is_dir() { // Recursively scan directory contents self.scan_directory_recursive(path)?; - } else { - let metadata = path.symlink_metadata()?; - if metadata.is_file() && metadata.nlink() > 1 { - let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups - .entry(key) - .or_default() - .push(path.to_path_buf()); - } + } else if metadata.is_file() && metadata.nlink() > 1 { + let key = (metadata.dev(), metadata.ino()); + self.hardlink_groups + .entry(key) + .or_default() + .push(path.to_path_buf()); } Ok(()) } @@ -238,15 +236,17 @@ impl HardlinkGroupScanner { let entries = std::fs::read_dir(dir)?; for entry in entries { let entry = entry?; - let path = entry.path(); - if path.is_dir() { - self.scan_directory_recursive(&path)?; + if entry.file_type()?.is_dir() { + self.scan_directory_recursive(&entry.path())?; } else { - let metadata = path.symlink_metadata()?; + let metadata = entry.metadata()?; if metadata.is_file() && metadata.nlink() > 1 { let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups.entry(key).or_default().push(path); + self.hardlink_groups + .entry(key) + .or_default() + .push(entry.path()); } } } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 7dfd5530507..77aaa470c95 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1988,6 +1988,28 @@ fn test_move_should_not_fallback_to_copy() { assert!(!at.file_exists(target_file)); } +// A directory containing two symlinks that point back at an ancestor must not +// send the hardlink pre-scan into an exponential walk. +#[test] +#[cfg(unix)] +fn test_mv_dir_with_symlink_cycles_terminates() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.mkdir("dest"); + at.write("dir/file", "content"); + at.relative_symlink_dir(".", "dir/loop1"); + at.relative_symlink_dir(".", "dir/loop2"); + + ucmd.arg("dir").arg("dest/").succeeds().no_output(); + + assert!(at.dir_exists("dest/dir")); + assert_eq!(at.read("dest/dir/file"), "content"); + assert!(at.is_symlink("dest/dir/loop1")); + assert!(at.is_symlink("dest/dir/loop2")); + assert!(!at.dir_exists("dir")); +} + // Todo: // $ at.touch a b