diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 51f41de87..ca488ac86 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag ispeed ospeed +// spell-checker:ignore tcsadrain mod flags; @@ -11,13 +12,14 @@ use crate::flags::AllFlags; use clap::{Arg, ArgAction, ArgMatches, Command}; use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; use nix::sys::termios::{ - ControlFlags, InputFlags, LocalFlags, OutputFlags, SpecialCharacterIndices, Termios, + ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices, Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr, }; use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::collections::HashMap; use std::fs::File; use std::io::{self, Stdout, stdout}; +use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; @@ -112,6 +114,7 @@ enum ControlCharMappingError { enum SpecialSetting { Rows(u16), Cols(u16), + Line(u8), } enum PrintSetting { @@ -238,6 +241,7 @@ fn stty(opts: &Options) -> UResult<()> { )); } + let mut set_arg = SetArg::TCSADRAIN; let mut valid_args: Vec = Vec::new(); if let Some(args) = &opts.settings { @@ -299,7 +303,59 @@ fn stty(opts: &Options) -> UResult<()> { )); } } - // baud rate setting + } else if *arg == "line" { + match args_iter.next() { + Some(line) => match parse_u8_or_err(line) { + Ok(n) => valid_args.push(ArgOptions::Special(SpecialSetting::Line(n))), + Err(e) => return Err(USimpleError::new(1, e)), + }, + None => { + return Err(USimpleError::new( + 1, + get_message_with_args( + "stty-error-missing-argument", + HashMap::from([("arg".to_string(), arg.to_string())]), + ), + )); + } + } + } else if *arg == "min" { + match args_iter.next() { + Some(min) => match parse_u8_or_err(min) { + Ok(n) => { + valid_args + .push(ArgOptions::Mapping((SpecialCharacterIndices::VMIN, n))); + } + Err(e) => return Err(USimpleError::new(1, e)), + }, + None => { + return Err(USimpleError::new( + 1, + get_message_with_args( + "stty-error-missing-argument", + HashMap::from([("arg".to_string(), arg.to_string())]), + ), + )); + } + } + } else if *arg == "time" { + match args_iter.next() { + Some(time) => match parse_u8_or_err(time) { + Ok(n) => valid_args + .push(ArgOptions::Mapping((SpecialCharacterIndices::VTIME, n))), + Err(e) => return Err(USimpleError::new(1, e)), + }, + None => { + return Err(USimpleError::new( + 1, + get_message_with_args( + "stty-error-missing-argument", + HashMap::from([("arg".to_string(), arg.to_string())]), + ), + )); + } + } + // baud rate setting } else if let Some(baud_flag) = string_to_baud(arg) { valid_args.push(ArgOptions::Flags(baud_flag)); // non control char flag @@ -365,6 +421,10 @@ fn stty(opts: &Options) -> UResult<()> { ), )); } + } else if *arg == "drain" { + set_arg = SetArg::TCSADRAIN; + } else if *arg == "-drain" { + set_arg = SetArg::TCSANOW; } else if *arg == "size" { valid_args.push(ArgOptions::Print(PrintSetting::Size)); // not a valid option @@ -388,19 +448,15 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping), ArgOptions::Flags(flag) => apply_setting(&mut termios, flag), ArgOptions::Special(setting) => { - apply_special_setting(setting, opts.file.as_raw_fd())?; + apply_special_setting(&mut termios, setting, opts.file.as_raw_fd())?; } ArgOptions::Print(setting) => { print_special_setting(setting, opts.file.as_raw_fd())?; } } } - tcsetattr( - opts.file.as_fd(), - nix::sys::termios::SetArg::TCSANOW, - &termios, - ) - .expect("Could not write terminal attributes"); + tcsetattr(opts.file.as_fd(), set_arg, &termios) + .expect("Could not write terminal attributes"); } else { // TODO: Figure out the right error message for when tcgetattr fails let termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes"); @@ -409,6 +465,21 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +// GNU uses different error messages if values overflow or underflow a u8 +// this function returns the appropriate error message in the case of overflow or underflow, or u8 on success +fn parse_u8_or_err(arg: &str) -> Result { + arg.parse::().map_err(|e| match e.kind() { + IntErrorKind::PosOverflow => get_message_with_args( + "stty-error-invalid-integer-argument-value-too-large", + HashMap::from([("value".to_string(), format!("'{}'", arg))]), + ), + _ => get_message_with_args( + "stty-error-invalid-integer-argument", + HashMap::from([("value".to_string(), format!("'{}'", arg))]), + ), + }) +} + // GNU uses an unsigned 32 bit integer for row/col sizes, but then wraps around 16 bits // this function returns Some(n), where n is a u16 row/col size, or None if the string arg cannot be parsed as a u32 fn parse_rows_cols(arg: &str) -> Option { @@ -745,12 +816,23 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, termios.control_chars[mapping.0 as usize] = mapping.1; } -fn apply_special_setting(setting: &SpecialSetting, fd: i32) -> nix::Result<()> { +fn apply_special_setting( + _termios: &mut Termios, + setting: &SpecialSetting, + fd: i32, +) -> nix::Result<()> { let mut size = TermSize::default(); unsafe { tiocgwinsz(fd, &raw mut size)? }; match setting { SpecialSetting::Rows(n) => size.rows = *n, SpecialSetting::Cols(n) => size.columns = *n, + SpecialSetting::Line(_n) => { + // nix only defines Termios's `line_discipline` field on these platforms + #[cfg(any(target_os = "linux", target_os = "android"))] + { + _termios.line_discipline = *_n; + } + } } unsafe { tiocswinsz(fd, &raw mut size)? }; Ok(()) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 0fefec992..b6f569fa5 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -241,3 +241,55 @@ fn row_column_sizes() { .fails() .stderr_contains("missing argument to 'rows'"); } + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn line() { + new_ucmd!() + .args(&["line"]) + .fails() + .stderr_contains("missing argument to 'line'"); + + new_ucmd!() + .args(&["line", "-1"]) + .fails() + .stderr_contains("invalid integer argument: '-1'"); + + new_ucmd!() + .args(&["line", "256"]) + .fails() + .stderr_contains("invalid integer argument: '256'"); +} + +#[test] +fn min_and_time() { + new_ucmd!() + .args(&["min"]) + .fails() + .stderr_contains("missing argument to 'min'"); + + new_ucmd!() + .args(&["time"]) + .fails() + .stderr_contains("missing argument to 'time'"); + + new_ucmd!() + .args(&["min", "-1"]) + .fails() + .stderr_contains("invalid integer argument: '-1'"); + + new_ucmd!() + .args(&["time", "-1"]) + .fails() + .stderr_contains("invalid integer argument: '-1'"); + + new_ucmd!() + .args(&["min", "256"]) + .fails() + .stderr_contains("invalid integer argument: '256': Value too large for defined data type"); + + new_ucmd!() + .args(&["time", "256"]) + .fails() + .stderr_contains("invalid integer argument: '256': Value too large for defined data type"); +}