diff --git a/Cargo.lock b/Cargo.lock index 78281c3239f..c06edeaf66d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4262,6 +4262,7 @@ dependencies = [ "clap", "fluent", "nix", + "thiserror 2.0.18", "uucore", ] diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index e1fb8c5bac3..b36d1417ba5 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -20,6 +20,7 @@ doctest = false [dependencies] clap = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["parser"] } fluent = { workspace = true } diff --git a/src/uu/stty/locales/en-US.ftl b/src/uu/stty/locales/en-US.ftl index 0fb55f2e28a..7e35551609c 100644 --- a/src/uu/stty/locales/en-US.ftl +++ b/src/uu/stty/locales/en-US.ftl @@ -16,6 +16,7 @@ stty-error-invalid-speed = invalid {$arg} '{$speed}' stty-error-invalid-argument = invalid argument '{$arg}' stty-error-invalid-integer-argument = invalid integer argument: {$value} stty-error-invalid-integer-argument-value-too-large = invalid integer argument: {$value}: Value too large for defined data type +stty-error-write-error = write error # Output format strings stty-output-speed = speed {$speed} baud; diff --git a/src/uu/stty/locales/fr-FR.ftl b/src/uu/stty/locales/fr-FR.ftl index 5fd9f410a9c..f190ec7ea79 100644 --- a/src/uu/stty/locales/fr-FR.ftl +++ b/src/uu/stty/locales/fr-FR.ftl @@ -16,6 +16,7 @@ stty-error-invalid-speed = {$arg} invalide '{$speed}' stty-error-invalid-argument = argument invalide '{$arg}' stty-error-invalid-integer-argument = argument entier invalide : {$value} stty-error-invalid-integer-argument-value-too-large = argument entier invalide : {$value} : Valeur trop grande pour le type de données défini +stty-error-write-error = erreur d'écriture # Chaînes de format de sortie stty-output-speed = vitesse {$speed} bauds ; diff --git a/src/uu/stty/src/main.rs b/src/uu/stty/src/main.rs index b2b8dae0ad1..8677bef7c10 100644 --- a/src/uu/stty/src/main.rs +++ b/src/uu/stty/src/main.rs @@ -3,4 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -uucore::bin!(uu_stty); +// All output paths in uu_stty handle stdout write errors themselves and end +// with a newline (draining the line buffer), so the defensive exit-time flush +// is not necessary and would report write errors a second time. +uucore::bin!(uu_stty, no_flush); diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index ea078435d51..b44b10b8279 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -35,12 +35,13 @@ use nix::sys::termios::{ use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; use std::fs::File; -use std::io::{self, Stdin, stdin, stdout}; +use std::io::{self, Stdin, Write, stdin, stdout}; use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; -use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; +use thiserror::Error; +use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError, strip_errno}; use uucore::format_usage; use uucore::parser::num_parser::ExtendedParser; use uucore::translate; @@ -550,17 +551,27 @@ fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } -fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> { +fn print_special_setting(setting: &PrintSetting, fd: i32) -> UResult<()> { match setting { PrintSetting::Size => { let mut size = TermSize::default(); unsafe { tiocgwinsz(fd, &raw mut size)? }; - println!("{} {}", size.rows, size.columns); + writeln!(stdout(), "{} {}", size.rows, size.columns).map_err(SttyError::Write)?; } } Ok(()) } +#[derive(Debug, Error)] +enum SttyError { + /// A write to stdout failed. Report instead of panicking the way + /// 'print!' family of macros does. + #[error("{}: {}", translate!("stty-error-write-error"), strip_errno(.0))] + Write(io::Error), +} + +impl UError for SttyError {} + /// Handles line wrapping for stty output to fit within terminal width struct WrappedPrinter { width: usize, @@ -591,29 +602,31 @@ impl WrappedPrinter { } } - fn print(&mut self, token: &str) { + fn print(&mut self, token: &str) -> UResult<()> { let token_len = self.prefix().chars().count() + token.chars().count(); if self.current > 0 && self.current + token_len > self.width { - println!(); + writeln!(stdout()).map_err(SttyError::Write)?; self.current = 0; self.first_in_line = true; } - print!("{}{token}", self.prefix()); + write!(stdout(), "{}{token}", self.prefix()).map_err(SttyError::Write)?; self.current += token_len; self.first_in_line = false; + Ok(()) } fn prefix(&self) -> &str { if self.first_in_line { "" } else { " " } } - fn flush(&mut self) { + fn flush(&mut self) -> UResult<()> { if self.current > 0 { - println!(); + writeln!(stdout()).map_err(SttyError::Write)?; self.current = 0; self.first_in_line = false; } + Ok(()) } } @@ -626,7 +639,7 @@ fn print_terminal_size( opts: &Options, window_size: Option<&TermSize>, term_size: Option<&TermSize>, -) -> nix::Result<()> { +) -> UResult<()> { // GNU linked against glibc 2.42 provides us baudrate 51 which panics cfgetospeed #[cfg(not(target_os = "linux"))] let speed = nix::sys::termios::cfgetospeed(termios); @@ -653,7 +666,7 @@ fn print_terminal_size( not(target_arch = "powerpc"), not(all(target_arch = "powerpc64", target_endian = "big")) ))] - printer.print(&translate!("stty-output-speed", "speed" => speed)); + printer.print(&translate!("stty-output-speed", "speed" => speed))?; // Big-endian Linux PowerPC uses BaudRate enum, need to convert to display format #[cfg(target_os = "linux")] @@ -668,7 +681,7 @@ fn print_terminal_size( .find(|(_, rate)| *rate == speed) .map(|(text, _)| *text) .unwrap_or("unknown"); - printer.print(&translate!("stty-output-speed", "speed" => speed_str)); + printer.print(&translate!("stty-output-speed", "speed" => speed_str))?; } // Other platforms need to use the baud rate enum, so printing the right value @@ -676,7 +689,7 @@ fn print_terminal_size( #[cfg(not(any(target_os = "linux", bsd)))] for (text, baud_rate) in BAUD_RATES { if *baud_rate == speed { - printer.print(&translate!("stty-output-speed", "speed" => (*text))); + printer.print(&translate!("stty-output-speed", "speed" => (*text)))?; break; } } @@ -685,7 +698,7 @@ fn print_terminal_size( let term_size = term_size.as_ref().expect("terminal size should be set"); printer.print( &translate!("stty-output-rows-columns", "rows" => term_size.rows, "columns" => term_size.columns), - ); + )?; } #[cfg(any(target_os = "linux", target_os = "redox"))] @@ -694,9 +707,9 @@ fn print_terminal_size( // so we get the underlying libc::termios struct to get that information. let libc_termios: nix::libc::termios = termios.clone().into(); let line = libc_termios.c_line; - printer.print(&translate!("stty-output-line", "line" => line)); + printer.print(&translate!("stty-output-line", "line" => line))?; } - printer.flush(); + printer.flush()?; Ok(()) } @@ -854,7 +867,7 @@ fn print_control_chars( termios: &Termios, opts: &Options, term_size: Option<&TermSize>, -) -> nix::Result<()> { +) -> UResult<()> { if !opts.all { // Print only control chars that differ from sane defaults let mut printer = WrappedPrinter::new(term_size); @@ -866,10 +879,10 @@ fn print_control_chars( printer.print(&format!( "{text} = {};", control_char_to_string(current_val)? - )); + ))?; } } - printer.flush(); + printer.flush()?; return Ok(()); } @@ -878,28 +891,31 @@ fn print_control_chars( printer.print(&format!( "{text} = {};", control_char_to_string(termios.control_chars[*cc_index as usize])? - )); + ))?; } printer.print(&translate!("stty-output-min-time", "min" => termios.control_chars[S::VMIN as usize], "time" => termios.control_chars[S::VTIME as usize] - )); - printer.flush(); + ))?; + printer.flush()?; Ok(()) } -fn print_in_save_format(termios: &Termios) { - print!( +fn print_in_save_format(termios: &Termios) -> UResult<()> { + write!( + stdout(), "{:x}:{:x}:{:x}:{:x}", termios.input_flags.bits(), termios.output_flags.bits(), termios.control_flags.bits(), termios.local_flags.bits() - ); + ) + .map_err(SttyError::Write)?; for cc in termios.control_chars { - print!(":{cc:x}"); + write!(stdout(), ":{cc:x}").map_err(SttyError::Write)?; } - println!(); + writeln!(stdout()).map_err(SttyError::Write)?; + Ok(()) } /// Gets terminal size using the tiocgwinsz ioctl system call. @@ -909,9 +925,9 @@ fn get_terminal_size(fd: RawFd) -> nix::Result { unsafe { tiocgwinsz(fd, &raw mut term_size) }.map(|_| term_size) } -fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { +fn print_settings(termios: &Termios, opts: &Options) -> UResult<()> { if opts.save { - print_in_save_format(termios); + print_in_save_format(termios)?; } else { let device_fd = opts.file.as_raw_fd(); let term_size = if opts.all { @@ -929,10 +945,10 @@ fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { print_terminal_size(termios, opts, window_size.as_ref(), term_size.as_ref())?; print_control_chars(termios, opts, window_size.as_ref())?; - print_flags(termios, opts, CONTROL_FLAGS, window_size.as_ref()); - print_flags(termios, opts, INPUT_FLAGS, window_size.as_ref()); - print_flags(termios, opts, OUTPUT_FLAGS, window_size.as_ref()); - print_flags(termios, opts, LOCAL_FLAGS, window_size.as_ref()); + print_flags(termios, opts, CONTROL_FLAGS, window_size.as_ref())?; + print_flags(termios, opts, INPUT_FLAGS, window_size.as_ref())?; + print_flags(termios, opts, OUTPUT_FLAGS, window_size.as_ref())?; + print_flags(termios, opts, LOCAL_FLAGS, window_size.as_ref())?; } Ok(()) } @@ -942,7 +958,7 @@ fn print_flags( opts: &Options, flags: &[Flag], term_size: Option<&TermSize>, -) { +) -> UResult<()> { let mut printer = WrappedPrinter::new(term_size); for &Flag { name, @@ -958,17 +974,17 @@ fn print_flags( let val = flag.is_in(termios, group); if group.is_some() { if val && (!sane || opts.all) { - printer.print(name); + printer.print(name)?; } } else if opts.all || val != sane { if !val { - printer.print(&format!("-{name}")); + printer.print(&format!("-{name}"))?; continue; } - printer.print(name); + printer.print(name)?; } } - printer.flush(); + printer.flush() } /// Apply a single setting diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index f8a2095a89d..db0a8459505 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1759,3 +1759,23 @@ fn test_columns_env_wrapping() { ); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_to_dev_full() { + // stty must report write errors like GNU instead of panicking. + for args in [&[][..], &["--all"][..], &["--save"][..], &["size"][..]] { + let (path, _controller, _replica) = pty_path(); + let dev_full = std::fs::OpenOptions::new() + .write(true) + .open("/dev/full") + .expect("/dev/full must exists on Linux"); + + new_ucmd!() + .args(&["--file", &path]) + .args(args) + .set_stdout(dev_full) + .fails_with_code(1) + .stderr_only("stty: write error: No space left on device\n"); + } +}