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 }
uucore = { workspace = true, features = [
"backup-control",
"buf-copy",
"entries",
"fs",
"fsxattr",

View file

@ -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<FileInformation>,
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) {
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<FileInformation>,
#[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 {

View file

@ -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<P>(source: P, dest: P) -> std::io::Result<u64>
/// 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<P>(source: P, dest: P, is_fifo: bool) -> std::io::Result<u64>
where
P: AsRef<Path>,
{
@ -250,8 +252,14 @@ where
.write(true)
.mode(mode)
.open(&dest)?;
let num_bytes_copied = std::io::copy(&mut src_file, &mut dst_file)?;
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<CopyDebug> {
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);

View file

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

View file

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

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.
#[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);
}