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() {