diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index fc32a4be5da..015d6dddf6e 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -8,7 +8,6 @@ use rustix::fs::{SeekFrom, ftruncate, ioctl_ficlone, seek}; use std::fs::File; use std::io::{self, Read}; use std::os::unix::fs::FileExt; -use std::os::unix::fs::FileTypeExt; use std::os::unix::fs::MetadataExt; use std::path::Path; @@ -154,7 +153,25 @@ fn sparse_copy_without_hole_fd(src_file: &File, dst_file: &File, context: &str) let ctx_err = |e: io::Error| CpError::IoErrContext(e, context.to_owned()); let size = src_file.metadata().map_err(&ctx_err)?.size(); - ftruncate(dst_file, size).map_err(|e| CpError::IoErrContext(e.into(), context.to_owned()))?; + // A fifo, a socket, or a character device such as the `/dev/null` that + // `/dev/stdout` may resolve to all reject `ftruncate` with `EINVAL` + // since they support neither it nor the writes at explicit offsets the + // rest of this function makes; take the standard copy instead, which is + // what GNU does with them. Discovered here rather than checked in + // advance, since checking first would cost every ordinary destination + // -- overwhelmingly a regular file, for which this never fires -- a + // `metadata` call for no benefit. Nothing has been read from `src_file` + // or written to `dst_file` yet, so falling back here cannot duplicate + // or corrupt output. + match ftruncate(dst_file, size) { + Ok(()) => {} + Err(rustix::io::Errno::INVAL) => { + let mut src = src_file; + let mut dst = dst_file; + return buf_copy::copy_fast(&mut src, &mut dst).map_err(&ctx_err); + } + Err(e) => return Err(CpError::IoErrContext(e.into(), context.to_owned())), + } let mut current_offset = 0; // Maximize the data read at once to 16 MiB to avoid memory hogging with large files // 16 MiB chunks should saturate an SSD @@ -194,7 +211,16 @@ fn sparse_copy_fd(src_file: &mut File, dst_file: &File, context: &str) -> CopyRe // Keep the size as u64: on 32-bit targets a usize conversion would // panic for sources of 4 GiB and more. let size = src_file.metadata().map_err(&ctx_err)?.size(); - ftruncate(dst_file, size).map_err(|e| CpError::IoErrContext(e.into(), context.to_owned()))?; + // See the matching comment in `sparse_copy_without_hole_fd` for why an + // `EINVAL` here falls back to a plain copy instead of erroring. + match ftruncate(dst_file, size) { + Ok(()) => {} + Err(rustix::io::Errno::INVAL) => { + let mut dst = dst_file; + return buf_copy::copy_fast(src_file, &mut dst).map_err(&ctx_err); + } + Err(e) => return Err(CpError::IoErrContext(e.into(), context.to_owned())), + } let blksize = dst_file.metadata().map_err(&ctx_err)?.blksize(); let mut buf: Vec = vec![0; blksize as usize]; @@ -224,12 +250,6 @@ fn sparse_copy_fd(src_file: &mut File, dst_file: &File, context: &str) -> CopyRe Ok(()) } -/// Checks whether an existing destination is a fifo -fn check_dest_is_fifo(dest: &Path) -> bool { - // If our destination file exists and its a fifo , we do a standard copy . - std::fs::metadata(dest).is_ok_and(|f| f.file_type().is_fifo()) -} - /// Copy the contents of a stream from `source` to `dest`. fn copy_stream

(source: P, dest: P, nofollow: bool, context: &str) -> CopyResult<()> where @@ -314,7 +334,7 @@ pub(crate) fn copy_on_write( let mut src_file = open_source(source, nofollow) .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; let mut copy_method = CopyMethod::Default; - let result = handle_reflink_never_sparse_always(&mut src_file, dest); + let result = handle_reflink_never_sparse_always(&mut src_file); if let Ok((debug, method)) = result { copy_debug = debug; copy_method = method; @@ -356,7 +376,7 @@ pub(crate) fn copy_on_write( let mut src_file = open_source(source, nofollow) .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; let mut copy_method = CopyMethod::Default; - let result = handle_reflink_never_sparse_auto(&mut src_file, dest); + let result = handle_reflink_never_sparse_auto(&mut src_file); if let Ok((debug, method)) = result { copy_debug = debug; copy_method = method; @@ -382,7 +402,7 @@ pub(crate) fn copy_on_write( let mut src_file = open_source(source, nofollow) .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; let mut copy_method = CopyMethod::Default; - let result = handle_reflink_auto_sparse_always(&mut src_file, dest); + let result = handle_reflink_auto_sparse_always(&mut src_file); if let Ok((debug, method)) = result { copy_debug = debug; copy_method = method; @@ -421,7 +441,7 @@ pub(crate) fn copy_on_write( let mut src_file = open_source(source, nofollow) .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; let mut copy_method = CopyMethod::Default; - let result = handle_reflink_auto_sparse_auto(&mut src_file, dest); + let result = handle_reflink_auto_sparse_auto(&mut src_file); if let Ok((debug, method)) = result { copy_debug = debug; copy_method = method; @@ -457,10 +477,7 @@ pub(crate) fn copy_on_write( /// Handles debug results when flags are "--reflink=auto" and "--sparse=always" and specifies what /// type of copy should be used -fn handle_reflink_auto_sparse_always( - src_file: &mut File, - dest: &Path, -) -> io::Result<(CopyDebug, CopyMethod)> { +fn handle_reflink_auto_sparse_always(src_file: &mut File) -> io::Result<(CopyDebug, CopyMethod)> { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::Unsupported, @@ -485,9 +502,6 @@ fn handle_reflink_auto_sparse_always( (true, false, _) => copy_debug.sparse_detection = SparseDebug::SeekHole, (_, _, _) => (), } - if check_dest_is_fifo(dest) { - copy_method = CopyMethod::FSCopy; - } Ok((copy_debug, copy_method)) } @@ -536,10 +550,7 @@ fn handle_reflink_auto_sparse_never(src_file: &mut File) -> io::Result io::Result<(CopyDebug, CopyMethod)> { +fn handle_reflink_auto_sparse_auto(src_file: &mut File) -> io::Result<(CopyDebug, CopyMethod)> { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::Unsupported, @@ -571,18 +582,12 @@ fn handle_reflink_auto_sparse_auto( copy_debug.sparse_detection = SparseDebug::SeekHole; } - if check_dest_is_fifo(dest) { - copy_method = CopyMethod::FSCopy; - } Ok((copy_debug, copy_method)) } /// Handles debug results when flags are "--reflink=never" and "--sparse=auto" and specifies what /// type of copy should be used -fn handle_reflink_never_sparse_auto( - src_file: &mut File, - dest: &Path, -) -> io::Result<(CopyDebug, CopyMethod)> { +fn handle_reflink_never_sparse_auto(src_file: &mut File) -> io::Result<(CopyDebug, CopyMethod)> { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::No, @@ -607,18 +612,12 @@ fn handle_reflink_never_sparse_auto( copy_debug.sparse_detection = SparseDebug::SeekHole; } - if check_dest_is_fifo(dest) { - copy_method = CopyMethod::FSCopy; - } Ok((copy_debug, copy_method)) } /// Handles debug results when flags are "--reflink=never" and "--sparse=always" and specifies what /// type of copy should be used -fn handle_reflink_never_sparse_always( - src_file: &mut File, - dest: &Path, -) -> io::Result<(CopyDebug, CopyMethod)> { +fn handle_reflink_never_sparse_always(src_file: &mut File) -> io::Result<(CopyDebug, CopyMethod)> { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::No, @@ -652,9 +651,6 @@ fn handle_reflink_never_sparse_always( (_, _, _) => (), } - if check_dest_is_fifo(dest) { - copy_method = CopyMethod::FSCopy; - } Ok((copy_debug, copy_method)) } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index f216f52a3be..893befa9d46 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -8937,3 +8937,16 @@ fn test_progressbar_inexistent_source() { .fails_with_code(1) .stderr_contains("cp: cannot stat 'inexistent1': No such file or directory"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_sparse_always_to_character_device() { + // The sparse paths use ftruncate and positional writes, which a character + // device rejects with EINVAL. Only a fifo destination was excluded, so + // `cp --sparse=always FILE /dev/null` failed where GNU copies normally. + let (at, mut ucmd) = at_and_ucmd!(); + at.write("src.txt", "hello world\n"); + ucmd.args(&["--sparse=always", "src.txt", "/dev/null"]) + .succeeds() + .no_stderr(); +}