Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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!(
Expand Down
17 changes: 17 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading