cat: check if the input file is also the output file

This commit is contained in:
Michael Debertol 2021-08-08 00:48:37 +02:00
parent d967a7a553
commit 03ceb6750e
4 changed files with 87 additions and 6 deletions

1
Cargo.lock generated
View file

@ -2023,6 +2023,7 @@ dependencies = [
"unix_socket",
"uucore",
"uucore_procs",
"winapi-util",
]
[[package]]

View file

@ -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"

View file

@ -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<T> = Result<T, CatError>;
@ -297,7 +302,13 @@ fn cat_handle<R: Read>(
}
}
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<String>, 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<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) {
if let Err(err) = cat_path(path, options, &mut state, &out_info) {
error_messages.push(format!("{}: {}", path, err));
}
}

View file

@ -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."
);
}