From 90a98292872ef53a8b93d438e480944bcef17998 Mon Sep 17 00:00:00 2001 From: Pierre Marsais Date: Thu, 4 Aug 2022 07:50:19 +0100 Subject: [PATCH] cp: truncate destination when `--reflink` is set (#3759) * cp: truncate destination when `--reflink` is set This is needed in order to allow overriding an existing file. ``` $ truncate -s 512M /tmp/disk.img $ mkfs.btrfs /tmp/disk.img [...] $ mkdir /tmp/disk $ sudo mount /tmp/disk.img /tmp/disk $ sudo chown $(id -u):$(id -g) -R /tmp/disk $ for i in $(seq 0 8192); do echo -ne 'a' >>/tmp/disk/src1; done $ echo "success" >/tmp/disk/src2 $ $ # GNU ls supports overriding files created with `--reflink` $ cp --reflink=always /tmp/disk/src1 /tmp/disk/dst1 $ cp --reflink=always /tmp/disk/src2 /tmp/disk/dst1 $ cat /tmp/disk/dst1 success $ $ # Now testing with uutils $ cargo run cp --reflink=always /tmp/disk/src1 /tmp/disk/dst2 Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/coreutils cp --reflink=always /tmp/disk/src1 /tmp/disk/dst2` $ cargo run cp --reflink=always /tmp/disk/src2 /tmp/disk/dst2 Finished dev [unoptimized + debuginfo] target(s) in 0.26s Running `target/debug/coreutils cp --reflink=always /tmp/disk/src2 /tmp/disk/dst2` cp: failed to clone "/tmp/disk/src2" from "/tmp/disk/dst2": Invalid argument (os error 22) $ cat /tmp/disk/dst2 [lots of 'a'] $ $ # With truncate(true) $ cargo run cp --reflink=always /tmp/disk/src1 /tmp/disk/dst3 Finished dev [unoptimized + debuginfo] target(s) in 7.98s Running `target/debug/coreutils cp --reflink=always /tmp/disk/src1 /tmp/disk/dst3` $ cargo run cp --reflink=always /tmp/disk/src2 /tmp/disk/dst3 Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/coreutils cp --reflink=always /tmp/disk/src2 /tmp/disk/dst3` $ cat /tmp/disk/dst3 success ``` --- src/uu/cp/src/cp.rs | 2 +- tests/by-util/test_cp.rs | 80 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 8c6a38373..2c0626d73 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1535,7 +1535,7 @@ fn copy_on_write_linux( let src_file = File::open(source).context(context)?; let dst_file = OpenOptions::new() .write(true) - .truncate(false) + .truncate(true) .create(true) .open(dest) .context(context)?; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 321f38990..f4cdd3c6c 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob +// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs ROOTDIR USERDIR use crate::common::util::*; #[cfg(not(windows))] @@ -1388,6 +1388,84 @@ fn test_closes_file_descriptors() { .succeeds(); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_reflink_always_override() { + let scene = TestScenario::new(util_name!()); + + const DISK: &str = "disk.img"; + const ROOTDIR: &str = "disk_root/"; + const USERDIR: &str = "dir/"; + const MOUNTPOINT: &str = "mountpoint/"; + + let src1_path: &str = &vec![MOUNTPOINT, USERDIR, "src1"].concat(); + let src2_path: &str = &vec![MOUNTPOINT, USERDIR, "src2"].concat(); + let dst_path: &str = &vec![MOUNTPOINT, USERDIR, "dst"].concat(); + + scene.fixtures.mkdir(ROOTDIR); + scene.fixtures.mkdir(&vec![ROOTDIR, USERDIR].concat()); + + // Setup: + // Because neither `mkfs.btrfs` not btrfs `mount` options allow us to have a mountpoint owned + // by a non-root user, we want the following directory structure: + // + // uid | path + // --------------------------- + // user | . + // root | └── mountpoint + // user | └── dir + // user | ├── src1 + // user | └── src2 + + scene + .ccmd("truncate") + .args(&["-s", "128M", DISK]) + .succeeds(); + + if !scene + .cmd_keepenv("env") + .args(&["mkfs.btrfs", "--rootdir", ROOTDIR, DISK]) + .run() + .succeeded() + { + print!("Test skipped; couldn't make btrfs disk image"); + return; + } + + scene.fixtures.mkdir(MOUNTPOINT); + + let mount = scene + .cmd_keepenv("sudo") + .args(&["-E", "--non-interactive", "mount", DISK, MOUNTPOINT]) + .run(); + + if !mount.succeeded() { + print!("Test skipped; requires root user"); + return; + } + + scene.fixtures.make_file(src1_path); + scene.fixtures.write_bytes(src1_path, &[0x64; 8192]); + + scene.fixtures.make_file(src2_path); + scene.fixtures.write(src2_path, "other data"); + + scene + .ucmd() + .args(&["--reflink=always", src1_path, dst_path]) + .succeeds(); + + scene + .ucmd() + .args(&["--reflink=always", src2_path, dst_path]) + .succeeds(); + + scene + .cmd_keepenv("sudo") + .args(&["-E", "--non-interactive", "umount", MOUNTPOINT]) + .succeeds(); +} + #[test] fn test_copy_dir_symlink() { let (at, mut ucmd) = at_and_ucmd!();