From 751ddfb62fea1b671e9256c99c8120ccf2dec7ec Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Thu, 9 Oct 2025 21:35:10 +0200 Subject: [PATCH 1/3] cp: fix path resolution on -T Fixed a couple of bugs: firstly, one where cp would error out when providing -T on some cases where the assumption of a prefix on the destination path was false; the second one was a logic error in which cp would not respect -T if the destination directory was provided with a terminator slash or (presumably, on Windows/DOS) a backslash. --- src/uu/cp/src/copydir.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index a4901903d..efc4e2bd5 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -9,8 +9,9 @@ #[cfg(windows)] use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +use std::convert::identity; use std::env; -use std::fs; +use std::fs::{self, exists}; use std::io; use std::path::{Path, PathBuf, StripPrefixError}; @@ -20,10 +21,9 @@ use uucore::error::UIoError; use uucore::fs::{ FileInformation, MissingHandling, ResolveMode, canonicalize, path_ends_with_terminator, }; -use uucore::translate; - use uucore::show; use uucore::show_error; +use uucore::translate; use uucore::uio_error; use walkdir::{DirEntry, WalkDir}; @@ -194,15 +194,22 @@ impl Entry { get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?; if no_target_dir { let source_is_dir = source.is_dir(); - if path_ends_with_terminator(context.target) && source_is_dir { + if path_ends_with_terminator(context.target) + && source_is_dir + && !exists(context.target).is_ok_and(identity) + { if let Err(e) = fs::create_dir_all(context.target) { eprintln!( "{}", translate!("cp-error-failed-to-create-directory", "error" => e) ); } - } else { - descendant = descendant.strip_prefix(context.root)?.to_path_buf(); + } else if let Ok(stripped) = + // The following unwrap is unreachable because context.root is always *something*. + descendant + .strip_prefix(context.root.components().next_back().unwrap()) + { + descendant = stripped.to_path_buf(); } } else if context.root == Path::new(".") && context.target.is_dir() { // Special case: when copying current directory (.) to an existing directory, From 9b1f45820446008061d700f35792a8ae6bedb94e Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Thu, 9 Oct 2025 21:35:15 +0200 Subject: [PATCH 2/3] chore: add test coverage for latest cp bug fix --- tests/by-util/test_cp.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 618d789a8..cd4f8ee73 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7090,3 +7090,49 @@ fn test_cp_recursive_files_ending_in_backslash() { ts.ucmd().args(&["-r", "a", "b"]).succeeds(); assert!(at.file_exists("b/foo\\")); } + +#[test] +fn test_cp_no_preserve_target_directory() { + /* Expected result: + ├── a + │ └── b + │ └── c + │ └── d + │ └── f1 + ├── d + │ └── f1 + └── e + ├── b + │ └── c + │ └── d + │ ├── c + │ │ └── d + │ │ └── f1 + │ └── f1 + ├── d + │ └── f1 + ├── f2 + └── f3 + */ + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir_all("a/b/c/d"); + at.touch("a/b/c/d/f1"); + ts.ucmd().args(&["-rT", "a", "e"]).succeeds(); + at.touch("e/f2"); + ts.ucmd().args(&["-rT", "a/", "e/"]).succeeds(); + at.touch("e/f3"); + ts.ucmd().args(&["-rvT", "a/b/c", "e/"]).succeeds(); + ts.ucmd().args(&["-rvT", "a/b/", "e/b/c/d/"]).succeeds(); + ts.ucmd().args(&["-rT", "a/b/c", "."]).succeeds(); + assert!(!at.dir_exists("e/a")); + assert!(at.file_exists("e/b/c/d/f1")); + assert!(at.file_exists("e/b/c/d/c/d/f1")); + assert!(!at.dir_exists("e/c")); + assert!(!at.dir_exists("e/c/d/b")); + assert!(at.file_exists("e/d/f1")); + assert!(at.file_exists("./d/f1")); + assert!(at.file_exists("e/f2")); + assert!(at.file_exists("e/f3")); +} From 9a8c8a58a9253809bd5ebc019c836d0b6e220a98 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Fri, 10 Oct 2025 09:57:54 +0200 Subject: [PATCH 3/3] cp: remove unnecessary unwrap --- src/uu/cp/src/copydir.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index efc4e2bd5..a5c7e76c1 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -204,10 +204,11 @@ impl Entry { translate!("cp-error-failed-to-create-directory", "error" => e) ); } - } else if let Ok(stripped) = - // The following unwrap is unreachable because context.root is always *something*. - descendant - .strip_prefix(context.root.components().next_back().unwrap()) + } else if let Some(stripped) = context + .root + .components() + .next_back() + .and_then(|stripped| descendant.strip_prefix(stripped).ok()) { descendant = stripped.to_path_buf(); }