From 03ceb6750e54f643e39cfb701b98bdbceb4e3b02 Mon Sep 17 00:00:00 2001 From: Michael Debertol Date: Sun, 8 Aug 2021 00:48:37 +0200 Subject: [PATCH] cat: check if the input file is also the output file --- Cargo.lock | 1 + src/uu/cat/Cargo.toml | 5 +++-- src/uu/cat/src/cat.rs | 40 ++++++++++++++++++++++++++++++--- tests/by-util/test_cat.rs | 47 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b954fffaf..9fe45b941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2023,6 +2023,7 @@ dependencies = [ "unix_socket", "uucore", "uucore_procs", + "winapi-util", ] [[package]] diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index b0721cee0..d80514385 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -23,9 +23,10 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [target.'cfg(unix)'.dependencies] unix_socket = "0.5.0" +nix = "0.20.0" -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -nix = "0.20" +[target.'cfg(windows)'.dependencies] +winapi-util = "0.1.5" [[bin]] name = "cat" diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index f340fa9fa..9d982c048 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -22,11 +22,14 @@ use std::io::{self, Read, Write}; use thiserror::Error; use uucore::error::UResult; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; + /// Linux splice support #[cfg(any(target_os = "linux", target_os = "android"))] mod splice; #[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; /// Unix domain socket support #[cfg(unix)] @@ -59,6 +62,8 @@ enum CatError { }, #[error("Is a directory")] IsDirectory, + #[error("input file is output file")] + OutputIsInput, } type CatResult = Result; @@ -297,7 +302,13 @@ fn cat_handle( } } -fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { +fn cat_path( + path: &str, + options: &OutputOptions, + state: &mut OutputState, + #[cfg(unix)] out_info: &nix::sys::stat::FileStat, + #[cfg(windows)] out_info: &winapi_util::file::Information, +) -> CatResult<()> { if path == "-" { let stdin = io::stdin(); let mut handle = InputHandle { @@ -324,6 +335,10 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat } _ => { let file = File::open(path)?; + #[cfg(any(windows, unix))] + if same_file(out_info, &file) { + return Err(CatError::OutputIsInput); + } let mut handle = InputHandle { #[cfg(any(target_os = "linux", target_os = "android"))] file_descriptor: file.as_raw_fd(), @@ -335,7 +350,26 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat } } +#[cfg(unix)] +fn same_file(a_info: &nix::sys::stat::FileStat, b: &File) -> bool { + let b_info = nix::sys::stat::fstat(b.as_raw_fd()).unwrap(); + b_info.st_size != 0 && b_info.st_dev == a_info.st_dev && b_info.st_ino == a_info.st_ino +} + +#[cfg(windows)] +fn same_file(a_info: &winapi_util::file::Information, b: &File) -> bool { + let b_info = winapi_util::file::information(b).unwrap(); + b_info.file_size() != 0 + && b_info.volume_serial_number() == a_info.volume_serial_number() + && b_info.file_index() == a_info.file_index() +} + fn cat_files(files: Vec, options: &OutputOptions) -> UResult<()> { + #[cfg(windows)] + let out_info = winapi_util::file::information(&std::io::stdout()).unwrap(); + #[cfg(unix)] + let out_info = nix::sys::stat::fstat(std::io::stdout().as_raw_fd()).unwrap(); + let mut state = OutputState { line_number: 1, at_line_start: true, @@ -343,7 +377,7 @@ fn cat_files(files: Vec, options: &OutputOptions) -> UResult<()> { let mut error_messages: Vec = Vec::new(); for path in &files { - if let Err(err) = cat_path(path, options, &mut state) { + if let Err(err) = cat_path(path, options, &mut state, &out_info) { error_messages.push(format!("{}: {}", path, err)); } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index d83b5515b..03fea23e9 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,5 +1,4 @@ use crate::common::util::*; -#[cfg(unix)] use std::fs::OpenOptions; #[cfg(unix)] use std::io::Read; @@ -443,3 +442,49 @@ fn test_domain_socket() { thread.join().unwrap(); } + +#[test] +fn test_write_to_self_empty() { + // it's ok if the input file is also the output file if it's empty + let s = TestScenario::new(util_name!()); + let file_path = s.fixtures.plus("file.txt"); + + let file = OpenOptions::new() + .create_new(true) + .write(true) + .append(true) + .open(&file_path) + .unwrap(); + + s.ucmd().set_stdout(file).arg(&file_path).succeeds(); +} + +#[test] +fn test_write_to_self() { + let s = TestScenario::new(util_name!()); + let file_path = s.fixtures.plus("first_file"); + s.fixtures.write("second_file", "second_file_content."); + + let file = OpenOptions::new() + .create_new(true) + .write(true) + .append(true) + .open(&file_path) + .unwrap(); + + s.fixtures.append("first_file", "first_file_content."); + + s.ucmd() + .set_stdout(file) + .arg("first_file") + .arg("first_file") + .arg("second_file") + .fails() + .code_is(2) + .stderr_only("cat: first_file: input file is output file\ncat: first_file: input file is output file"); + + assert_eq!( + s.fixtures.read("first_file"), + "first_file_content.second_file_content." + ); +}