Skip to content
Open
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
1 change: 1 addition & 0 deletions src/uu/cat/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ cat-error-is-directory = Is a directory
cat-error-input-file-is-output-file = input file is output file
cat-error-too-many-symbolic-links = Too many levels of symbolic links
cat-error-no-such-device-or-address = No such device or address
cat-error-write-error = write error
1 change: 1 addition & 0 deletions src/uu/cat/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ cat-error-is-directory = Est un répertoire
cat-error-input-file-is-output-file = le fichier d'entrée est le fichier de sortie
cat-error-too-many-symbolic-links = Trop de niveaux de liens symboliques
cat-error-no-such-device-or-address = Aucun appareil ou adresse de ce type
cat-error-write-error = erreur d'écriture
112 changes: 68 additions & 44 deletions src/uu/cat/src/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::os::fd::AsFd;
use std::os::unix::fs::FileTypeExt;
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::error::{UIoError, UResult};
#[cfg(not(target_os = "windows"))]
use uucore::libc;
use uucore::translate;
Expand Down Expand Up @@ -86,6 +86,9 @@ enum CatError {
/// Wrapper around `io::Error`
#[error("{0}")]
Io(#[from] io::Error),
/// Wrapper around `io::Error` for stdout writes
#[error("{0}")]
WriteError(io::Error),
/// Wrapper around `nix::Error`
#[cfg(any(target_os = "linux", target_os = "android"))]
#[error("{0}")]
Expand All @@ -109,6 +112,11 @@ enum CatError {

type CatResult<T> = Result<T, CatError>;

#[inline]
fn map_write_err<T>(res: io::Result<T>) -> CatResult<T> {
res.map_err(CatError::WriteError)
}

#[derive(PartialEq)]
enum NumberingMode {
None,
Expand Down Expand Up @@ -416,15 +424,30 @@ fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> {
one_blank_kept: false,
};
let mut error_messages: Vec<String> = Vec::new();
let mut write_error: Option<io::Error> = None;

for path in files {
if let Err(err) = cat_path(path, options, &mut state) {
error_messages.push(format!("{}: {err}", path.maybe_quote()));
match cat_path(path, options, &mut state) {
Ok(()) => {}
Err(CatError::WriteError(err)) => {
write_error = Some(err);
break;
}
Err(err) => {
error_messages.push(format!("{}: {err}", path.maybe_quote()));
}
}
}
if state.skipped_carriage_return {
print!("\r");
}
if let Some(err) = write_error {
let message = UIoError::from(err);
error_messages.push(format!(
"{}: {message}",
translate!("cat-error-write-error")
));
}
if error_messages.is_empty() {
Ok(())
} else {
Expand Down Expand Up @@ -505,9 +528,11 @@ fn write_fast<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
if n == 0 {
break;
}
stdout_lock
.write_all(&buf[..n])
.inspect_err(handle_broken_pipe)?;
map_write_err(
stdout_lock
.write_all(&buf[..n])
.inspect_err(handle_broken_pipe),
)?;
}
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
Expand All @@ -519,7 +544,7 @@ fn write_fast<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
// that will succeed, data pushed through splice will be output before
// the data buffered in stdout.lock. Therefore additional explicit flush
// is required here.
stdout_lock.flush().inspect_err(handle_broken_pipe)?;
map_write_err(stdout_lock.flush().inspect_err(handle_broken_pipe))?;
Ok(())
}

Expand Down Expand Up @@ -554,18 +579,18 @@ fn write_lines<R: FdReadable>(
continue;
}
if state.skipped_carriage_return {
writer.write_all(b"\r")?;
map_write_err(writer.write_all(b"\r"))?;
state.skipped_carriage_return = false;
state.at_line_start = false;
}
state.one_blank_kept = false;
if state.at_line_start && options.number != NumberingMode::None {
state.line_number.write(&mut writer)?;
map_write_err(state.line_number.write(&mut writer))?;
state.line_number.increment();
}

// print to end of line or end of buffer
let offset = write_end(&mut writer, &in_buf[pos..], options);
let offset = write_end(&mut writer, &in_buf[pos..], options)?;

// end of buffer?
if offset + pos == in_buf.len() {
Expand Down Expand Up @@ -593,7 +618,7 @@ fn write_lines<R: FdReadable>(
// and not be buffered internally to the `cat` process.
// Hence it's necessary to flush our buffer before every time we could potentially block
// on a `std::io::Read::read` call.
writer.flush().inspect_err(handle_broken_pipe)?;
map_write_err(writer.flush().inspect_err(handle_broken_pipe))?;
}

Ok(())
Expand All @@ -608,9 +633,9 @@ fn write_new_line<W: Write>(
) -> CatResult<()> {
if state.skipped_carriage_return {
if options.show_ends {
writer.write_all(b"^M")?;
map_write_err(writer.write_all(b"^M"))?;
} else {
writer.write_all(b"\r")?;
map_write_err(writer.write_all(b"\r"))?;
}
state.skipped_carriage_return = false;

Expand All @@ -620,15 +645,15 @@ fn write_new_line<W: Write>(
if !state.at_line_start || !options.squeeze_blank || !state.one_blank_kept {
state.one_blank_kept = true;
if state.at_line_start && options.number == NumberingMode::All {
state.line_number.write(writer)?;
map_write_err(state.line_number.write(writer))?;
state.line_number.increment();
}
write_end_of_line(writer, options.end_of_line().as_bytes(), is_interactive)?;
}
Ok(())
}

fn write_end<W: Write>(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -> usize {
fn write_end<W: Write>(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -> CatResult<usize> {
if options.show_nonprint {
write_nonprint_to_end(in_buf, writer, options.tab().as_bytes())
} else if options.show_tabs {
Expand All @@ -644,76 +669,75 @@ fn write_end<W: Write>(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -
// however, write_nonprint_to_end doesn't need to stop at \r because it will always write \r as ^M.
// Return the number of written symbols

fn write_to_end<W: Write>(in_buf: &[u8], writer: &mut W) -> usize {
fn write_to_end<W: Write>(in_buf: &[u8], writer: &mut W) -> CatResult<usize> {
// using memchr2 significantly improves performances
match memchr2(b'\n', b'\r', in_buf) {
Some(p) => {
writer.write_all(&in_buf[..p]).unwrap();
p
map_write_err(writer.write_all(&in_buf[..p]))?;
Ok(p)
}
None => {
writer.write_all(in_buf).unwrap();
in_buf.len()
map_write_err(writer.write_all(in_buf))?;
Ok(in_buf.len())
}
}
}

fn write_tab_to_end<W: Write>(mut in_buf: &[u8], writer: &mut W) -> usize {
fn write_tab_to_end<W: Write>(mut in_buf: &[u8], writer: &mut W) -> CatResult<usize> {
let mut count = 0;
loop {
match in_buf
.iter()
.position(|c| *c == b'\n' || *c == b'\t' || *c == b'\r')
{
Some(p) => {
writer.write_all(&in_buf[..p]).unwrap();
map_write_err(writer.write_all(&in_buf[..p]))?;
if in_buf[p] == b'\t' {
writer.write_all(b"^I").unwrap();
map_write_err(writer.write_all(b"^I"))?;
in_buf = &in_buf[p + 1..];
count += p + 1;
} else {
// b'\n' or b'\r'
return count + p;
return Ok(count + p);
}
}
None => {
writer.write_all(in_buf).unwrap();
return in_buf.len() + count;
map_write_err(writer.write_all(in_buf))?;
return Ok(in_buf.len() + count);
}
}
}
}

fn write_nonprint_to_end<W: Write>(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> usize {
fn write_nonprint_to_end<W: Write>(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> CatResult<usize> {
let mut count = 0;

for byte in in_buf.iter().copied() {
if byte == b'\n' {
break;
}
match byte {
9 => writer.write_all(tab),
0..=8 | 10..=31 => writer.write_all(&[b'^', byte + 64]),
32..=126 => writer.write_all(&[byte]),
127 => writer.write_all(b"^?"),
128..=159 => writer.write_all(&[b'M', b'-', b'^', byte - 64]),
160..=254 => writer.write_all(&[b'M', b'-', byte - 128]),
_ => writer.write_all(b"M-^?"),
9 => map_write_err(writer.write_all(tab))?,
0..=8 | 10..=31 => map_write_err(writer.write_all(&[b'^', byte + 64]))?,
32..=126 => map_write_err(writer.write_all(&[byte]))?,
127 => map_write_err(writer.write_all(b"^?"))?,
128..=159 => map_write_err(writer.write_all(&[b'M', b'-', b'^', byte - 64]))?,
160..=254 => map_write_err(writer.write_all(&[b'M', b'-', byte - 128]))?,
_ => map_write_err(writer.write_all(b"M-^?"))?,
}
.unwrap();
count += 1;
}
count
Ok(count)
}

fn write_end_of_line<W: Write>(
writer: &mut W,
end_of_line: &[u8],
is_interactive: bool,
) -> CatResult<()> {
writer.write_all(end_of_line)?;
map_write_err(writer.write_all(end_of_line))?;
if is_interactive {
writer.flush().inspect_err(handle_broken_pipe)?;
map_write_err(writer.flush().inspect_err(handle_broken_pipe))?;
}
Ok(())
}
Expand All @@ -733,22 +757,22 @@ mod tests {
fn test_write_tab_to_end_with_newline() {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = b"a\tb\tc\n";
assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5);
assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5);
}

#[test]
fn test_write_tab_to_end_no_newline() {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = b"a\tb\tc";
assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5);
assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5);
}

#[test]
fn test_write_nonprint_to_end_new_line() {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = b"\n";
let tab = b"";
super::write_nonprint_to_end(in_buf, &mut writer, tab);
super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap();
assert_eq!(writer.buffer().len(), 0);
}

Expand All @@ -757,7 +781,7 @@ mod tests {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = &[9u8];
let tab = b"tab";
super::write_nonprint_to_end(in_buf, &mut writer, tab);
super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap();
assert_eq!(writer.buffer(), tab);
}

Expand All @@ -767,7 +791,7 @@ mod tests {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = &[byte];
let tab = b"";
super::write_nonprint_to_end(in_buf, &mut writer, tab);
super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap();
assert_eq!(writer.buffer(), [b'^', byte + 64]);
}
}
Expand All @@ -778,7 +802,7 @@ mod tests {
let mut writer = BufWriter::with_capacity(1024 * 64, stdout());
let in_buf = &[byte];
let tab = b"";
super::write_nonprint_to_end(in_buf, &mut writer, tab);
super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap();
assert_eq!(writer.buffer(), [b'^', byte + 64]);
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/uu/cat/src/splice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use super::{CatResult, FdReadable, InputHandle};
use super::{CatError, CatResult, FdReadable, InputHandle};

use nix::unistd;
use std::io;
use std::os::{fd::AsFd, unix::io::AsRawFd};

use uucore::pipes::{pipe, splice, splice_exact};
Expand Down Expand Up @@ -38,7 +39,11 @@ pub(super) fn write_fast_using_splice<R: FdReadable, S: AsRawFd + AsFd>(
// we can recover by copying the data that we have from the
// intermediate pipe to stdout using normal read/write. Then
// we tell the caller to fall back.
copy_exact(&pipe_rd, write_fd, n)?;
if let Err(err) = copy_exact(&pipe_rd, write_fd, n) {
return Err(CatError::WriteError(io::Error::from_raw_os_error(
err as i32,
)));
}
return Ok(true);
}
}
Expand Down
19 changes: 17 additions & 2 deletions tests/by-util/test_cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore NOFILE nonewline cmdline
// spell-checker:ignore NOFILE nonewline cmdline ENOSPC

#[cfg(any(target_os = "linux", target_os = "android"))]
use rlimit::Resource;
Expand Down Expand Up @@ -223,11 +223,26 @@ fn test_piped_to_dev_full() {
.pipe_in_fixture("alpha.txt")
.ignore_stdin_write_error()
.fails()
.stderr_contains("No space left on device");
.stderr_contains("write error: No space left on device");
}
}
}

#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
fn test_piped_to_dev_full_from_dev_stdin() {
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();

new_ucmd!()
.arg("/dev/stdin")
.set_stdout(dev_full)
.pipe_in("1")
.ignore_stdin_write_error()
.fails()
.stderr_contains("write error: No space left on device")
.stderr_does_not_contain("ENOSPC");
}

#[test]
fn test_directory() {
let s = TestScenario::new(util_name!());
Expand Down
Loading