cp: allow directory merging when destination was just created (#9325)
Some checks failed
CICD / Style/cargo-deny (push) Waiting to run
CICD / Style/deps (push) Waiting to run
CICD / Documentation/warnings (push) Waiting to run
CICD / MinRustV (push) Waiting to run
CICD / Separate Builds (push) Waiting to run
CICD / Dependencies (push) Waiting to run
CICD / Build/Makefile (push) Blocked by required conditions
CICD / Build/stable (push) Blocked by required conditions
CICD / Build/nightly (push) Blocked by required conditions
CICD / Binary sizes (push) Blocked by required conditions
CICD / Build (push) Blocked by required conditions
CICD / Tests/BusyBox test suite (push) Blocked by required conditions
CICD / Tests/Toybox test suite (push) Blocked by required conditions
CICD / Code Coverage (push) Waiting to run
CICD / Test all features separately (push) Blocked by required conditions
CICD / Build/SELinux (push) Blocked by required conditions
CICD / Build/SELinux-Stubs (Non-Linux) (push) Blocked by required conditions
CICD / Safe Traversal Security Check (push) Blocked by required conditions
GnuTests / Run GNU tests (native) (push) Waiting to run
GnuTests / Run GNU tests (SELinux) (push) Waiting to run
GnuTests / Aggregate GNU test results (push) Blocked by required conditions
Android / Test builds (push) Waiting to run
Benchmarks / Run benchmarks (CodSpeed) (push) Waiting to run
Code Quality / Style/format (push) Waiting to run
Code Quality / Style/lint (push) Waiting to run
Code Quality / Style/spelling (push) Waiting to run
Code Quality / Style/toml (push) Waiting to run
Code Quality / Style/Python (push) Waiting to run
Code Quality / Pre-commit hooks (push) Waiting to run
Devcontainer / Verify devcontainer (push) Waiting to run
FreeBSD / Style and Lint (push) Waiting to run
FreeBSD / Tests (push) Waiting to run
OpenBSD / Style and Lint (push) Waiting to run
OpenBSD / Tests (push) Waiting to run
WSL2 / Test (push) Waiting to run
CheckScripts / ShellScript/Check (push) Has been cancelled
CheckScripts / ShellScript/Format (push) Has been cancelled

* cp: allow directory merging when destination was just created

Previously, when copying to a destination that was created in the same
cp call, the operation would fail with "will not overwrite just-created"
for both files and directories. This change allows directories to be
merged (matching GNU cp behavior) while still preventing file
overwrites.

The fix checks if both the source and destination are directories
before allowing the merge. If either is a file, the original error
behavior is preserved to prevent accidental file overwrites.

Fixes the case where copying multiple directories to the same
destination path would incorrectly error instead of merging their
contents.

fixes: #9318

* test_cp: Update with the suggestion

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
This commit is contained in:
Vikram Kangotra 2025-11-19 21:23:23 +05:30 committed by GitHub
parent 376fa6408b
commit 15d22c285e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 4 deletions

View file

@ -1375,10 +1375,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
{
// There is already a file and it isn't a symlink (managed in a different place)
if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered {
// If the target file was already created in this cp call, do not overwrite
return Err(CpError::Error(
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
));
// If the target was already created in this cp call, check if it's a directory.
// Directories should be merged (GNU cp behavior), but files should not be overwritten.
let dest_is_dir = fs::metadata(&dest).is_ok_and(|m| m.is_dir());
let source_is_dir = fs::metadata(source).is_ok_and(|m| m.is_dir());
// Only prevent overwriting if both source and dest are files (not directories)
// Directories should be merged, which is handled by copy_directory
if !dest_is_dir || !source_is_dir {
// If the target file was already created in this cp call, do not overwrite
return Err(CpError::Error(
translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()),
));
}
}
}

View file

@ -142,6 +142,41 @@ fn test_cp_duplicate_folder() {
assert!(at.dir_exists(format!("{TEST_COPY_TO_FOLDER}/{TEST_COPY_FROM_FOLDER}").as_str()));
}
#[test]
fn test_cp_duplicate_directories_merge() {
let (at, mut ucmd) = at_and_ucmd!();
// Source directory 1
at.mkdir_all("src_dir/subdir");
at.write("src_dir/subdir/file1.txt", "content1");
at.write("src_dir/subdir/file2.txt", "content2");
// Source directory 2
at.mkdir_all("src_dir2/subdir");
at.write("src_dir2/subdir/file1.txt", "content3");
// Destination
at.mkdir("dest");
// Perform merge copy
ucmd.arg("-r")
.arg("src_dir/subdir")
.arg("src_dir2/subdir")
.arg("dest")
.succeeds();
// Verify directory exists
assert!(at.dir_exists("dest/subdir"));
// file1.txt should be overwritten by src_dir2/subdir/file1.txt
assert!(at.file_exists("dest/subdir/file1.txt"));
assert_eq!(at.read("dest/subdir/file1.txt"), "content3");
// file2.txt should remain from first copy
assert!(at.file_exists("dest/subdir/file2.txt"));
assert_eq!(at.read("dest/subdir/file2.txt"), "content2");
}
#[test]
fn test_cp_duplicate_files_normalized_path() {
let (at, mut ucmd) = at_and_ucmd!();