diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index f7069f04f..a1cbd4aec 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -229,10 +229,79 @@ pub struct Options { backup_suffix: String, target_dir: Option, update: UpdateMode, + debug: bool, verbose: bool, progress_bar: bool, } +/// Enum representing various debug states of the offload and reflink actions. +#[derive(Debug)] +#[allow(dead_code)] // All of them are used on Linux +enum OffloadReflinkDebug { + Unknown, + No, + Yes, + Avoided, + Unsupported, +} + +/// Enum representing various debug states of the sparse detection. +#[derive(Debug)] +#[allow(dead_code)] // silent for now until we use them +enum SparseDebug { + Unknown, + No, + Zeros, + SeekHole, + SeekHoleZeros, + Unsupported, +} + +/// Struct that contains the debug state for each action in a file copy operation. +#[derive(Debug)] +struct CopyDebug { + offload: OffloadReflinkDebug, + reflink: OffloadReflinkDebug, + sparse_detection: SparseDebug, +} + +impl OffloadReflinkDebug { + fn to_string(&self) -> &'static str { + match self { + Self::No => "no", + Self::Yes => "yes", + Self::Avoided => "avoided", + Self::Unsupported => "unsupported", + Self::Unknown => "unknown", + } + } +} + +impl SparseDebug { + fn to_string(&self) -> &'static str { + match self { + Self::No => "no", + Self::Zeros => "zeros", + Self::SeekHole => "SEEK_HOLE", + Self::SeekHoleZeros => "SEEK_HOLE + zeros", + Self::Unsupported => "unsupported", + Self::Unknown => "unknown", + } + } +} + +/// This function prints the debug information of a file copy operation if +/// no hard link or symbolic link is required, and data copy is required. +/// It prints the debug information of the offload, reflink, and sparse detection actions. +fn show_debug(copy_debug: &CopyDebug) { + println!( + "copy offload: {}, reflink: {}, sparse detection: {}", + copy_debug.offload.to_string(), + copy_debug.reflink.to_string(), + copy_debug.sparse_detection.to_string(), + ); +} + const ABOUT: &str = help_about!("cp.md"); const USAGE: &str = help_usage!("cp.md"); const AFTER_HELP: &str = help_section!("after help", "cp.md"); @@ -269,6 +338,7 @@ mod options { pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; pub const SYMBOLIC_LINK: &str = "symbolic-link"; pub const TARGET_DIRECTORY: &str = "target-directory"; + pub const DEBUG: &str = "debug"; pub const VERBOSE: &str = "verbose"; } @@ -361,6 +431,12 @@ pub fn uu_app() -> Command { .help("remove any trailing slashes from each SOURCE argument") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help("explain how a file is copied. Implies -v") + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::VERBOSE) .short('v') @@ -831,7 +907,8 @@ impl Options { one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), parents: matches.get_flag(options::PARENTS), update: update_mode, - verbose: matches.get_flag(options::VERBOSE), + debug: matches.get_flag(options::DEBUG), + verbose: matches.get_flag(options::VERBOSE) || matches.get_flag(options::DEBUG), strip_trailing_slashes: matches.get_flag(options::STRIP_TRAILING_SLASHES), reflink_mode: { if let Some(reflink) = matches.get_one::(options::REFLINK) { @@ -1745,7 +1822,7 @@ fn copy_helper( } else if source_is_symlink { copy_link(source, dest, symlinked_files)?; } else { - copy_on_write( + let copy_debug = copy_on_write( source, dest, options.reflink_mode, @@ -1754,6 +1831,10 @@ fn copy_helper( #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] source_is_fifo, )?; + + if !options.attributes_only && options.debug { + show_debug(©_debug); + } } Ok(()) diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 7d97813dd..18f2520a2 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -13,7 +13,7 @@ use quick_error::ResultExt; use uucore::mode::get_umask; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; // From /usr/include/linux/fs.h: // #define FICLONE _IOW(0x94, 9, int) @@ -145,24 +145,51 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, -) -> CopyResult<()> { +) -> CopyResult { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::No, + }; + let result = match (reflink_mode, sparse_mode) { - (ReflinkMode::Never, SparseMode::Always) => sparse_copy(source, dest), - (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), - (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), + (ReflinkMode::Never, SparseMode::Always) => { + copy_debug.sparse_detection = SparseDebug::Zeros; + copy_debug.offload = OffloadReflinkDebug::Avoided; + 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_fifo_contents(source, dest).map(|_| ()) } else { clone(source, dest, CloneFallback::FSCopy) } } - (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, CloneFallback::Error), + (ReflinkMode::Always, SparseMode::Auto) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::Yes; + + clone(source, dest, CloneFallback::Error) + } (ReflinkMode::Always, _) => { return Err("`--reflink=always` can be used only with --sparse=auto".into()) } }; result.context(context)?; - Ok(()) + Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 4407e0edf..b173aa959 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -11,7 +11,7 @@ use std::path::Path; use quick_error::ResultExt; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; /// Copies `source` to `dest` using copy-on-write if possible. /// @@ -24,10 +24,15 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, -) -> CopyResult<()> { +) -> CopyResult { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; // Extract paths in a form suitable to be passed to a syscall. // The unwrap() is safe because they come from the command-line and so contain non nul @@ -72,6 +77,7 @@ pub(crate) fn copy_on_write( return Err(format!("failed to clone {source:?} from {dest:?}: {error}").into()) } _ => { + copy_debug.reflink = OffloadReflinkDebug::Yes; if source_is_fifo { let mut src_file = File::open(source)?; let mut dst_file = File::create(dest)?; @@ -83,5 +89,5 @@ pub(crate) fn copy_on_write( }; } - Ok(()) + Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs index b70da2f23..f5882f75e 100644 --- a/src/uu/cp/src/platform/other.rs +++ b/src/uu/cp/src/platform/other.rs @@ -8,7 +8,7 @@ use std::path::Path; use quick_error::ResultExt; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; /// Copies `source` to `dest` for systems without copy-on-write pub(crate) fn copy_on_write( @@ -17,7 +17,7 @@ pub(crate) fn copy_on_write( reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, -) -> CopyResult<()> { +) -> CopyResult { if reflink_mode != ReflinkMode::Never { return Err("--reflink is only supported on linux and macOS" .to_string() @@ -26,8 +26,12 @@ pub(crate) fn copy_on_write( if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } - + let copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unsupported, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; fs::copy(source, dest).context(context)?; - Ok(()) + Ok(copy_debug) } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index ef35f6c2d..1feeb0ede 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2909,3 +2909,260 @@ fn test_cp_archive_on_directory_ending_dot() { ucmd.args(&["-a", "dir1/.", "dir2"]).succeeds(); assert!(at.file_exists("dir2/file")); } + +#[test] +fn test_cp_debug_default() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts.ucmd().arg("--debug").arg("a").arg("b").succeeds(); + + let stdout_str = result.stdout_str(); + #[cfg(target_os = "macos")] + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + #[cfg(target_os = "linux")] + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + + #[cfg(windows)] + if !stdout_str + .contains("copy offload: unsupported, reflink: unsupported, sparse detection: unsupported") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +} + +#[test] +fn test_cp_debug_multiple_default() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let dir = "dir"; + at.touch("a"); + at.touch("b"); + at.mkdir(dir); + let result = ts + .ucmd() + .arg("--debug") + .arg("a") + .arg("b") + .arg(dir) + .succeeds(); + + let stdout_str = result.stdout_str(); + + #[cfg(target_os = "macos")] + { + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches( + "copy offload: unknown, reflink: unsupported, sparse detection: unsupported" + ) + .count(), + 2 + ); + } + + #[cfg(target_os = "linux")] + { + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches("copy offload: unknown, reflink: unsupported, sparse detection: no") + .count(), + 2 + ); + } + + #[cfg(target_os = "windows")] + { + if !stdout_str.contains( + "copy offload: unsupported, reflink: unsupported, sparse detection: unsupported", + ) { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches("copy offload: unsupported, reflink: unsupported, sparse detection: unsupported") + .count(), + 2 + ); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_reflink() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .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: zeros") { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_always() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: unsupported, sparse detection: zeros") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_never() { + 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: unknown, reflink: unsupported, sparse detection: no") { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +} + +#[test] +fn test_cp_debug_sparse_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=auto") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + + #[cfg(target_os = "macos")] + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + + #[cfg(target_os = "linux")] + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] +fn test_cp_debug_reflink_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("a") + .arg("b") + .succeeds(); + + #[cfg(target_os = "linux")] + { + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + } + + #[cfg(target_os = "macos")] + { + let stdout_str = result.stdout_str(); + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_always_reflink_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("--reflink=auto") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: unsupported, sparse detection: zeros") + { + println!("Failure: stdout was \n{stdout_str}"); + assert!(false); + } +}