diff --git a/src/uu/cat/locales/en-US.ftl b/src/uu/cat/locales/en-US.ftl index bf81d6d7ffb..16cda4f2e5a 100644 --- a/src/uu/cat/locales/en-US.ftl +++ b/src/uu/cat/locales/en-US.ftl @@ -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 diff --git a/src/uu/cat/locales/fr-FR.ftl b/src/uu/cat/locales/fr-FR.ftl index 2316544ce03..fd348e0d89a 100644 --- a/src/uu/cat/locales/fr-FR.ftl +++ b/src/uu/cat/locales/fr-FR.ftl @@ -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 diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 3497429d2de..9ddd7ed0567 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -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; @@ -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}")] @@ -109,6 +112,11 @@ enum CatError { type CatResult = Result; +#[inline] +fn map_write_err(res: io::Result) -> CatResult { + res.map_err(CatError::WriteError) +} + #[derive(PartialEq)] enum NumberingMode { None, @@ -416,15 +424,30 @@ fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> { one_blank_kept: false, }; let mut error_messages: Vec = Vec::new(); + let mut write_error: Option = 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 { @@ -505,9 +528,11 @@ fn write_fast(handle: &mut InputHandle) -> 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()), @@ -519,7 +544,7 @@ fn write_fast(handle: &mut InputHandle) -> 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(()) } @@ -554,13 +579,13 @@ fn write_lines( 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(); } @@ -593,7 +618,7 @@ fn write_lines( // 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(()) @@ -608,9 +633,9 @@ fn write_new_line( ) -> 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; @@ -620,7 +645,7 @@ fn write_new_line( 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)?; @@ -628,11 +653,7 @@ fn write_new_line( Ok(()) } -fn write_end( - writer: &mut W, - in_buf: &[u8], - options: &OutputOptions, -) -> io::Result { +fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -> CatResult { if options.show_nonprint { write_nonprint_to_end(in_buf, writer, options.tab().as_bytes()) } else if options.show_tabs { @@ -648,21 +669,21 @@ fn write_end( // 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(in_buf: &[u8], writer: &mut W) -> io::Result { +fn write_to_end(in_buf: &[u8], writer: &mut W) -> CatResult { // using memchr2 significantly improves performances match memchr2(b'\n', b'\r', in_buf) { Some(p) => { - writer.write_all(&in_buf[..p])?; + map_write_err(writer.write_all(&in_buf[..p]))?; Ok(p) } None => { - writer.write_all(in_buf)?; + map_write_err(writer.write_all(in_buf))?; Ok(in_buf.len()) } } } -fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> io::Result { +fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> CatResult { let mut count = 0; loop { match in_buf @@ -670,9 +691,9 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> io::Result { - writer.write_all(&in_buf[..p])?; + map_write_err(writer.write_all(&in_buf[..p]))?; if in_buf[p] == b'\t' { - writer.write_all(b"^I")?; + map_write_err(writer.write_all(b"^I"))?; in_buf = &in_buf[p + 1..]; count += p + 1; } else { @@ -681,14 +702,14 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> io::Result { - writer.write_all(in_buf)?; + map_write_err(writer.write_all(in_buf))?; return Ok(in_buf.len() + count); } } } } -fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> io::Result { +fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> CatResult { let mut count = 0; for byte in in_buf.iter().copied() { @@ -696,14 +717,14 @@ fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> 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-^?"))?, + } count += 1; } Ok(count) @@ -714,9 +735,9 @@ fn write_end_of_line( 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(()) } diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index ca5265d2bf8..cca0f09221e 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -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}; @@ -38,7 +39,11 @@ pub(super) fn write_fast_using_splice( // 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); } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 33796f3ae41..b4d0736408c 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -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; @@ -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!());