1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #7061 from DaringCuteSeal/cp-stream-2

cp: implement copying from streams
This commit is contained in:
Sylvestre Ledru 2025-01-13 14:13:13 +01:00 committed by GitHub
commit 74d80eab0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 189 additions and 50 deletions

View file

@ -28,6 +28,7 @@ quick-error = { workspace = true }
selinux = { workspace = true, optional = true } selinux = { workspace = true, optional = true }
uucore = { workspace = true, features = [ uucore = { workspace = true, features = [
"backup-control", "backup-control",
"buf-copy",
"entries", "entries",
"fs", "fs",
"fsxattr", "fsxattr",

View file

@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::ffi::CString; use std::ffi::CString;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::{self, File, Metadata, OpenOptions, Permissions}; use std::fs::{self, Metadata, OpenOptions, Permissions};
use std::io; use std::io;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::ffi::OsStrExt; 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. /// * `Ok(())` - The file was copied successfully.
/// * `Err(CopyError)` - An error occurred while copying the file. /// * `Err(CopyError)` - An error occurred while copying the file.
#[allow(clippy::too_many_arguments)]
fn handle_copy_mode( fn handle_copy_mode(
source: &Path, source: &Path,
dest: &Path, dest: &Path,
@ -1971,15 +1972,10 @@ fn handle_copy_mode(
source_metadata: &Metadata, source_metadata: &Metadata,
symlinked_files: &mut HashSet<FileInformation>, symlinked_files: &mut HashSet<FileInformation>,
source_in_command_line: bool, source_in_command_line: bool,
source_is_fifo: bool,
#[cfg(unix)] source_is_stream: bool,
) -> CopyResult<()> { ) -> CopyResult<()> {
let source_file_type = source_metadata.file_type(); let source_is_symlink = source_metadata.is_symlink();
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;
match options.copy_mode { match options.copy_mode {
CopyMode::Link => { CopyMode::Link => {
@ -2016,6 +2012,8 @@ fn handle_copy_mode(
source_is_symlink, source_is_symlink,
source_is_fifo, source_is_fifo,
symlinked_files, symlinked_files,
#[cfg(unix)]
source_is_stream,
)?; )?;
} }
CopyMode::SymLink => { CopyMode::SymLink => {
@ -2036,6 +2034,8 @@ fn handle_copy_mode(
source_is_symlink, source_is_symlink,
source_is_fifo, source_is_fifo,
symlinked_files, symlinked_files,
#[cfg(unix)]
source_is_stream,
)?; )?;
} }
update_control::UpdateMode::ReplaceNone => { update_control::UpdateMode::ReplaceNone => {
@ -2066,6 +2066,8 @@ fn handle_copy_mode(
source_is_symlink, source_is_symlink,
source_is_fifo, source_is_fifo,
symlinked_files, symlinked_files,
#[cfg(unix)]
source_is_stream,
)?; )?;
} }
} }
@ -2079,6 +2081,8 @@ fn handle_copy_mode(
source_is_symlink, source_is_symlink,
source_is_fifo, source_is_fifo,
symlinked_files, 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)?; 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( handle_copy_mode(
source, source,
dest, dest,
@ -2313,6 +2329,9 @@ fn copy_file(
&source_metadata, &source_metadata,
symlinked_files, symlinked_files,
source_in_command_line, source_in_command_line,
source_is_fifo,
#[cfg(unix)]
source_is_stream,
)?; )?;
// TODO: implement something similar to gnu's lchown // TODO: implement something similar to gnu's lchown
@ -2328,8 +2347,16 @@ fn copy_file(
if options.dereference(source_in_command_line) { if options.dereference(source_in_command_line) {
if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { 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 { } else {
copy_attributes(source, dest, &options.attributes)?; 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 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. /// copy-on-write scheme if --reflink is specified and the filesystem supports it.
#[allow(clippy::too_many_arguments)]
fn copy_helper( fn copy_helper(
source: &Path, source: &Path,
dest: &Path, dest: &Path,
@ -2401,6 +2429,7 @@ fn copy_helper(
source_is_symlink: bool, source_is_symlink: bool,
source_is_fifo: bool, source_is_fifo: bool,
symlinked_files: &mut HashSet<FileInformation>, symlinked_files: &mut HashSet<FileInformation>,
#[cfg(unix)] source_is_stream: bool,
) -> CopyResult<()> { ) -> CopyResult<()> {
if options.parents { if options.parents {
let parent = dest.parent().unwrap_or(dest); let parent = dest.parent().unwrap_or(dest);
@ -2411,12 +2440,7 @@ fn copy_helper(
return Err(Error::NotADirectory(dest.to_path_buf())); return Err(Error::NotADirectory(dest.to_path_buf()));
} }
if source.as_os_str() == "/dev/null" { if source_is_fifo && options.recursive && !options.copy_contents {
/* 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 {
#[cfg(unix)] #[cfg(unix)]
copy_fifo(dest, options.overwrite, options.debug)?; copy_fifo(dest, options.overwrite, options.debug)?;
} else if source_is_symlink { } else if source_is_symlink {
@ -2428,8 +2452,10 @@ fn copy_helper(
options.reflink_mode, options.reflink_mode,
options.sparse_mode, options.sparse_mode,
context, context,
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] #[cfg(unix)]
source_is_fifo, source_is_fifo,
#[cfg(unix)]
source_is_stream,
)?; )?;
if !options.attributes_only && options.debug { if !options.attributes_only && options.debug {

View file

@ -12,6 +12,7 @@ use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::path::Path; use std::path::Path;
use uucore::buf_copy;
use quick_error::ResultExt; 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. /// Copy the contents of a stream from `source` to `dest`. The `if_fifo` argument is used to
fn copy_fifo_contents<P>(source: P, dest: P) -> std::io::Result<u64> /// determine if we need to modify the file's attributes before and after copying.
fn copy_stream<P>(source: P, dest: P, is_fifo: bool) -> std::io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -250,8 +252,14 @@ where
.write(true) .write(true)
.mode(mode) .mode(mode)
.open(&dest)?; .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) Ok(num_bytes_copied)
} }
@ -268,6 +276,7 @@ pub(crate) fn copy_on_write(
sparse_mode: SparseMode, sparse_mode: SparseMode,
context: &str, context: &str,
source_is_fifo: bool, source_is_fifo: bool,
source_is_stream: bool,
) -> CopyResult<CopyDebug> { ) -> CopyResult<CopyDebug> {
let mut copy_debug = CopyDebug { let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown, offload: OffloadReflinkDebug::Unknown,
@ -279,10 +288,9 @@ pub(crate) fn copy_on_write(
copy_debug.sparse_detection = SparseDebug::Zeros; copy_debug.sparse_detection = SparseDebug::Zeros;
// Default SparseDebug val for SparseMode::Always // Default SparseDebug val for SparseMode::Always
copy_debug.reflink = OffloadReflinkDebug::No; copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
copy_fifo_contents(source, dest).map(|_| ())
} else { } else {
let mut copy_method = CopyMethod::Default; let mut copy_method = CopyMethod::Default;
let result = handle_reflink_never_sparse_always(source, dest); let result = handle_reflink_never_sparse_always(source, dest);
@ -300,10 +308,9 @@ pub(crate) fn copy_on_write(
(ReflinkMode::Never, SparseMode::Never) => { (ReflinkMode::Never, SparseMode::Never) => {
copy_debug.reflink = OffloadReflinkDebug::No; copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
copy_fifo_contents(source, dest).map(|_| ())
} else { } else {
let result = handle_reflink_never_sparse_never(source); let result = handle_reflink_never_sparse_never(source);
if let Ok(debug) = result { if let Ok(debug) = result {
@ -315,9 +322,9 @@ pub(crate) fn copy_on_write(
(ReflinkMode::Never, SparseMode::Auto) => { (ReflinkMode::Never, SparseMode::Auto) => {
copy_debug.reflink = OffloadReflinkDebug::No; copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_fifo_contents(source, dest).map(|_| ()) copy_stream(source, dest, source_is_fifo).map(|_| ())
} else { } else {
let mut copy_method = CopyMethod::Default; let mut copy_method = CopyMethod::Default;
let result = handle_reflink_never_sparse_auto(source, dest); let result = handle_reflink_never_sparse_auto(source, dest);
@ -335,10 +342,9 @@ pub(crate) fn copy_on_write(
(ReflinkMode::Auto, SparseMode::Always) => { (ReflinkMode::Auto, SparseMode::Always) => {
copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for
// SparseMode::Always // SparseMode::Always
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
copy_fifo_contents(source, dest).map(|_| ())
} else { } else {
let mut copy_method = CopyMethod::Default; let mut copy_method = CopyMethod::Default;
let result = handle_reflink_auto_sparse_always(source, dest); let result = handle_reflink_auto_sparse_always(source, dest);
@ -356,9 +362,9 @@ pub(crate) fn copy_on_write(
(ReflinkMode::Auto, SparseMode::Never) => { (ReflinkMode::Auto, SparseMode::Never) => {
copy_debug.reflink = OffloadReflinkDebug::No; copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided; copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_fifo_contents(source, dest).map(|_| ()) copy_stream(source, dest, source_is_fifo).map(|_| ())
} else { } else {
let result = handle_reflink_auto_sparse_never(source); let result = handle_reflink_auto_sparse_never(source);
if let Ok(debug) = result { if let Ok(debug) = result {
@ -369,9 +375,9 @@ pub(crate) fn copy_on_write(
} }
} }
(ReflinkMode::Auto, SparseMode::Auto) => { (ReflinkMode::Auto, SparseMode::Auto) => {
if source_is_fifo { if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Unsupported; copy_debug.offload = OffloadReflinkDebug::Unsupported;
copy_fifo_contents(source, dest).map(|_| ()) copy_stream(source, dest, source_is_fifo).map(|_| ())
} else { } else {
let mut copy_method = CopyMethod::Default; let mut copy_method = CopyMethod::Default;
let result = handle_reflink_auto_sparse_auto(source, dest); let result = handle_reflink_auto_sparse_auto(source, dest);

View file

@ -4,12 +4,14 @@
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore reflink // spell-checker:ignore reflink
use std::ffi::CString; use std::ffi::CString;
use std::fs::{self, File}; use std::fs::{self, File, OpenOptions};
use std::io;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path; use std::path::Path;
use quick_error::ResultExt; use quick_error::ResultExt;
use uucore::buf_copy;
use uucore::mode::get_umask;
use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode};
@ -24,6 +26,7 @@ pub(crate) fn copy_on_write(
sparse_mode: SparseMode, sparse_mode: SparseMode,
context: &str, context: &str,
source_is_fifo: bool, source_is_fifo: bool,
source_is_stream: bool,
) -> CopyResult<CopyDebug> { ) -> CopyResult<CopyDebug> {
if sparse_mode != SparseMode::Auto { if sparse_mode != SparseMode::Auto {
return Err("--sparse is only supported on linux".to_string().into()); 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; copy_debug.reflink = OffloadReflinkDebug::Yes;
if source_is_fifo { if source_is_stream {
let mut src_file = File::open(source)?; let mut src_file = File::open(source)?;
let mut dst_file = File::create(dest)?; let mode = 0o622 & !get_umask();
io::copy(&mut src_file, &mut dst_file).context(context)? 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 { } else {
fs::copy(source, dest).context(context)? fs::copy(source, dest).context(context)?
} }

View file

@ -2,6 +2,18 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // 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")] #[cfg(target_os = "macos")]
mod macos; mod macos;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -12,7 +24,13 @@ mod linux;
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) use self::linux::copy_on_write; 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; 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; pub(crate) use self::other::copy_on_write;

View file

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

View file

@ -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. /// 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] #[test]
fn test_copy_contents_fifo() { 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 scenario = TestScenario::new(util_name!());
let at = &scenario.fixtures; let at = &scenario.fixtures;
@ -6037,3 +6031,19 @@ fn test_cp_preserve_xattr_readonly_source() {
"Extended attributes were not preserved" "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);
}