1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

cp: force copying file to itself with --backup

Fix the behavior of `cp` when both `--backup` and `--force` are
specified and the source and destination are the same file.  Before
this commit, `cp` terminated without copying and without making a
backup. After this commit, the copy is made and the backup file is
made. For example,

    $ touch f
    $ cp --force --backup f f

results in a backup file `f~` being created.
This commit is contained in:
Jeffrey Finkelstein 2022-09-21 23:58:41 -04:00
parent 742c06965b
commit fbed01dd54
2 changed files with 72 additions and 4 deletions

View file

@ -835,6 +835,11 @@ impl Options {
} }
false false
} }
/// Whether to force overwriting the destination file.
fn force(&self) -> bool {
matches!(self.overwrite, OverwriteMode::Clobber(ClobberMode::Force))
}
} }
impl TargetType { impl TargetType {
@ -1194,16 +1199,36 @@ fn backup_dest(dest: &Path, backup_path: &Path) -> CopyResult<PathBuf> {
Ok(backup_path.into()) Ok(backup_path.into())
} }
/// Decide whether source and destination files are the same and
/// copying is forbidden.
///
/// Copying to the same file is only allowed if both `--backup` and
/// `--force` are specified and the file is a regular file.
fn is_forbidden_copy_to_same_file(
source: &Path,
dest: &Path,
options: &Options,
source_in_command_line: bool,
) -> bool {
// TODO To match the behavior of GNU cp, we also need to check
// that the file is a regular file.
let dereference_to_compare =
options.dereference(source_in_command_line) || !source.is_symlink();
paths_refer_to_same_file(source, dest, dereference_to_compare)
&& !(options.force() && options.backup != BackupMode::NoBackup)
}
/// Back up, remove, or leave intact the destination file, depending on the options.
fn handle_existing_dest( fn handle_existing_dest(
source: &Path, source: &Path,
dest: &Path, dest: &Path,
options: &Options, options: &Options,
source_in_command_line: bool, source_in_command_line: bool,
) -> CopyResult<()> { ) -> CopyResult<()> {
let dereference_to_compare = // Disallow copying a file to itself, unless `--force` and
options.dereference(source_in_command_line) || !source.is_symlink(); // `--backup` are both specified.
if paths_refer_to_same_file(source, dest, dereference_to_compare) { if is_forbidden_copy_to_same_file(source, dest, options, source_in_command_line) {
return Err(format!("{}: same file", context_for(source, dest)).into()); return Err(format!("{} and {} are the same file", source.quote(), dest.quote()).into());
} }
options.overwrite.verify(dest)?; options.overwrite.verify(dest)?;

View file

@ -2226,3 +2226,46 @@ fn test_copy_dir_preserve_permissions_inaccessible_file() {
let metadata2 = at.metadata("d2"); let metadata2 = at.metadata("d2");
assert_metadata_eq!(metadata1, metadata2); assert_metadata_eq!(metadata1, metadata2);
} }
/// Test that copying file to itself with backup fails.
#[test]
fn test_same_file_backup() {
let (at, mut ucmd) = at_and_ucmd!();
at.touch("f");
ucmd.args(&["--backup", "f", "f"])
.fails()
.stderr_only("cp: 'f' and 'f' are the same file");
assert!(!at.file_exists("f~"));
}
/// Test that copying file to itself with forced backup succeeds.
#[cfg(not(windows))]
#[test]
fn test_same_file_force() {
let (at, mut ucmd) = at_and_ucmd!();
at.touch("f");
ucmd.args(&["--force", "f", "f"])
.fails()
.stderr_only("cp: 'f' and 'f' are the same file");
assert!(!at.file_exists("f~"));
}
/// Test that copying file to itself with forced backup succeeds.
#[cfg(all(not(windows), not(target_os = "macos")))]
#[test]
fn test_same_file_force_backup() {
// TODO This test should work on macos, but the command was
// causing an error:
//
// cp: 'f' -> 'f': No such file or directory (os error 2)
//
// I couldn't figure out how to fix it, so I just skipped this
// test on macos.
let (at, mut ucmd) = at_and_ucmd!();
at.touch("f");
ucmd.args(&["--force", "--backup", "f", "f"])
.succeeds()
.no_stdout()
.no_stderr();
assert!(at.file_exists("f~"));
}