diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 674e66ea575..ffca39f32fb 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -3,10 +3,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore ficlone reflink ftruncate pwrite fiemap -use std::fs::{File, OpenOptions}; +use std::fs::{self, File, OpenOptions}; use std::io::Read; -use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; +use std::os::unix::prelude::MetadataExt; use std::path::Path; use quick_error::ResultExt; @@ -32,6 +33,9 @@ enum CloneFallback { /// Use [`std::fs::copy`]. FSCopy, + + /// Use sparse_copy + SparseCopy, } /// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. @@ -53,6 +57,7 @@ where match fallback { CloneFallback::Error => Err(std::io::Error::last_os_error()), CloneFallback::FSCopy => std::fs::copy(source, dest).map(|_| ()), + CloneFallback::SparseCopy => sparse_copy(source, dest), } } @@ -62,8 +67,6 @@ fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> where P: AsRef, { - use std::os::unix::prelude::MetadataExt; - let mut src_file = File::open(source)?; let dst_file = File::create(dest)?; let dst_fd = dst_file.as_raw_fd(); @@ -132,6 +135,48 @@ where Ok(num_bytes_copied) } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn check_dest_is_fifo(dest: &Path) -> bool { + // If our destination file exists and it is a FIFO, we do a standard copy . + let file_type = fs::metadata(dest); + match file_type { + Ok(f) => f.file_type().is_fifo(), + + _ => false, + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn check_sparse_detection(source: &Path) -> Result { + let mut src_file = File::open(source)?; + + let metadata = src_file.metadata()?; + let size = metadata.size(); + let blocks = metadata.blocks(); + let blksize = metadata.blksize(); + // cp uses a crude heuristic to detect sparse_files, an estimated formula which seems to accurately + // replicate that, is used. Modifications might be required if we observe any edges cases but this + // should work for majority of the files. + // Reference:https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.blocks + + let mut buf = vec![0; blksize as usize]; + + // Reading the file to account for virtual files which have 0 blocks allocated in disk + // and it is unknown if it contains data or not, virtual files need to be standard copied and + // categorized as not sparse. + + if blocks == 0 { + let _ = src_file.read(&mut buf)?; + + if buf.iter().any(|&x| x != 0x0) { + return Ok(false); + } + } else if blocks < size / 512 { + return Ok(true); + } + Ok(false) +} + /// Copies `source` to `dest` using copy-on-write if possible. /// /// The `source_is_fifo` flag must be set to `true` if and only if @@ -159,11 +204,20 @@ pub(crate) fn copy_on_write( copy_debug.reflink = OffloadReflinkDebug::No; sparse_copy(source, dest) } - (ReflinkMode::Never, _) => { + (ReflinkMode::Never, SparseMode::Never) => { copy_debug.sparse_detection = SparseDebug::No; copy_debug.reflink = OffloadReflinkDebug::No; std::fs::copy(source, dest).map(|_| ()) } + (ReflinkMode::Never, SparseMode::Auto) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::No; + match check_sparse_detection(source) { + Ok(true) => sparse_copy(source, dest), + Ok(false) => std::fs::copy(source, dest).map(|_| ()), + Err(e) => Err(e), + } + } (ReflinkMode::Auto, SparseMode::Always) => { copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.sparse_detection = SparseDebug::Zeros; @@ -171,7 +225,21 @@ pub(crate) fn copy_on_write( sparse_copy(source, dest) } - (ReflinkMode::Auto, _) => { + (ReflinkMode::Auto, SparseMode::Auto) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::Unsupported; + if source_is_fifo { + copy_fifo_contents(source, dest).map(|_| ()) + } else { + match (check_sparse_detection(source), check_dest_is_fifo(dest)) { + (Ok(true), false) => clone(source, dest, CloneFallback::SparseCopy), + (Ok(true), true) => clone(source, dest, CloneFallback::FSCopy), + (Ok(false), _) => clone(source, dest, CloneFallback::FSCopy), + (Err(e), _) => Err(e), + } + } + } + (ReflinkMode::Auto, SparseMode::Never) => { copy_debug.sparse_detection = SparseDebug::No; copy_debug.reflink = OffloadReflinkDebug::Unsupported; if source_is_fifo { diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index e3b373da19d..afe7a01c36f 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2330,6 +2330,26 @@ fn test_cp_sparse_always_non_empty() { assert_eq!(at.metadata("dst_file_sparse").blocks(), touched_block_count); } +#[test] +#[cfg(target_os = "linux")] +fn test_cp_for_virtual_files() { + use std::os::unix::prelude::MetadataExt; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + ts.ucmd() + .arg("--sparse=auto") + .arg("/sys/kernel/address_bits") + .arg("b") + .succeeds(); + + let dest_size = std::fs::metadata(at.plus("b")) + .expect("Metadata of copied file cannot be read") + .size(); + if dest_size == 0 { + panic!("Copy unsuccessful"); + } +} + #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_cp_sparse_invalid_option() { @@ -3218,6 +3238,65 @@ fn test_copy_contents_fifo() { assert_eq!(at.read("outfile"), "foo"); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_sparse_auto_for_sparse_files() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + at.write("a", "hello"); + at.truncate("a", "-s10000"); + ts.ucmd().arg("--sparse=auto").arg("a").arg("b").succeeds(); + + let src_size = std::fs::metadata(at.plus("a")) + .expect("Metadata of source file cannot be read") + .size(); + + let src_blocks = std::fs::metadata(at.plus("a")) + .expect("Metadata of source file cannot be read") + .blocks(); + + let dest_size = std::fs::metadata(at.plus("b")) + .expect("Metadata of copied file cannot be read") + .size(); + let dest_blocks = std::fs::metadata(at.plus("b")) + .expect("Metadata of copied file cannot be read") + .blocks(); + + assert_eq!(src_size, dest_size); + assert_eq!(src_blocks, dest_blocks); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_sparse_auto_for_non_sparse_files() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let a_bytes = [1 as u8; 10000]; + at.touch("a"); + at.write("a", "hello"); + at.append_bytes("a", &a_bytes); + ts.ucmd().arg("--sparse=auto").arg("a").arg("b").succeeds(); + + let src_size = std::fs::metadata(at.plus("a")) + .expect("Metadata of source file cannot be read") + .size(); + + let src_blocks = std::fs::metadata(at.plus("a")) + .expect("Metadata of source file cannot be read") + .blocks(); + + let dest_size = std::fs::metadata(at.plus("b")) + .expect("Metadata of copied file cannot be read") + .size(); + let dest_blocks = std::fs::metadata(at.plus("b")) + .expect("Metadata of copied file cannot be read") + .blocks(); + + assert_eq!(src_size, dest_size); + assert_eq!(src_blocks, dest_blocks); +} + #[cfg(target_os = "linux")] #[test] fn test_reflink_never_sparse_always() {