diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1dd48df112c..eaf5ef121d2 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -9,7 +9,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) ficlone linkgs lstat nlink nlinks pathbuf reflink strs xattrs symlinked +// spell-checker:ignore (ToDO) ficlone ftruncate linkgs lstat nlink nlinks pathbuf reflink strs xattrs symlinked #[macro_use] extern crate quick_error; @@ -1563,16 +1563,18 @@ fn copy_no_cow_fallback( sparse_mode: SparseMode, context: &str, ) -> CopyResult<()> { - if (reflink_mode != ReflinkMode::Never) { + if reflink_mode != ReflinkMode::Never { return Err("--reflink is only supported on linux and macOS" .to_string() .into()); } - if (sparse_mode != SparseMode::Auto) { + if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } - fs::copy(source, dest).context(context) + fs::copy(source, dest).context(context)?; + + Ok(()) } /// Copies `source` to `dest` using copy-on-write if possible. @@ -1581,18 +1583,21 @@ fn copy_on_write_linux( source: &Path, dest: &Path, reflink_mode: ReflinkMode, - _sparse_mode: SparseMode, + sparse_mode: SparseMode, context: &str, ) -> CopyResult<()> { - let src_file = File::open(source).context(context)?; + use std::os::unix::prelude::MetadataExt; + + let mut src_file = File::open(source).context(context)?; let dst_file = OpenOptions::new() .write(true) - .truncate(false) + .truncate(true) .create(true) .open(dest) .context(context)?; - match reflink_mode { - ReflinkMode::Always => unsafe { + + match (reflink_mode, sparse_mode) { + (ReflinkMode::Always, SparseMode::Auto) => unsafe { let result = libc::ioctl(dst_file.as_raw_fd(), FICLONE!(), src_file.as_raw_fd()); if result != 0 { @@ -1607,7 +1612,50 @@ fn copy_on_write_linux( Ok(()) } }, - ReflinkMode::Auto => unsafe { + (ReflinkMode::Always, SparseMode::Always) | (ReflinkMode::Always, SparseMode::Never) => { + Err("`--reflink=always` can be used only with --sparse=auto".into()) + } + (_, SparseMode::Always) => unsafe { + let size: usize = src_file.metadata()?.size().try_into().unwrap(); + if libc::ftruncate(dst_file.as_raw_fd(), i64::try_from(size).unwrap()) < 0 { + return Err(format!( + "failed to ftruncate {:?} to size {}: {}", + dest, + size, + std::io::Error::last_os_error() + ) + .into()); + } + + let blksize = dst_file.metadata()?.blksize(); + let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; + let mut current_offset: usize = 0; + + while current_offset < size { + use std::io::Read; + let (mut off_src, mut off_dst): (i64, i64) = ( + current_offset.try_into().unwrap(), + current_offset.try_into().unwrap(), + ); + + let this_read = src_file.read(&mut buf)?; + current_offset += this_read; + + if buf.iter().any(|&x| x != 0) { + const COPY_FLAGS: u32 = 0; + libc::copy_file_range( + src_file.as_raw_fd(), + &mut off_src, + dst_file.as_raw_fd(), + &mut off_dst, + this_read, + COPY_FLAGS, + ); + } + } + Ok(()) + }, + (ReflinkMode::Auto, SparseMode::Auto) | (ReflinkMode::Auto, SparseMode::Never) => unsafe { let result = libc::ioctl(dst_file.as_raw_fd(), FICLONE!(), src_file.as_raw_fd()); if result != 0 { @@ -1615,7 +1663,7 @@ fn copy_on_write_linux( } Ok(()) }, - ReflinkMode::Never => { + (ReflinkMode::Never, _) => { fs::copy(source, dest).context(context)?; Ok(()) } @@ -1631,7 +1679,7 @@ fn copy_on_write_macos( sparse_mode: SparseMode, context: &str, ) -> CopyResult<()> { - if (sparse_mode != SparseMode::Auto) { + if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 321f38990ae..34e32168b16 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1388,6 +1388,117 @@ fn test_closes_file_descriptors() { .succeeds(); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_never_empty() { + let (at, mut ucmd) = at_and_ucmd!(); + + const BUFFER_SIZE: usize = 4096 * 4; + let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + + at.make_file("src_file1"); + at.write_bytes("src_file1", &buf); + + ucmd.args(&["--sparse=never", "src_file1", "dst_file_non_sparse"]) + .succeeds(); + assert_eq!(at.read_bytes("dst_file_non_sparse"), buf); + assert_eq!( + at.metadata("dst_file_non_sparse").blocks() * 512, + buf.len() as u64 + ); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_always_empty() { + let (at, mut ucmd) = at_and_ucmd!(); + + const BUFFER_SIZE: usize = 4096 * 4; + let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + + at.make_file("src_file1"); + at.write_bytes("src_file1", &buf); + + ucmd.args(&["--sparse=always", "src_file1", "dst_file_sparse"]) + .succeeds(); + + assert_eq!(at.read_bytes("dst_file_sparse"), buf); + assert_eq!(at.metadata("dst_file_sparse").blocks(), 0); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_always_non_empty() { + let (at, mut ucmd) = at_and_ucmd!(); + + const BUFFER_SIZE: usize = 4096 * 16 + 3; + let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let blocks_to_touch = [buf.len() / 3, 2 * (buf.len() / 3)]; + + for i in blocks_to_touch { + buf[i] = b'x'; + } + + at.make_file("src_file1"); + at.write_bytes("src_file1", &buf); + + ucmd.args(&["--sparse=always", "src_file1", "dst_file_sparse"]) + .succeeds(); + + let touched_block_count = + blocks_to_touch.len() as u64 * at.metadata("dst_file_sparse").blksize() / 512; + + assert_eq!(at.read_bytes("dst_file_sparse"), buf); + assert_eq!(at.metadata("dst_file_sparse").blocks(), touched_block_count); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_invalid_option() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.make_file("src_file1"); + + ucmd.args(&[ + "--sparse=invalid", + "src_file1", + "dst_file", + ]) + .fails(); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_always_reflink_always() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.make_file("src_file1"); + + ucmd.args(&[ + "--sparse=always", + "--reflink=always", + "src_file1", + "dst_file", + ]) + .fails(); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_cp_sparse_never_reflink_always() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.make_file("src_file1"); + + ucmd.args(&[ + "--sparse=never", + "--reflink=always", + "src_file1", + "dst_file", + ]) + .fails(); +} + #[test] fn test_copy_dir_symlink() { let (at, mut ucmd) = at_and_ucmd!();