Repository: uutils/coreutils (language: rust)

Issue:
cat: should error when appending to input file redirected to stdin but doesn't
Environment: Ubuntu 20.04, uutil `main` branch (git commit d39dcad47ca4d9fa96e8f8132b4d1c298f1f7753), GNU coreutils v9.6.3-3189c-dirty.

Steps to reproduce:
```
touch f && cat <f >>f
```

What happens now: uutils `cat` succeeds with no output.

What I expected to happen: GNU `cat` terminates with an error message:
``` 
cat: -: input file is output file
```

Notes: this is causing a failure in the GNU test file `tests/cat/cat-self.sh`.


Discussion / hints:

Fix patch:
diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs
index 544af3138fd..0725971b4be 100644
--- a/src/uu/cat/src/cat.rs
+++ b/src/uu/cat/src/cat.rs
@@ -4,30 +4,31 @@
 // file that was distributed with this source code.
 
 // spell-checker:ignore (ToDO) nonprint nonblank nonprinting ELOOP
-use clap::{crate_version, Arg, ArgAction, Command};
 use std::fs::{metadata, File};
 use std::io::{self, IsTerminal, Read, Write};
-use thiserror::Error;
-use uucore::display::Quotable;
-use uucore::error::UResult;
-use uucore::fs::FileInformation;
-
-#[cfg(unix)]
-use std::os::fd::{AsFd, AsRawFd};
-
-/// Linux splice support
-#[cfg(any(target_os = "linux", target_os = "android"))]
-mod splice;
-
 /// Unix domain socket support
 #[cfg(unix)]
 use std::net::Shutdown;
 #[cfg(unix)]
+use std::os::fd::{AsFd, AsRawFd};
+#[cfg(unix)]
 use std::os::unix::fs::FileTypeExt;
 #[cfg(unix)]
 use std::os::unix::net::UnixStream;
+
+use clap::{crate_version, Arg, ArgAction, Command};
+#[cfg(unix)]
+use nix::fcntl::{fcntl, FcntlArg};
+use thiserror::Error;
+use uucore::display::Quotable;
+use uucore::error::UResult;
+use uucore::fs::FileInformation;
 use uucore::{format_usage, help_about, help_usage};
 
+/// Linux splice support
+#[cfg(any(target_os = "linux", target_os = "android"))]
+mod splice;
+
 const USAGE: &str = help_usage!("cat.md");
 const ABOUT: &str = help_about!("cat.md");
 
@@ -322,6 +323,24 @@ fn cat_handle<R: FdReadable>(
     }
 }
 
+/// Whether this process is appending to stdout.
+#[cfg(unix)]
+fn is_appending() -> bool {
+    let stdout = std::io::stdout();
+    let flags = match fcntl(stdout.as_raw_fd(), FcntlArg::F_GETFL) {
+        Ok(flags) => flags,
+        Err(_) => return false,
+    };
+    // TODO Replace `1 << 10` with `nix::fcntl::Oflag::O_APPEND`.
+    let o_append = 1 << 10;
+    (flags & o_append) > 0
+}
+
+#[cfg(not(unix))]
+fn is_appending() -> bool {
+    false
+}
+
 fn cat_path(
     path: &str,
     options: &OutputOptions,
@@ -331,10 +350,16 @@ fn cat_path(
     match get_input_type(path)? {
         InputType::StdIn => {
             let stdin = io::stdin();
+            let in_info = FileInformation::from_file(&stdin)?;
             let mut handle = InputHandle {
                 reader: stdin,
                 is_interactive: std::io::stdin().is_terminal(),
             };
+            if let Some(out_info) = out_info {
+                if in_info == *out_info && is_appending() {
+                    return Err(CatError::OutputIsInput);
+                }
+            }
             cat_handle(&mut handle, options, state)
         }
         InputType::Directory => Err(CatError::IsDirectory),


Test patch:
diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs
index d9be694365a..3e23191e3ba 100644
--- a/tests/by-util/test_cat.rs
+++ b/tests/by-util/test_cat.rs
@@ -9,6 +9,8 @@ use crate::common::util::vec_of_size;
 use crate::common::util::TestScenario;
 #[cfg(any(target_os = "linux", target_os = "android"))]
 use rlimit::Resource;
+#[cfg(target_os = "linux")]
+use std::fs::File;
 use std::fs::OpenOptions;
 #[cfg(not(windows))]
 use std::process::Stdio;
@@ -646,3 +648,23 @@ fn test_u_ignored() {
             .stdout_only("hello");
     }
 }
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_appending_same_input_output() {
+    let (at, mut ucmd) = at_and_ucmd!();
+
+    at.write("foo", "content");
+    let foo_file = at.plus_as_string("foo");
+
+    let file_read = File::open(&foo_file).unwrap();
+    let file_write = OpenOptions::new().append(true).open(&foo_file).unwrap();
+
+    ucmd.set_stdin(file_read);
+    ucmd.set_stdout(file_write);
+
+    ucmd.run()
+        .failure()
+        .no_stdout()
+        .stderr_contains("input file is output file");
+}