mirror of
https://github.com/uutils/coreutils
synced 2024-07-21 09:54:42 +00:00
cat: Unrevert splice patch (#2020)
* cat: Unrevert splice patch * cat: Add fifo test * cat: Add tests for error cases * cat: Add tests for character devices * wc: Make sure we handle short splice writes * cat: Fix tests for 1.40.0 compiler * cat: Run rustfmt on test_cat.rs * Run 'cargo +1.40.0 update'
This commit is contained in:
parent
bf1944271c
commit
eb4971e6f4
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1396,9 +1396,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.68"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
|
||||
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.9",
|
||||
|
@ -1618,7 +1618,8 @@ name = "uu_cat"
|
|||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"quick-error",
|
||||
"nix 0.20.0",
|
||||
"thiserror",
|
||||
"unix_socket",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
|
|
|
@ -16,13 +16,16 @@ path = "src/cat.rs"
|
|||
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
quick-error = "1.2.3"
|
||||
thiserror = "1.0"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
unix_socket = "0.5.0"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
nix = "0.20"
|
||||
|
||||
[[bin]]
|
||||
name = "cat"
|
||||
path = "src/main.rs"
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
// (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
// (c) Evgeniy Klyuchikov <evgeniy.klyuchikov@gmail.com>
|
||||
// (c) Joshua S. Miller <jsmiller@uchicago.edu>
|
||||
// (c) Árni Dagur <arni@dagur.eu>
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore (ToDO) nonprint nonblank nonprinting
|
||||
|
||||
#[macro_use]
|
||||
extern crate quick_error;
|
||||
#[cfg(unix)]
|
||||
extern crate unix_socket;
|
||||
#[macro_use]
|
||||
|
@ -18,9 +17,9 @@ extern crate uucore;
|
|||
|
||||
// last synced with: cat (GNU coreutils) 8.13
|
||||
use clap::{App, Arg};
|
||||
use quick_error::ResultExt;
|
||||
use std::fs::{metadata, File};
|
||||
use std::io::{self, stderr, stdin, stdout, BufWriter, Read, Write};
|
||||
use std::io::{self, Read, Write};
|
||||
use thiserror::Error;
|
||||
use uucore::fs::is_stdin_interactive;
|
||||
|
||||
/// Unix domain socket support
|
||||
|
@ -31,12 +30,41 @@ use std::os::unix::fs::FileTypeExt;
|
|||
#[cfg(unix)]
|
||||
use unix_socket::UnixStream;
|
||||
|
||||
/// Linux splice support
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::fcntl::{splice, SpliceFFlags};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::unistd::pipe;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
static NAME: &str = "cat";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static SYNTAX: &str = "[OPTION]... [FILE]...";
|
||||
static SUMMARY: &str = "Concatenate FILE(s), or standard input, to standard output
|
||||
With no FILE, or when FILE is -, read standard input.";
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum CatError {
|
||||
/// Wrapper around `io::Error`
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// Wrapper around `nix::Error`
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[error("{0}")]
|
||||
Nix(#[from] nix::Error),
|
||||
/// Unknown file type; it's not a regular file, socket, etc.
|
||||
#[error("unknown filetype: {}", ft_debug)]
|
||||
UnknownFiletype {
|
||||
/// A debug print of the file type
|
||||
ft_debug: String,
|
||||
},
|
||||
#[error("Is a directory")]
|
||||
IsDirectory,
|
||||
}
|
||||
|
||||
type CatResult<T> = Result<T, CatError>;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum NumberingMode {
|
||||
None,
|
||||
|
@ -44,39 +72,6 @@ enum NumberingMode {
|
|||
All,
|
||||
}
|
||||
|
||||
quick_error! {
|
||||
#[derive(Debug)]
|
||||
enum CatError {
|
||||
/// Wrapper for io::Error with path context
|
||||
Input(err: io::Error, path: String) {
|
||||
display("cat: {0}: {1}", path, err)
|
||||
context(path: &'a str, err: io::Error) -> (err, path.to_owned())
|
||||
cause(err)
|
||||
}
|
||||
|
||||
/// Wrapper for io::Error with no context
|
||||
Output(err: io::Error) {
|
||||
display("cat: {0}", err) from()
|
||||
cause(err)
|
||||
}
|
||||
|
||||
/// Unknown Filetype classification
|
||||
UnknownFiletype(path: String) {
|
||||
display("cat: {0}: unknown filetype", path)
|
||||
}
|
||||
|
||||
/// At least one error was encountered in reading or writing
|
||||
EncounteredErrors(count: usize) {
|
||||
display("cat: encountered {0} errors", count)
|
||||
}
|
||||
|
||||
/// Denotes an error caused by trying to `cat` a directory
|
||||
IsDirectory(path: String) {
|
||||
display("cat: {0}: Is a directory", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OutputOptions {
|
||||
/// Line numbering mode
|
||||
number: NumberingMode,
|
||||
|
@ -87,21 +82,56 @@ struct OutputOptions {
|
|||
/// display TAB characters as `tab`
|
||||
show_tabs: bool,
|
||||
|
||||
/// If `show_tabs == true`, this string will be printed in the
|
||||
/// place of tabs
|
||||
tab: String,
|
||||
|
||||
/// Can be set to show characters other than '\n' a the end of
|
||||
/// each line, e.g. $
|
||||
end_of_line: String,
|
||||
/// Show end of lines
|
||||
show_ends: bool,
|
||||
|
||||
/// use ^ and M- notation, except for LF (\\n) and TAB (\\t)
|
||||
show_nonprint: bool,
|
||||
}
|
||||
|
||||
impl OutputOptions {
|
||||
fn tab(&self) -> &'static str {
|
||||
if self.show_tabs {
|
||||
"^I"
|
||||
} else {
|
||||
"\t"
|
||||
}
|
||||
}
|
||||
|
||||
fn end_of_line(&self) -> &'static str {
|
||||
if self.show_ends {
|
||||
"$\n"
|
||||
} else {
|
||||
"\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// We can write fast if we can simply copy the contents of the file to
|
||||
/// stdout, without augmenting the output with e.g. line numbers.
|
||||
fn can_write_fast(&self) -> bool {
|
||||
!(self.show_tabs
|
||||
|| self.show_nonprint
|
||||
|| self.show_ends
|
||||
|| self.squeeze_blank
|
||||
|| self.number != NumberingMode::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// State that persists between output of each file. This struct is only used
|
||||
/// when we can't write fast.
|
||||
struct OutputState {
|
||||
/// The current line number
|
||||
line_number: usize,
|
||||
|
||||
/// Whether the output cursor is at the beginning of a new line
|
||||
at_line_start: bool,
|
||||
}
|
||||
|
||||
/// Represents an open file handle, stream, or other device
|
||||
struct InputHandle {
|
||||
reader: Box<dyn Read>,
|
||||
struct InputHandle<R: Read> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
file_descriptor: RawFd,
|
||||
reader: R,
|
||||
is_interactive: bool,
|
||||
}
|
||||
|
||||
|
@ -124,8 +154,6 @@ enum InputType {
|
|||
Socket,
|
||||
}
|
||||
|
||||
type CatResult<T> = Result<T, CatError>;
|
||||
|
||||
mod options {
|
||||
pub static FILE: &str = "file";
|
||||
pub static SHOW_ALL: &str = "show-all";
|
||||
|
@ -243,30 +271,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
None => vec!["-".to_owned()],
|
||||
};
|
||||
|
||||
let can_write_fast = !(show_tabs
|
||||
|| show_nonprint
|
||||
|| show_ends
|
||||
|| squeeze_blank
|
||||
|| number_mode != NumberingMode::None);
|
||||
|
||||
let success = if can_write_fast {
|
||||
write_fast(files).is_ok()
|
||||
} else {
|
||||
let tab = if show_tabs { "^I" } else { "\t" }.to_owned();
|
||||
|
||||
let end_of_line = if show_ends { "$\n" } else { "\n" }.to_owned();
|
||||
|
||||
let options = OutputOptions {
|
||||
end_of_line,
|
||||
number: number_mode,
|
||||
show_nonprint,
|
||||
show_tabs,
|
||||
squeeze_blank,
|
||||
tab,
|
||||
};
|
||||
|
||||
write_lines(files, &options).is_ok()
|
||||
let options = OutputOptions {
|
||||
show_ends,
|
||||
number: number_mode,
|
||||
show_nonprint,
|
||||
show_tabs,
|
||||
squeeze_blank,
|
||||
};
|
||||
let success = cat_files(files, &options).is_ok();
|
||||
|
||||
if success {
|
||||
0
|
||||
|
@ -275,6 +287,76 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
}
|
||||
}
|
||||
|
||||
fn cat_handle<R: Read>(
|
||||
handle: &mut InputHandle<R>,
|
||||
options: &OutputOptions,
|
||||
state: &mut OutputState,
|
||||
) -> CatResult<()> {
|
||||
if options.can_write_fast() {
|
||||
write_fast(handle)
|
||||
} else {
|
||||
write_lines(handle, &options, state)
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> {
|
||||
if path == "-" {
|
||||
let stdin = io::stdin();
|
||||
let mut handle = InputHandle {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
file_descriptor: stdin.as_raw_fd(),
|
||||
reader: stdin,
|
||||
is_interactive: is_stdin_interactive(),
|
||||
};
|
||||
return cat_handle(&mut handle, &options, state);
|
||||
}
|
||||
match get_input_type(path)? {
|
||||
InputType::Directory => Err(CatError::IsDirectory),
|
||||
#[cfg(unix)]
|
||||
InputType::Socket => {
|
||||
let socket = UnixStream::connect(path)?;
|
||||
socket.shutdown(Shutdown::Write)?;
|
||||
let mut handle = InputHandle {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
file_descriptor: socket.as_raw_fd(),
|
||||
reader: socket,
|
||||
is_interactive: false,
|
||||
};
|
||||
cat_handle(&mut handle, &options, state)
|
||||
}
|
||||
_ => {
|
||||
let file = File::open(path)?;
|
||||
let mut handle = InputHandle {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
file_descriptor: file.as_raw_fd(),
|
||||
reader: file,
|
||||
is_interactive: false,
|
||||
};
|
||||
cat_handle(&mut handle, &options, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_files(files: Vec<String>, options: &OutputOptions) -> Result<(), u32> {
|
||||
let mut error_count = 0;
|
||||
let mut state = OutputState {
|
||||
line_number: 1,
|
||||
at_line_start: true,
|
||||
};
|
||||
|
||||
for path in &files {
|
||||
if let Err(err) = cat_path(path, &options, &mut state) {
|
||||
show_info!("{}: {}", path, err);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
if error_count == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies the `InputType` of file at `path` if possible
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -285,7 +367,8 @@ fn get_input_type(path: &str) -> CatResult<InputType> {
|
|||
return Ok(InputType::StdIn);
|
||||
}
|
||||
|
||||
match metadata(path).context(path)?.file_type() {
|
||||
let ft = metadata(path)?.file_type();
|
||||
match ft {
|
||||
#[cfg(unix)]
|
||||
ft if ft.is_block_device() => Ok(InputType::BlockDevice),
|
||||
#[cfg(unix)]
|
||||
|
@ -297,125 +380,116 @@ fn get_input_type(path: &str) -> CatResult<InputType> {
|
|||
ft if ft.is_dir() => Ok(InputType::Directory),
|
||||
ft if ft.is_file() => Ok(InputType::File),
|
||||
ft if ft.is_symlink() => Ok(InputType::SymLink),
|
||||
_ => Err(CatError::UnknownFiletype(path.to_owned())),
|
||||
_ => Err(CatError::UnknownFiletype {
|
||||
ft_debug: format!("{:?}", ft),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an InputHandle from which a Reader can be accessed or an
|
||||
/// error
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - `InputHandler` will wrap a reader from this file path
|
||||
fn open(path: &str) -> CatResult<InputHandle> {
|
||||
if path == "-" {
|
||||
let stdin = stdin();
|
||||
return Ok(InputHandle {
|
||||
reader: Box::new(stdin) as Box<dyn Read>,
|
||||
is_interactive: is_stdin_interactive(),
|
||||
});
|
||||
}
|
||||
|
||||
match get_input_type(path)? {
|
||||
InputType::Directory => Err(CatError::IsDirectory(path.to_owned())),
|
||||
#[cfg(unix)]
|
||||
InputType::Socket => {
|
||||
let socket = UnixStream::connect(path).context(path)?;
|
||||
socket.shutdown(Shutdown::Write).context(path)?;
|
||||
Ok(InputHandle {
|
||||
reader: Box::new(socket) as Box<dyn Read>,
|
||||
is_interactive: false,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let file = File::open(path).context(path)?;
|
||||
Ok(InputHandle {
|
||||
reader: Box::new(file) as Box<dyn Read>,
|
||||
is_interactive: false,
|
||||
})
|
||||
/// Writes handle to stdout with no configuration. This allows a
|
||||
/// simple memory copy.
|
||||
fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
|
||||
let stdout = io::stdout();
|
||||
let mut stdout_lock = stdout.lock();
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
// If we're on Linux or Android, try to use the splice() system call
|
||||
// for faster writing. If it works, we're done.
|
||||
if !write_fast_using_splice(handle, stdout_lock.as_raw_fd())? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// If we're not on Linux or Android, or the splice() call failed,
|
||||
// fall back on slower writing.
|
||||
let mut buf = [0; 1024 * 64];
|
||||
while let Ok(n) = handle.reader.read(&mut buf) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
stdout_lock.write_all(&buf[..n])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes files to stdout with no configuration. This allows a
|
||||
/// simple memory copy. Returns `Ok(())` if no errors were
|
||||
/// encountered, or an error with the number of errors encountered.
|
||||
/// This function is called from `write_fast()` on Linux and Android. The
|
||||
/// function `splice()` is used to move data between two file descriptors
|
||||
/// without copying between kernel- and userspace. This results in a large
|
||||
/// speedup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `files` - There is no short circuit when encountering an error
|
||||
/// reading a file in this vector
|
||||
fn write_fast(files: Vec<String>) -> CatResult<()> {
|
||||
let mut writer = stdout();
|
||||
let mut in_buf = [0; 1024 * 64];
|
||||
let mut error_count = 0;
|
||||
/// The `bool` in the result value indicates if we need to fall back to normal
|
||||
/// copying or not. False means we don't have to.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn write_fast_using_splice<R: Read>(handle: &mut InputHandle<R>, writer: RawFd) -> CatResult<bool> {
|
||||
const BUF_SIZE: usize = 1024 * 16;
|
||||
|
||||
for file in files {
|
||||
match open(&file[..]) {
|
||||
Ok(mut handle) => {
|
||||
while let Ok(n) = handle.reader.read(&mut in_buf) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&in_buf[..n]).context(&file[..])?;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
writeln!(&mut stderr(), "{}", error)?;
|
||||
error_count += 1;
|
||||
let (pipe_rd, pipe_wr) = pipe()?;
|
||||
|
||||
// We only fall back if splice fails on the first call.
|
||||
match splice(
|
||||
handle.file_descriptor,
|
||||
None,
|
||||
pipe_wr,
|
||||
None,
|
||||
BUF_SIZE,
|
||||
SpliceFFlags::empty(),
|
||||
) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
splice_exact(pipe_rd, writer, n)?;
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
match error_count {
|
||||
0 => Ok(()),
|
||||
_ => Err(CatError::EncounteredErrors(error_count)),
|
||||
loop {
|
||||
let n = splice(
|
||||
handle.file_descriptor,
|
||||
None,
|
||||
pipe_wr,
|
||||
None,
|
||||
BUF_SIZE,
|
||||
SpliceFFlags::empty(),
|
||||
)?;
|
||||
if n == 0 {
|
||||
// We read 0 bytes from the input,
|
||||
// which means we're done copying.
|
||||
break;
|
||||
}
|
||||
splice_exact(pipe_rd, writer, n)?;
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// State that persists between output of each file
|
||||
struct OutputState {
|
||||
/// The current line number
|
||||
line_number: usize,
|
||||
|
||||
/// Whether the output cursor is at the beginning of a new line
|
||||
at_line_start: bool,
|
||||
}
|
||||
|
||||
/// Writes files to stdout with `options` as configuration. Returns
|
||||
/// `Ok(())` if no errors were encountered, or an error with the
|
||||
/// number of errors encountered.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `files` - There is no short circuit when encountering an error
|
||||
/// reading a file in this vector
|
||||
fn write_lines(files: Vec<String>, options: &OutputOptions) -> CatResult<()> {
|
||||
let mut error_count = 0;
|
||||
let mut state = OutputState {
|
||||
line_number: 1,
|
||||
at_line_start: true,
|
||||
};
|
||||
|
||||
for file in files {
|
||||
if let Err(error) = write_file_lines(&file, options, &mut state) {
|
||||
writeln!(&mut stderr(), "{}", error).context(&file[..])?;
|
||||
error_count += 1;
|
||||
/// Splice wrapper which handles short writes
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> {
|
||||
let mut left = num_bytes;
|
||||
loop {
|
||||
let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?;
|
||||
left -= written;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match error_count {
|
||||
0 => Ok(()),
|
||||
_ => Err(CatError::EncounteredErrors(error_count)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Outputs file contents to stdout in a line-by-line fashion,
|
||||
/// propagating any errors that might occur.
|
||||
fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> {
|
||||
let mut handle = open(file)?;
|
||||
fn write_lines<R: Read>(
|
||||
handle: &mut InputHandle<R>,
|
||||
options: &OutputOptions,
|
||||
state: &mut OutputState,
|
||||
) -> CatResult<()> {
|
||||
let mut in_buf = [0; 1024 * 31];
|
||||
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
|
||||
let stdout = io::stdout();
|
||||
let mut writer = stdout.lock();
|
||||
let mut one_blank_kept = false;
|
||||
|
||||
while let Ok(n) = handle.reader.read(&mut in_buf) {
|
||||
|
@ -433,9 +507,9 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState
|
|||
write!(&mut writer, "{0:6}\t", state.line_number)?;
|
||||
state.line_number += 1;
|
||||
}
|
||||
writer.write_all(options.end_of_line.as_bytes())?;
|
||||
writer.write_all(options.end_of_line().as_bytes())?;
|
||||
if handle.is_interactive {
|
||||
writer.flush().context(file)?;
|
||||
writer.flush()?;
|
||||
}
|
||||
}
|
||||
state.at_line_start = true;
|
||||
|
@ -450,7 +524,7 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState
|
|||
|
||||
// print to end of line or end of buffer
|
||||
let offset = if options.show_nonprint {
|
||||
write_nonprint_to_end(&in_buf[pos..], &mut writer, options.tab.as_bytes())
|
||||
write_nonprint_to_end(&in_buf[pos..], &mut writer, options.tab().as_bytes())
|
||||
} else if options.show_tabs {
|
||||
write_tab_to_end(&in_buf[pos..], &mut writer)
|
||||
} else {
|
||||
|
@ -462,7 +536,7 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState
|
|||
break;
|
||||
}
|
||||
// print suitable end of line
|
||||
writer.write_all(options.end_of_line.as_bytes())?;
|
||||
writer.write_all(options.end_of_line().as_bytes())?;
|
||||
if handle.is_interactive {
|
||||
writer.flush()?;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,21 @@ use nix::unistd::pipe;
|
|||
|
||||
const BUF_SIZE: usize = 16384;
|
||||
|
||||
/// Splice wrapper which handles short writes
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> {
|
||||
let mut left = num_bytes;
|
||||
loop {
|
||||
let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?;
|
||||
left -= written;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is a Linux-specific function to count the number of bytes using the
|
||||
/// `splice` system call, which is faster than using `read`.
|
||||
#[inline]
|
||||
|
@ -39,7 +54,7 @@ fn count_bytes_using_splice(fd: RawFd) -> nix::Result<usize> {
|
|||
break;
|
||||
}
|
||||
byte_count += res;
|
||||
splice(pipe_rd, None, null, None, res, SpliceFFlags::empty())?;
|
||||
splice_exact(pipe_rd, null, res)?;
|
||||
}
|
||||
|
||||
Ok(byte_count)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#[cfg(unix)]
|
||||
extern crate unix_socket;
|
||||
|
||||
use crate::common::util::*;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn test_output_simple() {
|
||||
|
@ -11,6 +9,129 @@ fn test_output_simple() {
|
|||
.stdout_only("abcde\nfghij\nklmno\npqrst\nuvwxyz\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_options() {
|
||||
for fixture in &["empty.txt", "alpha.txt", "nonewline.txt"] {
|
||||
// Give fixture through command line file argument
|
||||
new_ucmd!()
|
||||
.args(&[fixture])
|
||||
.succeeds()
|
||||
.stdout_is_fixture(fixture);
|
||||
// Give fixture through stdin
|
||||
new_ucmd!()
|
||||
.pipe_in_fixture(fixture)
|
||||
.succeeds()
|
||||
.stdout_is_fixture(fixture);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_options_big_input() {
|
||||
for &n in &[
|
||||
0,
|
||||
1,
|
||||
42,
|
||||
16 * 1024 - 7,
|
||||
16 * 1024 - 1,
|
||||
16 * 1024,
|
||||
16 * 1024 + 1,
|
||||
16 * 1024 + 3,
|
||||
32 * 1024,
|
||||
64 * 1024,
|
||||
80 * 1024,
|
||||
96 * 1024,
|
||||
112 * 1024,
|
||||
128 * 1024,
|
||||
] {
|
||||
let data = vec_of_size(n);
|
||||
let data2 = data.clone();
|
||||
assert_eq!(data.len(), data2.len());
|
||||
new_ucmd!().pipe_in(data).succeeds().stdout_is_bytes(&data2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_fifo_symlink() {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::thread;
|
||||
|
||||
let s = TestScenario::new(util_name!());
|
||||
s.fixtures.mkdir("dir");
|
||||
s.fixtures.mkfifo("dir/pipe");
|
||||
assert!(s.fixtures.is_fifo("dir/pipe"));
|
||||
|
||||
// Make cat read the pipe through a symlink
|
||||
s.fixtures.symlink_file("dir/pipe", "sympipe");
|
||||
let proc = s.ucmd().args(&["sympipe"]).run_no_wait();
|
||||
|
||||
let data = vec_of_size(128 * 1024);
|
||||
let data2 = data.clone();
|
||||
|
||||
let pipe_path = s.fixtures.plus("dir/pipe");
|
||||
let thread = thread::spawn(move || {
|
||||
let mut pipe = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(false)
|
||||
.open(pipe_path)
|
||||
.unwrap();
|
||||
pipe.write_all(&data).unwrap();
|
||||
});
|
||||
|
||||
let output = proc.wait_with_output().unwrap();
|
||||
assert_eq!(&output.stdout, &data2);
|
||||
thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_directory() {
|
||||
let s = TestScenario::new(util_name!());
|
||||
s.fixtures.mkdir("test_directory");
|
||||
s.ucmd()
|
||||
.args(&["test_directory"])
|
||||
.fails()
|
||||
.stderr_is("cat: test_directory: Is a directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_directory_and_file() {
|
||||
let s = TestScenario::new(util_name!());
|
||||
s.fixtures.mkdir("test_directory2");
|
||||
for fixture in &["empty.txt", "alpha.txt", "nonewline.txt"] {
|
||||
s.ucmd()
|
||||
.args(&["test_directory2", fixture])
|
||||
.fails()
|
||||
.stderr_is("cat: test_directory2: Is a directory")
|
||||
.stdout_is_fixture(fixture);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_directories_and_file_and_stdin() {
|
||||
let s = TestScenario::new(util_name!());
|
||||
s.fixtures.mkdir("test_directory3");
|
||||
s.fixtures.mkdir("test_directory3/test_directory4");
|
||||
s.fixtures.mkdir("test_directory3/test_directory5");
|
||||
s.ucmd()
|
||||
.args(&[
|
||||
"test_directory3/test_directory4",
|
||||
"alpha.txt",
|
||||
"-",
|
||||
"filewhichdoesnotexist.txt",
|
||||
"nonewline.txt",
|
||||
"test_directory3/test_directory5",
|
||||
"test_directory3/../test_directory3/test_directory5",
|
||||
"test_directory3",
|
||||
])
|
||||
.pipe_in("stdout bytes")
|
||||
.fails()
|
||||
.stderr_is_fixture("three_directories_and_file_and_stdin.stderr.expected")
|
||||
.stdout_is(
|
||||
"abcde\nfghij\nklmno\npqrst\nuvwxyz\nstdout bytestext without a trailing newline",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_multi_files_print_all_chars() {
|
||||
new_ucmd!()
|
||||
|
@ -149,13 +270,64 @@ fn test_squeeze_blank_before_numbering() {
|
|||
}
|
||||
}
|
||||
|
||||
/// This tests reading from Unix character devices
|
||||
#[test]
|
||||
#[cfg(foo)]
|
||||
#[cfg(unix)]
|
||||
fn test_dev_random() {
|
||||
let mut buf = [0; 2048];
|
||||
let mut proc = new_ucmd!().args(&["/dev/random"]).run_no_wait();
|
||||
let mut proc_stdout = proc.stdout.take().unwrap();
|
||||
proc_stdout.read_exact(&mut buf).unwrap();
|
||||
|
||||
let num_zeroes = buf.iter().fold(0, |mut acc, &n| {
|
||||
if n == 0 {
|
||||
acc += 1;
|
||||
}
|
||||
acc
|
||||
});
|
||||
// The probability of more than 512 zero bytes is essentially zero if the
|
||||
// output is truly random.
|
||||
assert!(num_zeroes < 512);
|
||||
proc.kill().unwrap();
|
||||
}
|
||||
|
||||
/// Reading from /dev/full should return an infinite amount of zero bytes.
|
||||
/// Wikipedia says there is support on Linux, FreeBSD, and NetBSD.
|
||||
#[test]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
|
||||
fn test_dev_full() {
|
||||
let mut buf = [0; 2048];
|
||||
let mut proc = new_ucmd!().args(&["/dev/full"]).run_no_wait();
|
||||
let mut proc_stdout = proc.stdout.take().unwrap();
|
||||
let expected = [0; 2048];
|
||||
proc_stdout.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..], &expected[..]);
|
||||
proc.kill().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
|
||||
fn test_dev_full_show_all() {
|
||||
let mut buf = [0; 2048];
|
||||
let mut proc = new_ucmd!().args(&["-A", "/dev/full"]).run_no_wait();
|
||||
let mut proc_stdout = proc.stdout.take().unwrap();
|
||||
proc_stdout.read_exact(&mut buf).unwrap();
|
||||
|
||||
let expected: Vec<u8> = (0..buf.len())
|
||||
.map(|n| if n & 1 == 0 { b'^' } else { b'@' })
|
||||
.collect();
|
||||
|
||||
assert_eq!(&buf[..], &expected[..]);
|
||||
proc.kill().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_domain_socket() {
|
||||
use self::tempdir::TempDir;
|
||||
use self::unix_socket::UnixListener;
|
||||
use std::io::prelude::*;
|
||||
use std::thread;
|
||||
use tempdir::TempDir;
|
||||
use unix_socket::UnixListener;
|
||||
|
||||
let dir = TempDir::new("unix_socket").expect("failed to create dir");
|
||||
let socket_path = dir.path().join("sock");
|
||||
|
|
|
@ -1,5 +1,33 @@
|
|||
use crate::common::util::*;
|
||||
|
||||
#[test]
|
||||
fn test_count_bytes_large_stdin() {
|
||||
for &n in &[
|
||||
0,
|
||||
1,
|
||||
42,
|
||||
16 * 1024 - 7,
|
||||
16 * 1024 - 1,
|
||||
16 * 1024,
|
||||
16 * 1024 + 1,
|
||||
16 * 1024 + 3,
|
||||
32 * 1024,
|
||||
64 * 1024,
|
||||
80 * 1024,
|
||||
96 * 1024,
|
||||
112 * 1024,
|
||||
128 * 1024,
|
||||
] {
|
||||
let data = vec_of_size(n);
|
||||
let expected = format!("{}\n", n);
|
||||
new_ucmd!()
|
||||
.args(&["-c"])
|
||||
.pipe_in(data)
|
||||
.succeeds()
|
||||
.stdout_is_bytes(&expected.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdin_default() {
|
||||
new_ucmd!()
|
||||
|
|
|
@ -222,6 +222,12 @@ impl CmdResult {
|
|||
self
|
||||
}
|
||||
|
||||
/// Like stdout_is_fixture, but for stderr
|
||||
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult {
|
||||
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
||||
self.stderr_is_bytes(contents)
|
||||
}
|
||||
|
||||
/// asserts that
|
||||
/// 1. the command resulted in stdout stream output that equals the
|
||||
/// passed in value
|
||||
|
@ -804,3 +810,12 @@ pub fn read_size(child: &mut Child, size: usize) -> String {
|
|||
.unwrap();
|
||||
String::from_utf8(output).unwrap()
|
||||
}
|
||||
|
||||
pub fn vec_of_size(n: usize) -> Vec<u8> {
|
||||
let mut result = Vec::new();
|
||||
for _ in 0..n {
|
||||
result.push('a' as u8);
|
||||
}
|
||||
assert_eq!(result.len(), n);
|
||||
result
|
||||
}
|
||||
|
|
0
tests/fixtures/cat/empty.txt
vendored
Normal file
0
tests/fixtures/cat/empty.txt
vendored
Normal file
5
tests/fixtures/cat/three_directories_and_file_and_stdin.stderr.expected
vendored
Normal file
5
tests/fixtures/cat/three_directories_and_file_and_stdin.stderr.expected
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
cat: test_directory3/test_directory4: Is a directory
|
||||
cat: filewhichdoesnotexist.txt: No such file or directory (os error 2)
|
||||
cat: test_directory3/test_directory5: Is a directory
|
||||
cat: test_directory3/../test_directory3/test_directory5: Is a directory
|
||||
cat: test_directory3: Is a directory
|
Loading…
Reference in a new issue