Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/uu/cat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ uucore = { workspace = true, features = ["fast-inc", "fs", "pipes"] }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }

[target.'cfg(windows)'.dependencies]
winapi-util = { workspace = true }
windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] }

[dev-dependencies]
tempfile = { workspace = true }

[[bin]]
name = "cat"
path = "src/main.rs"
54 changes: 11 additions & 43 deletions src/uu/cat/src/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) nonprint nonblank nonprinting ELOOP

mod platform;

use crate::platform::is_unsafe_overwrite;
use std::fs::{File, metadata};
use std::io::{self, BufWriter, IsTerminal, Read, Write};
/// Unix domain socket support
Expand All @@ -18,12 +22,9 @@ use std::os::unix::net::UnixStream;

use clap::{Arg, ArgAction, Command};
use memchr::memchr2;
#[cfg(unix)]
use nix::fcntl::{FcntlArg, fcntl};
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::fs::FileInformation;
use uucore::locale::get_message;
use uucore::{fast_inc::fast_inc_one, format_usage};

Expand Down Expand Up @@ -366,42 +367,17 @@ fn cat_handle<R: FdReadable>(
}
}

/// Whether this process is appending to stdout.
#[cfg(unix)]
fn is_appending() -> bool {
let stdout = io::stdout();
let Ok(flags) = fcntl(stdout.as_fd(), FcntlArg::F_GETFL) else {
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,
state: &mut OutputState,
out_info: Option<&FileInformation>,
) -> CatResult<()> {
fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> {
match get_input_type(path)? {
InputType::StdIn => {
let stdin = io::stdin();
let in_info = FileInformation::from_file(&stdin)?;
if is_unsafe_overwrite(&stdin, &io::stdout()) {
return Err(CatError::OutputIsInput);
}
let mut handle = InputHandle {
reader: stdin,
is_interactive: 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),
Expand All @@ -417,15 +393,9 @@ fn cat_path(
}
_ => {
let file = File::open(path)?;

if let Some(out_info) = out_info {
if out_info.file_size() != 0
&& FileInformation::from_file(&file).ok().as_ref() == Some(out_info)
{
return Err(CatError::OutputIsInput);
}
if is_unsafe_overwrite(&file, &io::stdout()) {
return Err(CatError::OutputIsInput);
}

let mut handle = InputHandle {
reader: file,
is_interactive: false,
Expand All @@ -436,8 +406,6 @@ fn cat_path(
}

fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {
let out_info = FileInformation::from_file(&io::stdout()).ok();

let mut state = OutputState {
line_number: LineNumber::new(),
at_line_start: true,
Expand All @@ -447,7 +415,7 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {
let mut error_messages: Vec<String> = Vec::new();

for path in files {
if let Err(err) = cat_path(path, options, &mut state, out_info.as_ref()) {
if let Err(err) = cat_path(path, options, &mut state) {
error_messages.push(format!("{}: {err}", path.maybe_quote()));
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/uu/cat/src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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.

#[cfg(unix)]
pub use self::unix::is_unsafe_overwrite;

#[cfg(windows)]
pub use self::windows::is_unsafe_overwrite;

#[cfg(unix)]
mod unix;

#[cfg(windows)]
mod windows;
108 changes: 108 additions & 0 deletions src/uu/cat/src/platform/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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 lseek seekable

use nix::fcntl::{FcntlArg, OFlag, fcntl};
use nix::unistd::{Whence, lseek};
use std::os::fd::AsFd;
use uucore::fs::FileInformation;

/// An unsafe overwrite occurs when the same nonempty file is used as both stdin and stdout,
/// and the file offset of stdin is positioned earlier than that of stdout.
/// In this scenario, bytes read from stdin are written to a later part of the file
/// via stdout, which can then be read again by stdin and written again by stdout,
/// causing an infinite loop and potential file corruption.
pub fn is_unsafe_overwrite<I: AsFd, O: AsFd>(input: &I, output: &O) -> bool {
// `FileInformation::from_file` returns an error if the file descriptor is closed, invalid,
// or refers to a non-regular file (e.g., socket, pipe, or special device).
let Ok(input_info) = FileInformation::from_file(input) else {
return false;

Check warning on line 22 in src/uu/cat/src/platform/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cat/src/platform/unix.rs#L22

Added line #L22 was not covered by tests
};
let Ok(output_info) = FileInformation::from_file(output) else {
return false;

Check warning on line 25 in src/uu/cat/src/platform/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cat/src/platform/unix.rs#L25

Added line #L25 was not covered by tests
};
if input_info != output_info || output_info.file_size() == 0 {
return false;
}
if is_appending(output) {
return true;
}
// `lseek` returns an error if the file descriptor is closed or it refers to
// a non-seekable resource (e.g., pipe, socket, or some devices).
let Ok(input_pos) = lseek(input.as_fd(), 0, Whence::SeekCur) else {
return false;

Check warning on line 36 in src/uu/cat/src/platform/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cat/src/platform/unix.rs#L36

Added line #L36 was not covered by tests
};
let Ok(output_pos) = lseek(output.as_fd(), 0, Whence::SeekCur) else {
return false;

Check warning on line 39 in src/uu/cat/src/platform/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cat/src/platform/unix.rs#L39

Added line #L39 was not covered by tests
};
input_pos < output_pos
}

/// Whether the file is opened with the `O_APPEND` flag
fn is_appending<F: AsFd>(file: &F) -> bool {
let flags_raw = fcntl(file.as_fd(), FcntlArg::F_GETFL).unwrap_or_default();
let flags = OFlag::from_bits_truncate(flags_raw);
flags.contains(OFlag::O_APPEND)
}

#[cfg(test)]
mod tests {
use crate::platform::unix::{is_appending, is_unsafe_overwrite};
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
use tempfile::NamedTempFile;

#[test]
fn test_is_appending() {
let temp_file = NamedTempFile::new().unwrap();
assert!(!is_appending(&temp_file));

let read_file = OpenOptions::new().read(true).open(&temp_file).unwrap();
assert!(!is_appending(&read_file));

let write_file = OpenOptions::new().write(true).open(&temp_file).unwrap();
assert!(!is_appending(&write_file));

let append_file = OpenOptions::new().append(true).open(&temp_file).unwrap();
assert!(is_appending(&append_file));
}

#[test]
fn test_is_unsafe_overwrite() {
// Create two temp files one of which is empty
let empty = NamedTempFile::new().unwrap();
let mut nonempty = NamedTempFile::new().unwrap();
nonempty.write_all(b"anything").unwrap();
nonempty.seek(SeekFrom::Start(0)).unwrap();

// Using a different file as input and output does not result in an overwrite
assert!(!is_unsafe_overwrite(&empty, &nonempty));

// Overwriting an empty file is always safe
assert!(!is_unsafe_overwrite(&empty, &empty));

// Overwriting a nonempty file with itself is safe
assert!(!is_unsafe_overwrite(&nonempty, &nonempty));

// Overwriting an empty file opened in append mode is safe
let empty_append = OpenOptions::new().append(true).open(&empty).unwrap();
assert!(!is_unsafe_overwrite(&empty, &empty_append));

// Overwriting a nonempty file opened in append mode is unsafe
let nonempty_append = OpenOptions::new().append(true).open(&nonempty).unwrap();
assert!(is_unsafe_overwrite(&nonempty, &nonempty_append));

// Overwriting a file opened in write mode is safe
let mut nonempty_write = OpenOptions::new().write(true).open(&nonempty).unwrap();
assert!(!is_unsafe_overwrite(&nonempty, &nonempty_write));

// Overwriting a file when the input and output file descriptors are pointing to
// different offsets is safe if the input offset is further than the output offset
nonempty_write.seek(SeekFrom::Start(1)).unwrap();
assert!(!is_unsafe_overwrite(&nonempty_write, &nonempty));
assert!(is_unsafe_overwrite(&nonempty, &nonempty_write));
}
}
56 changes: 56 additions & 0 deletions src/uu/cat/src/platform/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.

use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use uucore::fs::FileInformation;
use winapi_util::AsHandleRef;
use windows_sys::Win32::Storage::FileSystem::{
FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW, VOLUME_NAME_NT,
};

/// An unsafe overwrite occurs when the same file is used as both stdin and stdout
/// and the stdout file is not empty.
pub fn is_unsafe_overwrite<I: AsHandleRef, O: AsHandleRef>(input: &I, output: &O) -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function differs from the Unix version in that it does not attempt to determine when a file is safe to be overwritten, namely, when the input handle is pointing to an earlier or the same position of the file as the output handle. I also could not figure out an easy way of determining if the file handle is opened for append, like the O_APPEND flag in Unix.

if !is_same_file_by_path(input, output) {
return false;
}

// Check if the output file is empty
FileInformation::from_file(output)
.map(|info| info.file_size() > 0)
.unwrap_or(false)
Comment on lines +18 to +25
Copy link
Contributor Author

@frendsick frendsick Jun 4, 2025

Choose a reason for hiding this comment

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

I would have liked to do the same thing as with Unix, but unfortunately, the FileInformation objects were not equal when the same file was used as the input and output in Windows. Even their file indexes were different. I could not figure out a simpler way to verify if the files are the same than to use Win32 API to carve the file paths from the handles using GetFinalPathNameByHandleW. I am open to suggestions on a better way to do this.

The corresponding Unix code for a reference:

    let Ok(input_info) = FileInformation::from_file(input) else {
        return false;
    };
    let Ok(output_info) = FileInformation::from_file(output) else {
        return false;
    };
    if input_info != output_info || output_info.file_size() == 0 {
        return false;
    }

}

/// Get the file path for a file handle
fn get_file_path_from_handle<F: AsHandleRef>(file: &F) -> Option<PathBuf> {
let handle = file.as_raw();
let mut path_buf = vec![0u16; 4096];

// SAFETY: We should check how many bytes was written to `path_buf`
// and only read that many bytes from it.
let len = unsafe {
GetFinalPathNameByHandleW(
handle,
path_buf.as_mut_ptr(),
path_buf.len() as u32,
FILE_NAME_NORMALIZED | VOLUME_NAME_NT,
)
};
if len == 0 {
return None;
}
let path = OsString::from_wide(&path_buf[..len as usize]);
Some(PathBuf::from(path))
}

/// Compare two file handles if they correspond to the same file
fn is_same_file_by_path<A: AsHandleRef, B: AsHandleRef>(a: &A, b: &B) -> bool {
match (get_file_path_from_handle(a), get_file_path_from_handle(b)) {
(Some(path1), Some(path2)) => path1 == path2,
_ => false,
}
}
52 changes: 52 additions & 0 deletions tests/by-util/test_cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use rlimit::Resource;
#[cfg(unix)]
use std::fs::File;
use std::fs::OpenOptions;
use std::fs::read_to_string;
use std::process::Stdio;
use uutests::at_and_ucmd;
use uutests::new_ucmd;
Expand Down Expand Up @@ -637,6 +638,57 @@ fn test_write_to_self() {
);
}

/// Test derived from the following GNU test in `tests/cat/cat-self.sh`:
///
/// `cat fxy2 fy 1<>fxy2`
// TODO: make this work on windows
#[test]
#[cfg(unix)]
fn test_successful_write_to_read_write_self() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("fy", "y");
at.write("fxy2", "x");

// Open `rw_file` as both stdin and stdout (read/write)
let fxy2_file_path = at.plus("fxy2");
let fxy2_file = OpenOptions::new()
.read(true)
.write(true)
.open(&fxy2_file_path)
.unwrap();
ucmd.args(&["fxy2", "fy"]).set_stdout(fxy2_file).succeeds();

// The contents of `fxy2` and `fy` files should be merged
let fxy2_contents = read_to_string(fxy2_file_path).unwrap();
assert_eq!(fxy2_contents, "xy");
}

/// Test derived from the following GNU test in `tests/cat/cat-self.sh`:
///
/// `cat fx fx3 1<>fx3`
#[test]
fn test_failed_write_to_read_write_self() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("fx", "g");
at.write("fx3", "bold");

// Open `rw_file` as both stdin and stdout (read/write)
let fx3_file_path = at.plus("fx3");
let fx3_file = OpenOptions::new()
.read(true)
.write(true)
.open(&fx3_file_path)
.unwrap();
ucmd.args(&["fx", "fx3"])
.set_stdout(fx3_file)
.fails_with_code(1)
.stderr_only("cat: fx3: input file is output file\n");

// The contents of `fx` should have overwritten the beginning of `fx3`
let fx3_contents = read_to_string(fx3_file_path).unwrap();
assert_eq!(fx3_contents, "gold");
}

#[test]
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
Expand Down
Loading