diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 674e66ea5..637b8969c 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -2,10 +2,14 @@ // // 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 +// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap lseek + +use libc::{SEEK_DATA, SEEK_HOLE}; use std::fs::{File, OpenOptions}; use std::io::Read; -use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::fs::FileExt; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; use std::path::Path; @@ -32,6 +36,25 @@ enum CloneFallback { /// Use [`std::fs::copy`]. FSCopy, + + /// Use sparse_copy + SparseCopy, + + /// Use sparse_copy_without_hole + SparseCopyWithoutHole, +} + +/// Type of method used for copying files +#[derive(Clone, Copy)] +enum CopyMethod { + /// Do a sparse copy + SparseCopy, + /// Use [`std::fs::copy`]. + FSCopy, + /// Default (can either be sparse_copy or FSCopy) + Default, + /// Use sparse_copy_without_hole + SparseCopyWithoutHole, } /// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. @@ -53,17 +76,109 @@ 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), + CloneFallback::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest), } } +/// Checks whether a file contains any non null bytes i.e. any byte != 0x0 +/// This function returns a tuple of (bool, u64, u64) signifying a tuple of (whether a file has +/// data, its size, no of blocks it has allocated in disk) +#[cfg(any(target_os = "linux", target_os = "android"))] +fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> { + let mut src_file = File::open(source)?; + let metadata = src_file.metadata()?; + + let size = metadata.size(); + let blocks = metadata.blocks(); + // checks edge case of virtual files in /proc which have a size of zero but contains data + if size == 0 { + let mut buf: Vec = vec![0; metadata.blksize() as usize]; // Directly use metadata.blksize() + let _ = src_file.read(&mut buf)?; + return Ok((buf.iter().any(|&x| x != 0x0), size, 0)); + } + + let src_fd = src_file.as_raw_fd(); + + let result = unsafe { libc::lseek(src_fd, 0, SEEK_DATA) }; + + match result { + -1 => Ok((false, size, blocks)), // No data found or end of file + _ if result >= 0 => Ok((true, size, blocks)), // Data found + _ => Err(std::io::Error::last_os_error()), + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +/// Checks whether a file is sparse i.e. it contains holes, uses the crude heuristic blocks < size / 512 +/// Reference:`` +fn check_sparse_detection(source: &Path) -> Result { + let src_file = File::open(source)?; + let metadata = src_file.metadata()?; + let size = metadata.size(); + let blocks = metadata.blocks(); + + if blocks < size / 512 { + return Ok(true); + } + Ok(false) +} + +/// Optimized sparse_copy, doesn't create holes for large sequences of zeros in non sparse_files +/// Used when --sparse=auto +#[cfg(any(target_os = "linux", target_os = "android"))] +fn sparse_copy_without_hole

(source: P, dest: P) -> std::io::Result<()> +where + P: AsRef, +{ + let src_file = File::open(source)?; + let dst_file = File::create(dest)?; + let dst_fd = dst_file.as_raw_fd(); + + let size = src_file.metadata()?.size(); + if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { + return Err(std::io::Error::last_os_error()); + } + let src_fd = src_file.as_raw_fd(); + let mut current_offset: isize = 0; + loop { + let result = unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_DATA) } + .try_into() + .unwrap(); + + current_offset = result; + let hole: isize = + unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_HOLE) } + .try_into() + .unwrap(); + if result == -1 || hole == -1 { + break; + } + if result <= -2 || hole <= -2 { + return Err(std::io::Error::last_os_error()); + } + let len: isize = hole - current_offset; + let mut buf: Vec = vec![0x0; len as usize]; + src_file.read_exact_at(&mut buf, current_offset as u64)?; + unsafe { + libc::pwrite( + dst_fd, + buf.as_ptr() as *const libc::c_void, + len as usize, + current_offset.try_into().unwrap(), + ) + }; + current_offset = hole; + } + Ok(()) +} /// Perform a sparse copy from one file to another. +/// Creates a holes for large sequences of zeros in non_sparse_files, used for --sparse=always #[cfg(any(target_os = "linux", target_os = "android"))] 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(); @@ -97,6 +212,18 @@ where Ok(()) } +#[cfg(any(target_os = "linux", target_os = "android"))] +/// Checks whether an existing destination is a fifo +fn check_dest_is_fifo(dest: &Path) -> bool { + // If our destination file exists and its a fifo , we do a standard copy . + let file_type = std::fs::metadata(dest); + match file_type { + Ok(f) => f.file_type().is_fifo(), + + _ => false, + } +} + /// Copy the contents of the given source FIFO to the given file. fn copy_fifo_contents

(source: P, dest: P) -> std::io::Result where @@ -151,35 +278,121 @@ pub(crate) fn copy_on_write( reflink: OffloadReflinkDebug::Unsupported, sparse_detection: SparseDebug::No, }; - let result = match (reflink_mode, sparse_mode) { (ReflinkMode::Never, SparseMode::Always) => { copy_debug.sparse_detection = SparseDebug::Zeros; - copy_debug.offload = OffloadReflinkDebug::Avoided; + // Default SparseDebug val for SparseMode::Always copy_debug.reflink = OffloadReflinkDebug::No; - sparse_copy(source, dest) - } - (ReflinkMode::Never, _) => { - copy_debug.sparse_detection = SparseDebug::No; - copy_debug.reflink = OffloadReflinkDebug::No; - std::fs::copy(source, dest).map(|_| ()) - } - (ReflinkMode::Auto, SparseMode::Always) => { - copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_debug.sparse_detection = SparseDebug::Zeros; - copy_debug.reflink = OffloadReflinkDebug::Unsupported; - sparse_copy(source, dest) - } - - (ReflinkMode::Auto, _) => { - copy_debug.sparse_detection = SparseDebug::No; - copy_debug.reflink = OffloadReflinkDebug::Unsupported; if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_fifo_contents(source, dest).map(|_| ()) } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_never_sparse_always(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::FSCopy => std::fs::copy(source, dest).map(|_| ()), + _ => sparse_copy(source, dest), + } + } + } + (ReflinkMode::Never, SparseMode::Never) => { + copy_debug.reflink = OffloadReflinkDebug::No; + + if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Avoided; + + copy_fifo_contents(source, dest).map(|_| ()) + } else { + let result = handle_reflink_never_sparse_never(source); + if let Ok(debug) = result { + copy_debug = debug; + } + std::fs::copy(source, dest).map(|_| ()) + } + } + (ReflinkMode::Never, SparseMode::Auto) => { + copy_debug.reflink = OffloadReflinkDebug::No; + + if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_fifo_contents(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_never_sparse_auto(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest), + _ => std::fs::copy(source, dest).map(|_| ()), + } + } + } + (ReflinkMode::Auto, SparseMode::Always) => { + copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for + // SparseMode::Always + if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Avoided; + + copy_fifo_contents(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_auto_sparse_always(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::FSCopy => clone(source, dest, CloneFallback::FSCopy), + _ => clone(source, dest, CloneFallback::SparseCopy), + } + } + } + + (ReflinkMode::Auto, SparseMode::Never) => { + copy_debug.reflink = OffloadReflinkDebug::No; + if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_fifo_contents(source, dest).map(|_| ()) + } else { + let result = handle_reflink_auto_sparse_never(source); + if let Ok(debug) = result { + copy_debug = debug; + } + clone(source, dest, CloneFallback::FSCopy) } } + (ReflinkMode::Auto, SparseMode::Auto) => { + if source_is_fifo { + copy_debug.offload = OffloadReflinkDebug::Unsupported; + copy_fifo_contents(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_auto_sparse_auto(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::SparseCopyWithoutHole => { + clone(source, dest, CloneFallback::SparseCopyWithoutHole) + } + _ => clone(source, dest, CloneFallback::FSCopy), + } + } + } + (ReflinkMode::Always, SparseMode::Auto) => { copy_debug.sparse_detection = SparseDebug::No; copy_debug.reflink = OffloadReflinkDebug::Yes; @@ -193,3 +406,211 @@ pub(crate) fn copy_on_write( result.context(context)?; Ok(copy_debug) } + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=always" and specifies what +/// type of copy should be used +fn handle_reflink_auto_sparse_always( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Zeros, + }; + let mut copy_method = CopyMethod::Default; + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + match (sparse_flag, data_flag, blocks) { + (true, true, 0) => { + // Handling funny files with 0 block allocation but has data + // in it + copy_method = CopyMethod::FSCopy; + copy_debug.sparse_detection = SparseDebug::SeekHoleZeros; + } + (false, true, 0) => copy_method = CopyMethod::FSCopy, + + (true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole, + (true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros, + + (true, false, _) => copy_debug.sparse_detection = SparseDebug::SeekHole, + + (_, _, _) => (), + } + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_never(source: &Path) -> Result { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, + }; + let (data_flag, size, _blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if sparse_flag { + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + Ok(copy_debug) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=never", files will be copied +/// through cloning them with fallback switching to std::fs::copy +fn handle_reflink_auto_sparse_never(source: &Path) -> Result { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, + }; + + let (data_flag, size, _blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if sparse_flag { + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + Ok(copy_debug) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_auto_sparse_auto( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::No, + }; + + let mut copy_method = CopyMethod::Default; + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if (data_flag && size != 0) || (size > 0 && size < 512) { + copy_debug.offload = OffloadReflinkDebug::Yes; + } + + if data_flag && size == 0 { + // Handling /proc/ files + copy_debug.offload = OffloadReflinkDebug::Unsupported; + } + if sparse_flag { + if blocks == 0 && data_flag { + // Handling other "virtual" files + copy_debug.offload = OffloadReflinkDebug::Unsupported; + + copy_method = CopyMethod::FSCopy; // Doing a standard copy for the virtual files + } else { + copy_method = CopyMethod::SparseCopyWithoutHole; + } // Since sparse_flag is true, sparse_detection shall be SeekHole for any non virtual + // regular sparse file and the file will be sparsely copied + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=never" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_auto( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, + }; + + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + let mut copy_method = CopyMethod::Default; + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + + if sparse_flag { + if blocks == 0 && data_flag { + copy_method = CopyMethod::FSCopy; // Handles virtual files which have size > 0 but no + // disk allocation + } else { + copy_method = CopyMethod::SparseCopyWithoutHole; // Handles regular sparse-files + } + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=never" and "--sparse=always" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_always( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::Zeros, + }; + let mut copy_method = CopyMethod::SparseCopy; + + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + match (sparse_flag, data_flag, blocks) { + (true, true, 0) => { + // Handling funny files with 0 block allocation but has data + // in it, e.g. files in /sys and other virtual files + copy_method = CopyMethod::FSCopy; + copy_debug.sparse_detection = SparseDebug::SeekHoleZeros; + } + (false, true, 0) => copy_method = CopyMethod::FSCopy, // Handling data containing zero sized + // files in /proc + (true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole, // Handles files + // with 0 blocks allocated in disk and + (true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros, // Any + // sparse_files with data in it will display SeekHoleZeros + (true, false, _) => { + copy_debug.offload = OffloadReflinkDebug::Unknown; + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + (_, _, _) => (), + } + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + + Ok((copy_debug, copy_method)) +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index e417e40b1..8ab6ccc0e 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3581,7 +3581,7 @@ fn test_cp_debug_sparse_never() { .arg("b") .succeeds(); let stdout_str = result.stdout_str(); - if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { panic!("Failure: stdout was \n{stdout_str}"); } } @@ -3831,6 +3831,642 @@ fn test_acl_preserve() { assert!(compare_xattrs(&file, &file_target)); } +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + at.write("a", "hello"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("a") + .arg("b") + .succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + let src_file_metadata = std::fs::metadata(at.plus("a")).unwrap(); + if dst_file_metadata.blocks() != src_file_metadata.blocks() { + panic!("File not sparsely copied"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_empty_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("a") + .arg("b") + .succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + let src_file_metadata = std::fs::metadata(at.plus("a")).unwrap(); + if dst_file_metadata.blocks() != src_file_metadata.blocks() { + panic!("File not sparsely copied"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_default_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + at.append_bytes("a", "hello".as_bytes()); + let result = ts.ucmd().arg("--debug").arg("a").arg("b").succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + let src_file_metadata = std::fs::metadata(at.plus("a")).unwrap(); + if dst_file_metadata.blocks() != src_file_metadata.blocks() { + panic!("File not sparsely copied"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: yes, reflink: unsupported, sparse detection: SEEK_HOLE") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_default_less_than_512_bytes() { + let ts = TestScenario::new(util_name!()); + + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", "hello".as_bytes()); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(400).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("--sparse=auto") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: yes, reflink: unsupported, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_default_without_hole() { + let ts = TestScenario::new(util_name!()); + + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", "hello".as_bytes()); + + let filler_bytes = [0_u8; 10000]; + + at.append_bytes("a", &filler_bytes); + + let result = ts.ucmd().arg("--debug").arg("a").arg("b").succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: yes, reflink: unsupported, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_default_empty_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts.ucmd().arg("--debug").arg("a").arg("b").succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + let src_file_metadata = std::fs::metadata(at.plus("a")).unwrap(); + if dst_file_metadata.blocks() != src_file_metadata.blocks() { + panic!("File not sparsely copied"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: SEEK_HOLE") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_sparse_always_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + at.write("a", "hello"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + let src_file_metadata = std::fs::metadata(at.plus("a")).unwrap(); + if dst_file_metadata.blocks() != src_file_metadata.blocks() { + panic!("File not sparsely copied"); + } + let stdout_str = result.stdout_str(); + if !stdout_str + .contains("copy offload: avoided, reflink: no, sparse detection: SEEK_HOLE + zeros") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_sparse_always_without_hole() { + let ts = TestScenario::new(util_name!()); + let empty_bytes = [0_u8; 10000]; + let at = &ts.fixtures; + at.touch("a"); + at.write("a", "hello"); + at.append_bytes("a", &empty_bytes); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + + if dst_file_metadata.blocks() != dst_file_metadata.blksize() / 512 { + panic!("Zero sequenced blocks not removed"); + } + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: zeros") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_sparse_always_empty_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_default_virtual_file() { + use std::os::unix::prelude::MetadataExt; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + ts.ucmd() + .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"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_auto_sparse_always_non_sparse_file_with_long_zero_sequence() { + let ts = TestScenario::new(util_name!()); + + let buf: Vec = vec![0; 4096 * 4]; + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", &buf); + at.append_bytes("a", "hello".as_bytes()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + + if dst_file_metadata.blocks() != dst_file_metadata.blksize() / 512 { + panic!("Zero sequenced blocks not removed"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: unsupported, sparse detection: zeros") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_never_empty_sparse_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_sparse_always_non_sparse_file_with_long_zero_sequence() { + let ts = TestScenario::new(util_name!()); + + let buf: Vec = vec![0; 4096 * 4]; + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", &buf); + at.append_bytes("a", "hello".as_bytes()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + + let dst_file_metadata = std::fs::metadata(at.plus("b")).unwrap(); + + if dst_file_metadata.blocks() != dst_file_metadata.blksize() / 512 { + panic!("Zero sequenced blocks not removed"); + } + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: zeros") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_always_sparse_virtual_file() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("/sys/kernel/address_bits") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains( + "copy offload: avoided, reflink: unsupported, sparse detection: SEEK_HOLE + zeros", + ) { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_less_than_512_bytes() { + let ts = TestScenario::new(util_name!()); + + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", "hello".as_bytes()); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(400).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_sparse_never_empty_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + at.append_bytes("a", "hello".as_bytes()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_sparse_never_less_than_512_bytes() { + let ts = TestScenario::new(util_name!()); + + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", "hello".as_bytes()); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(400).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_sparse_never_without_hole() { + let ts = TestScenario::new(util_name!()); + + let at = &ts.fixtures; + at.touch("a"); + at.append_bytes("a", "hello".as_bytes()); + + let filler_bytes = [0_u8; 10000]; + + at.append_bytes("a", &filler_bytes); + + let result = ts + .ucmd() + .arg("--reflink=auto") + .arg("--sparse=never") + .arg("--debug") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_sparse_never_empty_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_sparse_never_file_with_hole() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let f = std::fs::OpenOptions::new() + .write(true) + .open(at.plus("a")) + .unwrap(); + f.set_len(10000).unwrap(); + at.append_bytes("a", "hello".as_bytes()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: SEEK_HOLE") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_default_sparse_virtual_file() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("--debug") + .arg("/sys/kernel/address_bits") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str + .contains("copy offload: unsupported, reflink: unsupported, sparse detection: SEEK_HOLE") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_never_zero_sized_virtual_file() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=never") + .arg("/proc/version") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_default_zero_sized_virtual_file() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("--debug") + .arg("/proc/version") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unsupported, reflink: unsupported, sparse detection: no") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_cp_debug_reflink_never_without_hole() { + let ts = TestScenario::new(util_name!()); + let filler_bytes = [0_u8; 1000]; + let at = &ts.fixtures; + at.touch("a"); + at.write("a", "hello"); + at.append_bytes("a", &filler_bytes); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=never") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} #[test] fn test_cp_force_remove_destination_attributes_only_with_symlink() {