Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 37 additions & 41 deletions src/uu/cp/src/platform/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Comment on lines +169 to +170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to rename them?

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
Expand Down Expand Up @@ -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<u8> = vec![0; blksize as usize];
Expand Down Expand Up @@ -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<P>(source: P, dest: P, nofollow: bool, context: &str) -> CopyResult<()>
where
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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))
}

Expand Down Expand Up @@ -536,10 +550,7 @@ fn handle_reflink_auto_sparse_never(src_file: &mut File) -> io::Result<CopyDebug

/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what
/// type of copy should be used
fn handle_reflink_auto_sparse_auto(
src_file: &mut File,
dest: &Path,
) -> 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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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))
}
13 changes: 13 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +8943 to +8946

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
fn test_cp_sparse_always_to_non_truncatable() {
// The sparse paths use ftruncate. Fallback to normal copy when target is not truncatable e.g. `/dev/null`.

More generic name.

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();
}
Loading