diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index ccf414beb5..c9e578f54d 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2213,31 +2213,15 @@ fn delete_path(path: &Path, options: &Options) -> CopyResult<()> { /// assert_eq!(actual, expected); /// ``` fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a Path)> { - // Collect the ancestors of each. For example, if `source` is - // "a/b/c", then the ancestors are "a/b/c", "a/b", "a/", and "". - let source_ancestors: Vec<&Path> = source.ancestors().collect(); - let dest_ancestors: Vec<&Path> = dest.ancestors().collect(); - - // For this particular application, we don't care about the null - // path "" and we don't care about the full path (e.g. "a/b/c"), - // so we exclude those. - let n = source_ancestors.len(); - let source_ancestors = &source_ancestors[1..n - 1]; - - // Get the matching number of elements from the ancestors of the - // destination path (for example, get "d/a" and "d/a/b"). - let k = source_ancestors.len(); - let dest_ancestors = &dest_ancestors[1..=k]; - - // Now we have two slices of the same length, so we zip them. - let mut result = vec![]; - for (x, y) in source_ancestors - .iter() - .rev() - .zip(dest_ancestors.iter().rev()) - { - result.push((*x, *y)); - } + // Assuming dest.ancestors().len() >= source.ancestors().len(), so zip is bounded by source's depth. + // skip(1) drops the full paths; pop() drops source's root ancestor. + let mut result: Vec<_> = source + .ancestors() + .skip(1) + .zip(dest.ancestors().skip(1)) + .collect(); + result.pop(); + result.reverse(); result } @@ -3001,6 +2985,25 @@ mod tests { ]; assert_eq!(actual, expected); } + + #[test] + fn test_aligned_ancestors_empty_source() { + let actual = aligned_ancestors(Path::new(""), Path::new("dest")); + assert!(actual.is_empty()); + } + + #[test] + fn test_aligned_ancestors_single_component_source() { + let actual = aligned_ancestors(Path::new("a"), Path::new("d/a")); + assert!(actual.is_empty()); + } + + #[test] + fn test_aligned_ancestors_two_component_source() { + let actual = aligned_ancestors(Path::new("a/b"), Path::new("d/a/b")); + let expected = vec![(Path::new("a"), Path::new("d/a"))]; + assert_eq!(actual, expected); + } #[test] fn test_diff_attrs() { assert_eq!( diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 7261ef51c9..c33811139d 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1572,6 +1572,23 @@ fn test_cp_parents_dest_not_directory() { .stderr_contains("with --parents, the destination must be a directory"); } +#[test] +fn test_cp_parents_empty_source_does_not_panic() { + // Regression: --parents with an empty source path previously caused a panic + // in aligned_ancestors due to slice indexing [1..0] (start > end). + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dest"); + ucmd.arg("--parents") + .arg("") + .arg("dest") + .fails() + .stderr_only(if cfg!(not(target_os = "windows")) { + "cp: cannot stat '': No such file or directory\n" + } else { + "cp: The system cannot find the path specified. (os error 3)\n" + }); +} + #[test] #[cfg(not(target_os = "openbsd"))] fn test_cp_parents_with_permissions_copy_file() {