diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 43113b4610c..ebcd8ff877e 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -28,6 +28,7 @@ quick-error = { workspace = true } selinux = { workspace = true, optional = true } uucore = { workspace = true, features = [ "backup-control", + "buf-copy", "entries", "fs", "fsxattr", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index f3bded69ef1..626b65ad63e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; #[cfg(not(windows))] use std::ffi::CString; use std::ffi::OsString; -use std::fs::{self, File, Metadata, OpenOptions, Permissions}; +use std::fs::{self, Metadata, OpenOptions, Permissions}; use std::io; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -1963,6 +1963,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { /// /// * `Ok(())` - The file was copied successfully. /// * `Err(CopyError)` - An error occurred while copying the file. +#[allow(clippy::too_many_arguments)] fn handle_copy_mode( source: &Path, dest: &Path, @@ -1971,15 +1972,10 @@ fn handle_copy_mode( source_metadata: &Metadata, symlinked_files: &mut HashSet, source_in_command_line: bool, + source_is_fifo: bool, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { - let source_file_type = source_metadata.file_type(); - - let source_is_symlink = source_file_type.is_symlink(); - - #[cfg(unix)] - let source_is_fifo = source_file_type.is_fifo(); - #[cfg(not(unix))] - let source_is_fifo = false; + let source_is_symlink = source_metadata.is_symlink(); match options.copy_mode { CopyMode::Link => { @@ -2016,6 +2012,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } CopyMode::SymLink => { @@ -2036,6 +2034,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } update_control::UpdateMode::ReplaceNone => { @@ -2066,6 +2066,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2079,6 +2081,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2305,6 +2309,18 @@ fn copy_file( let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; + #[cfg(unix)] + let source_is_fifo = source_metadata.file_type().is_fifo(); + #[cfg(not(unix))] + let source_is_fifo = false; + + #[cfg(unix)] + let source_is_stream = source_is_fifo + || source_metadata.file_type().is_char_device() + || source_metadata.file_type().is_block_device(); + #[cfg(not(unix))] + let source_is_stream = false; + handle_copy_mode( source, dest, @@ -2313,6 +2329,9 @@ fn copy_file( &source_metadata, symlinked_files, source_in_command_line, + source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; // TODO: implement something similar to gnu's lchown @@ -2328,8 +2347,16 @@ fn copy_file( if options.dereference(source_in_command_line) { if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { - copy_attributes(&src, dest, &options.attributes)?; + if src.exists() { + copy_attributes(&src, dest, &options.attributes)?; + } } + } else if source_is_stream && source.exists() { + // Some stream files may not exist after we have copied it, + // like anonymous pipes. Thus, we can't really copy its + // attributes. However, this is already handled in the stream + // copy function (see `copy_stream` under platform/linux.rs). + copy_attributes(source, dest, &options.attributes)?; } else { copy_attributes(source, dest, &options.attributes)?; } @@ -2393,6 +2420,7 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. +#[allow(clippy::too_many_arguments)] fn copy_helper( source: &Path, dest: &Path, @@ -2401,6 +2429,7 @@ fn copy_helper( source_is_symlink: bool, source_is_fifo: bool, symlinked_files: &mut HashSet, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { if options.parents { let parent = dest.parent().unwrap_or(dest); @@ -2411,12 +2440,7 @@ fn copy_helper( return Err(Error::NotADirectory(dest.to_path_buf())); } - if source.as_os_str() == "/dev/null" { - /* workaround a limitation of fs::copy - * https://github.com/rust-lang/rust/issues/79390 - */ - File::create(dest).context(dest.display().to_string())?; - } else if source_is_fifo && options.recursive && !options.copy_contents { + if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] copy_fifo(dest, options.overwrite, options.debug)?; } else if source_is_symlink { @@ -2428,8 +2452,10 @@ fn copy_helper( options.reflink_mode, options.sparse_mode, context, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + #[cfg(unix)] source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; if !options.attributes_only && options.debug { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 949bd5e03c7..0ca39a75ef2 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -12,6 +12,7 @@ use std::os::unix::fs::MetadataExt; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; use std::path::Path; +use uucore::buf_copy; use quick_error::ResultExt; @@ -220,8 +221,9 @@ fn check_dest_is_fifo(dest: &Path) -> bool { } } -/// Copy the contents of the given source FIFO to the given file. -fn copy_fifo_contents

(source: P, dest: P) -> std::io::Result +/// Copy the contents of a stream from `source` to `dest`. The `if_fifo` argument is used to +/// determine if we need to modify the file's attributes before and after copying. +fn copy_stream

(source: P, dest: P, is_fifo: bool) -> std::io::Result where P: AsRef, { @@ -250,8 +252,14 @@ where .write(true) .mode(mode) .open(&dest)?; - let num_bytes_copied = std::io::copy(&mut src_file, &mut dst_file)?; - dst_file.set_permissions(src_file.metadata()?.permissions())?; + + let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; + + if is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + Ok(num_bytes_copied) } @@ -268,6 +276,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, @@ -279,10 +288,9 @@ pub(crate) fn copy_on_write( copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for SparseMode::Always copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_always(source, dest); @@ -300,10 +308,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_never_sparse_never(source); if let Ok(debug) = result { @@ -315,9 +322,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Auto) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_auto(source, dest); @@ -335,10 +342,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Always) => { copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for // SparseMode::Always - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_always(source, dest); @@ -356,9 +362,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_auto_sparse_never(source); if let Ok(debug) = result { @@ -369,9 +375,9 @@ pub(crate) fn copy_on_write( } } (ReflinkMode::Auto, SparseMode::Auto) => { - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Unsupported; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_auto(source, dest); diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index bd47c44ae96..988dc6b2536 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -4,12 +4,14 @@ // file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; -use std::fs::{self, File}; -use std::io; +use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; @@ -24,6 +26,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); @@ -85,10 +88,23 @@ pub(crate) fn copy_on_write( } _ => { copy_debug.reflink = OffloadReflinkDebug::Yes; - if source_is_fifo { + if source_is_stream { let mut src_file = File::open(source)?; - let mut dst_file = File::create(dest)?; - io::copy(&mut src_file, &mut dst_file).context(context)? + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + let context = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + context } else { fs::copy(source, dest).context(context)? } diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs index c7942706868..2071e928f41 100644 --- a/src/uu/cp/src/platform/mod.rs +++ b/src/uu/cp/src/platform/mod.rs @@ -2,6 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +mod other_unix; +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +pub(crate) use self::other_unix::copy_on_write; + #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] @@ -12,7 +24,13 @@ mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] pub(crate) use self::linux::copy_on_write; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] mod other; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs new file mode 100644 index 00000000000..aa8fed3fab1 --- /dev/null +++ b/src/uu/cp/src/platform/other_unix.rs @@ -0,0 +1,62 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore reflink +use std::fs::{self, File, OpenOptions}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; + +use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; + +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, + source_is_fifo: bool, + source_is_stream: bool, +) -> CopyResult { + if reflink_mode != ReflinkMode::Never { + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + } + 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, + }; + + if source_is_stream { + let mut src_file = File::open(source)?; + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + return Ok(copy_debug); + } + + fs::copy(source, dest).context(context)?; + + Ok(copy_debug) +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index babd4885529..e44f35b8797 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3471,15 +3471,9 @@ fn test_same_file_force_backup() { } /// Test for copying the contents of a FIFO as opposed to the FIFO object itself. -#[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] +#[cfg(unix)] #[test] fn test_copy_contents_fifo() { - // TODO this test should work on FreeBSD, but the command was - // causing an error: - // - // cp: 'fifo' -> 'outfile': the source path is neither a regular file nor a symlink to a regular file - // - // the underlying `std::fs:copy` doesn't support copying fifo on freeBSD let scenario = TestScenario::new(util_name!()); let at = &scenario.fixtures; @@ -6037,3 +6031,19 @@ fn test_cp_preserve_xattr_readonly_source() { "Extended attributes were not preserved" ); } + +#[test] +#[cfg(unix)] +fn test_cp_from_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "target"; + let test_string = "Hello, World!\n"; + + ucmd.arg("/dev/fd/0") + .arg(target) + .pipe_in(test_string) + .succeeds(); + + assert!(at.file_exists(target)); + assert_eq!(at.read(target), test_string); +}