Repository: uutils/coreutils (2025-06-11T02:08:52Z)

Issue:
`cp` is is not properly cleaning existing file
I use the following command to get a "buffer" for my current clipboard stuff, which i use elsewhere - `wl-paste --watch cp /dev/stdin tmp`

when i was using gnu cp, with each clipboard copy, the `tmp` file is rewritten appropriately (old stuff cleaned, new stuff written). with uutils cp, old stuff partially remains. if original stuff was `n` characters long, and new content is `n'` (`n' < n`), then only first `n'` characters are rewritten, `n - n'` old characters remain. This seems very much dependent on how wl-paste is generateing output, but GNU cp, and busybox cp seem to handle it.

I know of `--remove-destination` option, and it works, but the way i am actually finally storing the buffer entries, is checking if the buffer file is modified, if modified, it handles the buffer. If i remove the file, the checker is thrown off.

My uutils package version is `0.0.29` from arch package, also tested `0.0.30`, same behaviour.

here is a video demo showing how existence of previous stuff in file 

![Image](https://github.com/user-attachments/assets/262b8f93-3c0e-4f2f-b7af-72cd3d3ea7e2)


Discussion:
Hi @sga-13, could you write out the steps to reproduce the issue? It's a bit hard to interpret the video.
I do not really know what the problem is, so i tried to demonstrate an example where it is breaking.

If you are using wayland, then you can install wl-clipboard (it may already be installed). If so, then run `wl-paste --watch cp /dev/stdin some_output_file`

if i replace output file with /dev/stdout, then behaviour is similar for both (newly copied stuff is just appended to stdout)

now anything you copy should be reflected in this file. with gnu or busybox cp, old clipboard entry is cleared well, and regardless of whatever length you copy, all old stuff is cleared properly. with uutils cp, only the starting bits are replaced. so, for example, earlier i ahd copied adcd...xyz, and then i copy aba, in the file, only aba should be present, but adadef...xyz is present. I do not have a idea how or why this would be happening. Maybe uu-cp is maintaing a buffer of the /dev/stdin, and /dev/stdin is maybe not clearly cleared when wl-paste updates it, so this happens. But I am just theorising. I have not been through any implentation of cp (or wl-paste) yet.
Thank you very much. I have not tested yet, but it seems to be the thing. Thanks again



Patch:
diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs
index e3f03a40d50..00fe8df1010 100644
--- a/src/uu/cp/src/cp.rs
+++ b/src/uu/cp/src/cp.rs
@@ -2545,12 +2545,7 @@ fn copy_file(
     #[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;
+    let source_is_stream = is_stream(&source_metadata);
 
     let performed_action = handle_copy_mode(
         source,
@@ -2620,6 +2615,19 @@ fn copy_file(
     Ok(())
 }
 
+fn is_stream(metadata: &Metadata) -> bool {
+    #[cfg(unix)]
+    {
+        let file_type = metadata.file_type();
+        file_type.is_fifo() || file_type.is_char_device() || file_type.is_block_device()
+    }
+    #[cfg(not(unix))]
+    {
+        let _ = metadata;
+        false
+    }
+}
+
 #[cfg(unix)]
 fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 {
     let (is_preserve_mode, is_explicit_no_preserve_mode) = options.preserve_mode();
@@ -2700,8 +2708,6 @@ fn copy_helper(
             options.sparse_mode,
             context,
             #[cfg(unix)]
-            source_is_fifo,
-            #[cfg(unix)]
             source_is_stream,
         )?;
 
diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs
index 56b8b2fe4de..c07b46aa9cb 100644
--- a/src/uu/cp/src/platform/linux.rs
+++ b/src/uu/cp/src/platform/linux.rs
@@ -18,6 +18,7 @@ use uucore::mode::get_umask;
 
 use crate::{
     CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode,
+    is_stream,
 };
 
 /// The fallback behavior for [`clone`] on failed system call.
@@ -220,9 +221,8 @@ fn check_dest_is_fifo(dest: &Path) -> bool {
     }
 }
 
-/// 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>
+/// Copy the contents of a stream from `source` to `dest`.
+fn copy_stream<P>(source: P, dest: P) -> std::io::Result<u64>
 where
     P: AsRef<Path>,
 {
@@ -252,29 +252,25 @@ where
         .mode(mode)
         .open(&dest)?;
 
+    let dest_is_stream = is_stream(&dst_file.metadata()?);
+    if !dest_is_stream {
+        // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually.
+        dst_file.set_len(0)?;
+    }
+
     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)
 }
 
 /// Copies `source` to `dest` using copy-on-write if possible.
-///
-/// The `source_is_fifo` flag must be set to `true` if and only if
-/// `source` is a FIFO (also known as a named pipe). In this case,
-/// copy-on-write is not possible, so we copy the contents using
-/// [`std::io::copy`].
 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> {
     let mut copy_debug = CopyDebug {
@@ -289,7 +285,7 @@ pub(crate) fn copy_on_write(
             copy_debug.reflink = OffloadReflinkDebug::No;
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Avoided;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).map(|_| ())
             } else {
                 let mut copy_method = CopyMethod::Default;
                 let result = handle_reflink_never_sparse_always(source, dest);
@@ -309,7 +305,7 @@ pub(crate) fn copy_on_write(
 
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Avoided;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).map(|_| ())
             } else {
                 let result = handle_reflink_never_sparse_never(source);
                 if let Ok(debug) = result {
@@ -323,7 +319,7 @@ pub(crate) fn copy_on_write(
 
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Avoided;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).map(|_| ())
             } else {
                 let mut copy_method = CopyMethod::Default;
                 let result = handle_reflink_never_sparse_auto(source, dest);
@@ -343,7 +339,7 @@ pub(crate) fn copy_on_write(
             // SparseMode::Always
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Avoided;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).map(|_| ())
             } else {
                 let mut copy_method = CopyMethod::Default;
                 let result = handle_reflink_auto_sparse_always(source, dest);
@@ -363,7 +359,7 @@ pub(crate) fn copy_on_write(
             copy_debug.reflink = OffloadReflinkDebug::No;
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Avoided;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).map(|_| ())
             } else {
                 let result = handle_reflink_auto_sparse_never(source);
                 if let Ok(debug) = result {
@@ -376,7 +372,7 @@ pub(crate) fn copy_on_write(
         (ReflinkMode::Auto, SparseMode::Auto) => {
             if source_is_stream {
                 copy_debug.offload = OffloadReflinkDebug::Unsupported;
-                copy_stream(source, dest, source_is_fifo).map(|_| ())
+                copy_stream(source, dest).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 8c915d2dfb0..d833c476a57 100644
--- a/src/uu/cp/src/platform/macos.rs
+++ b/src/uu/cp/src/platform/macos.rs
@@ -16,19 +16,16 @@ use uucore::mode::get_umask;
 
 use crate::{
     CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode,
+    is_stream,
 };
 
 /// Copies `source` to `dest` using copy-on-write if possible.
-///
-/// The `source_is_fifo` flag must be set to `true` if and only if
-/// `source` is a FIFO (also known as a named pipe).
 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 sparse_mode != SparseMode::Auto {
@@ -110,14 +107,15 @@ pub(crate) fn copy_on_write(
                         .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))
-                        .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?;
-
-                    if source_is_fifo {
-                        dst_file.set_permissions(src_file.metadata()?.permissions())?;
+                    let dest_is_stream = is_stream(&dst_file.metadata()?);
+                    if !dest_is_stream {
+                        // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually.
+                        dst_file.set_len(0)?;
                     }
-                    context
+
+                    buf_copy::copy_stream(&mut src_file, &mut dst_file)
+                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))
+                        .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?
                 } else {
                     fs::copy(source, dest)
                         .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?
diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs
index 00f3cbf6e2c..174eaca3db5 100644
--- a/src/uu/cp/src/platform/other_unix.rs
+++ b/src/uu/cp/src/platform/other_unix.rs
@@ -13,6 +13,7 @@ use uucore::mode::get_umask;
 
 use crate::{
     CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode,
+    is_stream,
 };
 
 /// Copies `source` to `dest` for systems without copy-on-write
@@ -22,7 +23,6 @@ pub(crate) fn copy_on_write(
     reflink_mode: ReflinkMode,
     sparse_mode: SparseMode,
     context: &str,
-    source_is_fifo: bool,
     source_is_stream: bool,
 ) -> CopyResult<CopyDebug> {
     if reflink_mode != ReflinkMode::Never {
@@ -50,13 +50,16 @@ pub(crate) fn copy_on_write(
             .mode(mode)
             .open(dest)?;
 
+        let dest_is_stream = is_stream(&dst_file.metadata()?);
+        if !dest_is_stream {
+            // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually.
+            dst_file.set_len(0)?;
+        }
+
         buf_copy::copy_stream(&mut src_file, &mut dst_file)
             .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))
             .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?;
 
-        if source_is_fifo {
-            dst_file.set_permissions(src_file.metadata()?.permissions())?;
-        }
         return Ok(copy_debug);
     }
 


Test patch:
diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs
index fc357686136..fda8bee872e 100644
--- a/tests/by-util/test_cp.rs
+++ b/tests/by-util/test_cp.rs
@@ -6247,6 +6247,55 @@ fn test_cp_update_none_interactive_prompt_no() {
     assert_eq!(at.read(new_file), "new content");
 }
 
+/// only unix has `/dev/fd/0`
+#[cfg(unix)]
+#[test]
+fn test_cp_from_stream() {
+    let target = "target";
+    let test_string1 = "longer: Hello, World!\n";
+    let test_string2 = "shorter";
+    let scenario = TestScenario::new(util_name!());
+    let at = &scenario.fixtures;
+    at.touch(target);
+
+    let mut ucmd = scenario.ucmd();
+    ucmd.arg("/dev/fd/0")
+        .arg(target)
+        .pipe_in(test_string1)
+        .succeeds();
+    assert_eq!(at.read(target), test_string1);
+
+    let mut ucmd = scenario.ucmd();
+    ucmd.arg("/dev/fd/0")
+        .arg(target)
+        .pipe_in(test_string2)
+        .succeeds();
+    assert_eq!(at.read(target), test_string2);
+}
+
+/// only unix has `/dev/fd/0`
+#[cfg(unix)]
+#[test]
+fn test_cp_from_stream_permission() {
+    let target = "target";
+    let link = "link";
+    let test_string = "Hello, World!\n";
+    let (at, mut ucmd) = at_and_ucmd!();
+
+    at.touch(target);
+    at.symlink_file(target, link);
+    let mode = 0o777;
+    at.set_mode("target", mode);
+
+    ucmd.arg("/dev/fd/0")
+        .arg(link)
+        .pipe_in(test_string)
+        .succeeds();
+
+    assert_eq!(at.read(target), test_string);
+    assert_eq!(at.metadata(target).permissions().mode(), 0o100_777);
+}
+
 #[cfg(feature = "feat_selinux")]
 fn get_getfattr_output(f: &str) -> String {
     use std::process::Command;