From a445f5c4b1e5e6f121502f92b4cc049b46295cff Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 28 Nov 2025 22:20:22 +0000 Subject: [PATCH 1/7] Implemented input and output baud rate setting for stty --- src/uu/stty/src/flags.rs | 10 ++++- src/uu/stty/src/stty.rs | 82 ++++++++++++++++++++++++++------------ tests/by-util/test_stty.rs | 64 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index c2a82198a95..f0fdbf2c0b6 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -27,6 +27,12 @@ use nix::sys::termios::{ SpecialCharacterIndices as S, }; +pub enum BaudType { + Input, + Output, + Both, +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub enum AllFlags<'a> { @@ -38,7 +44,7 @@ pub enum AllFlags<'a> { target_os = "netbsd", target_os = "openbsd" ))] - Baud(u32), + Baud(u32, BaudType), #[cfg(not(any( target_os = "freebsd", target_os = "dragonfly", @@ -47,7 +53,7 @@ pub enum AllFlags<'a> { target_os = "netbsd", target_os = "openbsd" )))] - Baud(BaudRate), + Baud(BaudRate, BaudType), ControlFlags((&'a Flag, bool)), InputFlags((&'a Flag, bool)), LocalFlags((&'a Flag, bool)), diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 8808857b630..14a46ab6ddf 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -10,7 +10,7 @@ // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp -// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS +// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS cfsetispeed // spell-checker:ignore notaflag notacombo notabaud mod flags; @@ -21,7 +21,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; use nix::sys::termios::{ ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S, - Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr, + Termios, cfgetospeed, cfsetispeed, cfsetospeed, tcgetattr, tcsetattr, }; use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; @@ -274,19 +274,24 @@ fn stty(opts: &Options) -> UResult<()> { let mut args_iter = args.iter(); while let Some(&arg) = args_iter.next() { match arg { - "ispeed" | "ospeed" => match args_iter.next() { + "ispeed" => match args_iter.next() { Some(speed) => { - if let Some(baud_flag) = string_to_baud(speed) { + if let Some(baud_flag) = string_to_baud(speed, flags::BaudType::Input) { valid_args.push(ArgOptions::Flags(baud_flag)); } else { - return Err(USimpleError::new( - 1, - translate!( - "stty-error-invalid-speed", - "arg" => *arg, - "speed" => *speed, - ), - )); + return invalid_speed(arg, speed); + } + } + None => { + return missing_arg(arg); + } + }, + "ospeed" => match args_iter.next() { + Some(speed) => { + if let Some(baud_flag) = string_to_baud(speed, flags::BaudType::Output) { + valid_args.push(ArgOptions::Flags(baud_flag)); + } else { + return invalid_speed(arg, speed); } } None => { @@ -383,12 +388,12 @@ fn stty(opts: &Options) -> UResult<()> { return missing_arg(arg); } // baud rate - } else if let Some(baud_flag) = string_to_baud(arg) { + } else if let Some(baud_flag) = string_to_baud(arg, flags::BaudType::Both) { valid_args.push(ArgOptions::Flags(baud_flag)); // non control char flag } else if let Some(flag) = string_to_flag(arg) { let remove_group = match flag { - AllFlags::Baud(_) => false, + AllFlags::Baud(_, _) => false, AllFlags::ControlFlags((flag, remove)) => { check_flag_group(flag, remove) } @@ -417,7 +422,7 @@ fn stty(opts: &Options) -> UResult<()> { for arg in &valid_args { match arg { ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping), - ArgOptions::Flags(flag) => apply_setting(&mut termios, flag), + ArgOptions::Flags(flag) => apply_setting(&mut termios, flag)?, ArgOptions::Special(setting) => { apply_special_setting(&mut termios, setting, opts.file.as_raw_fd())?; } @@ -468,6 +473,17 @@ fn invalid_integer_arg(arg: &str) -> Result> { )) } +fn invalid_speed(arg: &str, speed: &str) -> Result> { + Err(UUsageError::new( + 1, + translate!( + "stty-error-invalid-speed", + "arg" => arg, + "speed" => speed, + ), + )) +} + /// 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 { @@ -719,7 +735,7 @@ fn parse_baud_with_rounding(normalized: &str) -> Option { Some(value) } -fn string_to_baud(arg: &str) -> Option> { +fn string_to_baud(arg: &str, baud_type: flags::BaudType) -> Option> { // Reject invalid formats if arg != arg.trim_end() || arg.trim().starts_with('-') @@ -744,7 +760,7 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" ))] - return Some(AllFlags::Baud(value)); + return Some(AllFlags::Baud(value, baud_type)); #[cfg(not(any( target_os = "freebsd", @@ -757,7 +773,7 @@ fn string_to_baud(arg: &str) -> Option> { { for (text, baud_rate) in BAUD_RATES { if text.parse::().ok() == Some(value) { - return Some(AllFlags::Baud(*baud_rate)); + return Some(AllFlags::Baud(*baud_rate, baud_type)); } } None @@ -940,9 +956,9 @@ fn print_flags( } /// Apply a single setting -fn apply_setting(termios: &mut Termios, setting: &AllFlags) { +fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { match setting { - AllFlags::Baud(_) => apply_baud_rate_flag(termios, setting), + AllFlags::Baud(_, _) => apply_baud_rate_flag(termios, setting)?, AllFlags::ControlFlags((setting, disable)) => { setting.flag.apply(termios, !disable); } @@ -956,9 +972,10 @@ fn apply_setting(termios: &mut Termios, setting: &AllFlags) { setting.flag.apply(termios, !disable); } } + Ok(()) } -fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) { +fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result<()> { // BSDs use a u32 for the baud rate, so any decimal number applies. #[cfg(any( target_os = "freebsd", @@ -968,8 +985,15 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) { target_os = "netbsd", target_os = "openbsd" ))] - if let AllFlags::Baud(n) = input { - cfsetospeed(termios, *n).expect("Failed to set baud rate"); + if let AllFlags::Baud(n, baud_type) = input { + match baud_type { + flags::BaudType::Input => cfsetispeed(termios, *n)?, + flags::BaudType::Output => cfsetospeed(termios, *n)?, + flags::BaudType::Both => { + cfsetispeed(termios, *n)?; + cfsetospeed(termios, *n)?; + } + } } // Other platforms use an enum. @@ -981,9 +1005,17 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) { target_os = "netbsd", target_os = "openbsd" )))] - if let AllFlags::Baud(br) = input { - cfsetospeed(termios, *br).expect("Failed to set baud rate"); + if let AllFlags::Baud(br, baud_type) = input { + match baud_type { + flags::BaudType::Input => cfsetispeed(termios, *br)?, + flags::BaudType::Output => cfsetospeed(termios, *br)?, + flags::BaudType::Both => { + cfsetispeed(termios, *br)?; + cfsetospeed(termios, *br)?; + } + } } + Ok(()) } fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index ae64eb6aeb5..ade212e3af7 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1627,6 +1627,70 @@ fn test_stty_uses_stdin() { .stdout_contains("columns 100"); } +#[test] +#[cfg(unix)] +fn test_ispeed_ospeed_valid_speeds() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Test various valid baud rates for both ispeed and ospeed + let test_cases = [ + ("ispeed", "50"), + ("ispeed", "9600"), + ("ispeed", "19200"), + ("ospeed", "1200"), + ("ospeed", "9600"), + ("ospeed", "38400"), + ]; + + for (arg, speed) in test_cases { + let result = ts.ucmd().args(&["--file", &path, arg, speed]).run(); + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, arg, speed])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_result.stderr_str()); + } +} + +#[test] +#[cfg(all( + unix, + not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )) +))] +fn test_ispeed_ospeed_invalid_speeds() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Test invalid speed values (non-standard baud rates) + let test_cases = [ + ("ispeed", "12345"), + ("ospeed", "99999"), + ("ispeed", "abc"), + ("ospeed", "xyz"), + ]; + + for (arg, speed) in test_cases { + let result = ts.ucmd().args(&["--file", &path, arg, speed]).run(); + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, arg, speed])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_result.stderr_str()); + } +} + #[test] #[cfg(unix)] fn test_columns_env_wrapping() { From 2ad441c5bbc516cf32466ee9f02d7d45aeb58d1f Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Sun, 7 Dec 2025 10:36:58 -0800 Subject: [PATCH 2/7] Ignore test_ispeed_ospeed_valid_speeds for issue #9547 on different platforms the output is different due to different versions so cannot use this as a test. --- tests/by-util/test_stty.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index ade212e3af7..058cd679dea 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1629,6 +1629,7 @@ fn test_stty_uses_stdin() { #[test] #[cfg(unix)] +#[ignore = "Issue: #9547"] fn test_ispeed_ospeed_valid_speeds() { let (path, _controller, _replica) = pty_path(); let (_at, ts) = at_and_ts!(); From 58d50fe912a06dfaebbb31518402bcd583e0418c Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Sun, 7 Dec 2025 14:15:29 -0500 Subject: [PATCH 3/7] Re-enable ignored tests for valid and invalid speeds --- tests/by-util/test_stty.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 058cd679dea..c2b8a77e5a2 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1629,7 +1629,6 @@ fn test_stty_uses_stdin() { #[test] #[cfg(unix)] -#[ignore = "Issue: #9547"] fn test_ispeed_ospeed_valid_speeds() { let (path, _controller, _replica) = pty_path(); let (_at, ts) = at_and_ts!(); @@ -1668,6 +1667,7 @@ fn test_ispeed_ospeed_valid_speeds() { target_os = "openbsd" )) ))] +#[ignore = "Issue: #9547"] fn test_ispeed_ospeed_invalid_speeds() { let (path, _controller, _replica) = pty_path(); let (_at, ts) = at_and_ts!(); From 3eb5b2afdde69688b0065c0122b73a4caca633b2 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sun, 7 Dec 2025 21:38:01 +0000 Subject: [PATCH 4/7] Adding require_controlling_input_terminal_ for bad-speed test since missing that flag --- util/build-gnu.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 6075c863727..eaac032c9df 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -359,3 +359,6 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # * the selinux crate is handling errors # * the test says "maybe we should not fail when no context available" "${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh + +# Skip bad-speed test if not interactive (requires controlling terminal) +"${SED}" -i '/print_ver_ stty/a require_controlling_input_terminal_' tests/stty/bad-speed.sh From 2d1b9918b62472db4d5b757cdc15e0001571576c Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Tue, 30 Dec 2025 19:57:18 +0000 Subject: [PATCH 5/7] Refactor apply_baud_rate_flag to remove duplicate cfg blocks --- src/uu/stty/src/flags.rs | 1 + src/uu/stty/src/stty.rs | 63 +++++++++++----------------------------- 2 files changed, 18 insertions(+), 46 deletions(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index f0fdbf2c0b6..6c3f4c8788a 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -27,6 +27,7 @@ use nix::sys::termios::{ SpecialCharacterIndices as S, }; +#[derive(Debug)] pub enum BaudType { Input, Output, diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 14a46ab6ddf..f34f9b498e5 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -976,42 +976,13 @@ fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { } fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result<()> { - // BSDs use a u32 for the baud rate, so any decimal number applies. - #[cfg(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - ))] - if let AllFlags::Baud(n, baud_type) = input { - match baud_type { - flags::BaudType::Input => cfsetispeed(termios, *n)?, - flags::BaudType::Output => cfsetospeed(termios, *n)?, - flags::BaudType::Both => { - cfsetispeed(termios, *n)?; - cfsetospeed(termios, *n)?; - } - } - } - - // Other platforms use an enum. - #[cfg(not(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - )))] - if let AllFlags::Baud(br, baud_type) = input { + if let AllFlags::Baud(rate, baud_type) = input { match baud_type { - flags::BaudType::Input => cfsetispeed(termios, *br)?, - flags::BaudType::Output => cfsetospeed(termios, *br)?, + flags::BaudType::Input => cfsetispeed(termios, *rate)?, + flags::BaudType::Output => cfsetospeed(termios, *rate)?, flags::BaudType::Both => { - cfsetispeed(termios, *br)?; - cfsetospeed(termios, *br)?; + cfsetispeed(termios, *rate)?; + cfsetospeed(termios, *rate)?; } } } @@ -1478,10 +1449,10 @@ mod tests { target_os = "openbsd" )))] { - assert!(string_to_baud("9600").is_some()); - assert!(string_to_baud("115200").is_some()); - assert!(string_to_baud("38400").is_some()); - assert!(string_to_baud("19200").is_some()); + assert!(string_to_baud("9600", flags::BaudType::Both).is_some()); + assert!(string_to_baud("115200", flags::BaudType::Both).is_some()); + assert!(string_to_baud("38400", flags::BaudType::Both).is_some()); + assert!(string_to_baud("19200", flags::BaudType::Both).is_some()); } #[cfg(any( @@ -1493,10 +1464,10 @@ mod tests { target_os = "openbsd" ))] { - assert!(string_to_baud("9600").is_some()); - assert!(string_to_baud("115200").is_some()); - assert!(string_to_baud("1000000").is_some()); - assert!(string_to_baud("0").is_some()); + assert!(string_to_baud("9600", flags::BaudType::Both).is_some()); + assert!(string_to_baud("115200", flags::BaudType::Both).is_some()); + assert!(string_to_baud("1000000", flags::BaudType::Both).is_some()); + assert!(string_to_baud("0", flags::BaudType::Both).is_some()); } } @@ -1511,10 +1482,10 @@ mod tests { target_os = "openbsd" )))] { - assert_eq!(string_to_baud("995"), None); - assert_eq!(string_to_baud("invalid"), None); - assert_eq!(string_to_baud(""), None); - assert_eq!(string_to_baud("abc"), None); + assert_eq!(string_to_baud("995", flags::BaudType::Both), None); + assert_eq!(string_to_baud("invalid", flags::BaudType::Both), None); + assert_eq!(string_to_baud("", flags::BaudType::Both), None); + assert_eq!(string_to_baud("abc", flags::BaudType::Both), None); } } From 7f4091f4454cc01b14fb5c4cb710b51510ed1ed5 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Tue, 30 Dec 2025 20:33:04 +0000 Subject: [PATCH 6/7] Add PartialEq derive for BaudType in test builds --- src/uu/stty/src/flags.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 6c3f4c8788a..c346cbe7c5c 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -28,6 +28,7 @@ use nix::sys::termios::{ }; #[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub enum BaudType { Input, Output, From dc7ee6acfd2b4c274a90fb1082246f34b9052494 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 7 Jan 2026 11:09:16 -0500 Subject: [PATCH 7/7] Remove skip condition for bad-speed test now that upstream is updated --- util/build-gnu.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index eaac032c9df..6075c863727 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -359,6 +359,3 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # * the selinux crate is handling errors # * the test says "maybe we should not fail when no context available" "${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh - -# Skip bad-speed test if not interactive (requires controlling terminal) -"${SED}" -i '/print_ver_ stty/a require_controlling_input_terminal_' tests/stty/bad-speed.sh