cp: add support of cp --debug

This commit is contained in:
Sylvestre Ledru 2023-05-22 23:03:54 +02:00
parent e295cb7acb
commit f2006a9a6b
5 changed files with 391 additions and 16 deletions

View file

@ -229,10 +229,79 @@ pub struct Options {
backup_suffix: String,
target_dir: Option<PathBuf>,
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::<String>(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(&copy_debug);
}
}
Ok(())

View file

@ -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<CopyDebug> {
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)
}

View file

@ -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<CopyDebug> {
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)
}

View file

@ -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<CopyDebug> {
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)
}

View file

@ -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);
}
}