From 5b100fef62af741114a51d296e6c1d251fc320b7 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 9 Oct 2022 23:22:36 -0400 Subject: [PATCH] cp: implement --copy-contents option for fifos Implement the `--copy-contents` option when the source is a FIFO, so that the contents of the FIFO are copied (when the bytes become available for reading) instead of the FIFO object itself. For example, $ mkfifo fifo $ cp --copy-contents fifo outfile & [1] 1614080 $ echo foo > fifo $ cat outfile foo [1]+ Done cp --copy-contents fifo outfile --- src/uu/cp/src/cp.rs | 5 +-- src/uu/cp/src/platform/linux.rs | 55 +++++++++++++++++++++++---------- src/uu/cp/src/platform/macos.rs | 18 +++++++++-- tests/by-util/test_cp.rs | 31 ++++++++++++++++++- 4 files changed, 87 insertions(+), 22 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3a1697039..2b9cea233 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -677,7 +677,6 @@ fn add_all_attributes() -> Vec { impl Options { fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ - options::COPY_CONTENTS, #[cfg(not(any(windows, unix)))] options::ONE_FILE_SYSTEM, options::CONTEXT, @@ -1445,7 +1444,7 @@ fn copy_helper( * https://github.com/rust-lang/rust/issues/79390 */ File::create(dest).context(dest.display().to_string())?; - } else if source_is_fifo && options.recursive { + } else if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] copy_fifo(dest, options.overwrite)?; } else if source_is_symlink { @@ -1457,6 +1456,8 @@ fn copy_helper( options.reflink_mode, options.sparse_mode, context, + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + source_is_fifo, )?; } diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 7f47b884a..4af897d3c 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -21,29 +21,39 @@ macro_rules! FICLONE { }; } +/// The fallback behavior for [`clone`] on failed system call. +#[derive(Clone, Copy)] +enum CloneFallback { + /// Raise an error. + Error, + + /// Use [`std::io::copy`]. + IOCopy, + + /// Use [`std::fs::copy`]. + FSCopy, +} + /// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. /// -/// If `fallback` is true and there is a failure performing the clone, -/// then this function performs a standard [`std::fs::copy`]. Otherwise, -/// this function returns an error. +/// `fallback` controls what to do if the system call fails. #[cfg(any(target_os = "linux", target_os = "android"))] -fn clone

(source: P, dest: P, fallback: bool) -> std::io::Result<()> +fn clone

(source: P, dest: P, fallback: CloneFallback) -> std::io::Result<()> where P: AsRef, { - let src_file = File::open(&source)?; - let dst_file = File::create(&dest)?; + let mut src_file = File::open(&source)?; + let mut dst_file = File::create(&dest)?; let src_fd = src_file.as_raw_fd(); let dst_fd = dst_file.as_raw_fd(); let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; - if result != 0 { - if fallback { - std::fs::copy(source, dest).map(|_| ()) - } else { - Err(std::io::Error::last_os_error()) - } - } else { - Ok(()) + if result == 0 { + return Ok(()); + } + match fallback { + CloneFallback::Error => Err(std::io::Error::last_os_error()), + CloneFallback::IOCopy => std::io::copy(&mut src_file, &mut dst_file).map(|_| ()), + CloneFallback::FSCopy => std::fs::copy(source, dest).map(|_| ()), } } @@ -89,18 +99,31 @@ where } /// Copies `source` to `dest` using copy-on-write if possible. +/// +/// The `source_is_fifo` flag must be set to `true` if and only if +/// `source` is a FIFO (also known as a named pipe). In this case, +/// copy-on-write is not possible, so we copy the contents using +/// [`std::io::copy`]. pub(crate) fn copy_on_write( source: &Path, dest: &Path, reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, + source_is_fifo: bool, ) -> CopyResult<()> { let result = match (reflink_mode, sparse_mode) { (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), - (ReflinkMode::Auto, _) => clone(source, dest, true), - (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false), + + (ReflinkMode::Auto, _) => { + if source_is_fifo { + clone(source, dest, CloneFallback::IOCopy) + } else { + clone(source, dest, CloneFallback::FSCopy) + } + } + (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, CloneFallback::Error), (ReflinkMode::Always, _) => { return Err("`--reflink=always` can be used only with --sparse=auto".into()) } diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 7f1524154..1185b7f7d 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -4,7 +4,8 @@ // * file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; -use std::fs; +use std::fs::{self, File}; +use std::io; use std::os::unix::ffi::OsStrExt; use std::path::Path; @@ -13,12 +14,16 @@ use quick_error::ResultExt; use crate::{CopyResult, ReflinkMode, SparseMode}; /// Copies `source` to `dest` using copy-on-write if possible. +/// +/// The `source_is_fifo` flag must be set to `true` if and only if +/// `source` is a FIFO (also known as a named pipe). pub(crate) fn copy_on_write( source: &Path, dest: &Path, reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, + source_is_fifo: bool, ) -> CopyResult<()> { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); @@ -65,8 +70,15 @@ pub(crate) fn copy_on_write( format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), ) } - ReflinkMode::Auto => fs::copy(source, dest).context(context)?, - ReflinkMode::Never => fs::copy(source, dest).context(context)?, + _ => { + if source_is_fifo { + let mut src_file = File::open(source)?; + let mut dst_file = File::create(dest)?; + io::copy(&mut src_file, &mut dst_file).context(context)? + } else { + fs::copy(source, dest).context(context)? + } + } }; } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 8285905c5..e1ccdfb09 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs ROOTDIR USERDIR procfs +// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs ROOTDIR USERDIR procfs outfile use crate::common::util::*; #[cfg(not(windows))] @@ -2226,3 +2226,32 @@ fn test_copy_dir_preserve_permissions_inaccessible_file() { let metadata2 = at.metadata("d2"); assert_metadata_eq!(metadata1, metadata2); } + +/// Test for copying the contents of a FIFO as opposed to the FIFO object itself. +#[cfg(unix)] +#[test] +fn test_copy_contents_fifo() { + let scenario = TestScenario::new(util_name!()); + let at = &scenario.fixtures; + + // Start the `cp` process, reading the contents of `fifo` and + // writing to regular file `outfile`. + at.mkfifo("fifo"); + let mut ucmd = scenario.ucmd(); + let child = ucmd + .args(&["--copy-contents", "fifo", "outfile"]) + .run_no_wait(); + + // Write some bytes to the `fifo`. We expect these bytes to get + // copied through to `outfile`. + std::fs::write(at.plus("fifo"), "foo").unwrap(); + + // At this point the child process should have terminated + // successfully with no output. The `outfile` should have the + // contents of `fifo` copied into it. + let output = child.wait_with_output().unwrap(); + assert!(output.status.success()); + assert!(output.stdout.is_empty()); + assert!(output.stderr.is_empty()); + assert_eq!(at.read("outfile"), "foo"); +}