From d7dfafcdebe721a20b272f77833002535bf32a9d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:22:33 +0900 Subject: [PATCH 1/3] build-gnu.sh: Remove 2 sed hacks for tr --- util/build-gnu.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 46a2852ef..4a36f803e 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -135,7 +135,6 @@ else "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver - "${SED}" -i 's| tr | /usr/bin/tr |' tests/init.sh # Use a better diff "${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" @@ -342,11 +341,6 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # Slightly different error message "${SED}" -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh -# Most tests check that `/usr/bin/tr` is working correctly before running. -# However in NixOS/Nix-based distros, the tr util is located somewhere in -# /nix/store/xxxxxxxxxxxx...xxxx/bin/tr -# We just replace the references to `/usr/bin/tr` -"${SED}" -i 's/\/usr\/bin\/tr/$(command -v tr)/' tests/init.sh # upstream doesn't having the program name in the error message # but we do. We should keep it that way. From a16df34f9db42c7a6c2e6dab71c1e3ad91eb6dca Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:51:29 +0900 Subject: [PATCH 2/3] Update Dockerfile: Don't apt-get jq (preinstalled) --- .devcontainer/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9befa73fa..4296d58c4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update \ gcc \ gdb \ gperf \ - jq \ libacl1-dev \ libattr1-dev \ libcap-dev \ From 824c5c7c937a9ff60e996c88584c89f77d65339f Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 26 Nov 2025 01:33:26 -0500 Subject: [PATCH 3/3] stty: Implemented saved state parser for stty (#9480) * Implemented saved state parser for stty * Add compatibility to macos flag type * Added many example state parsing integration tests with GNU compatibility checks and documentation * Spelling and formatting fixes * Matching behaviour of adding the help command after invocations and spelling fixes * GNU tests were being skipped because they were not at the sufficient version * Fixed messaging error for invalid states to not show full path * Normalizing the test output and reverting lib change * Discovered that the limit depends on platform specific values derived from a LIBC value * Spelling fixes and setting flags to 0 for cross platform compatibility * Clippy fixes * Disabling tests due to invalid printing of control chars and using GNU for printing * Redisabling failing test as outside of the scope of this PR * Adding g prefix support to normalize stderr * Spell checker fixes * Normalizing command for both gnu and uutils output * removing single value from testing since it can be interpreted as Baud rate * Fixing spelling mistake --- src/uu/stty/src/stty.rs | 93 +++++++++++++++++-- tests/by-util/test_stty.rs | 183 ++++++++++++++++++++++++++++++++++++- 2 files changed, 266 insertions(+), 10 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 42432c22c..8b8da5135 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 +// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS mod flags; @@ -30,7 +30,7 @@ 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::{UError, UResult, USimpleError}; +use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::translate; @@ -150,6 +150,7 @@ enum ArgOptions<'a> { Mapping((S, u8)), Special(SpecialSetting), Print(PrintSetting), + SavedState(Vec), } impl<'a> From> for ArgOptions<'a> { @@ -352,8 +353,12 @@ fn stty(opts: &Options) -> UResult<()> { valid_args.push(ArgOptions::Print(PrintSetting::Size)); } _ => { + // Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...") + if let Some(state) = parse_saved_state(arg) { + valid_args.push(ArgOptions::SavedState(state)); + } // control char - if let Some(char_index) = cc_to_index(arg) { + else if let Some(char_index) = cc_to_index(arg) { if let Some(mapping) = args_iter.next() { let cc_mapping = string_to_control_char(mapping).map_err(|e| { let message = match e { @@ -370,7 +375,7 @@ fn stty(opts: &Options) -> UResult<()> { ) } }; - USimpleError::new(1, message) + UUsageError::new(1, message) })?; valid_args.push(ArgOptions::Mapping((char_index, cc_mapping))); } else { @@ -418,6 +423,9 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Print(setting) => { print_special_setting(setting, opts.file.as_raw_fd())?; } + ArgOptions::SavedState(state) => { + apply_saved_state(&mut termios, state)?; + } } } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; @@ -429,8 +437,9 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +// The GNU implementation adds the --help message when the args are incorrectly formatted fn missing_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-missing-argument", @@ -440,7 +449,7 @@ fn missing_arg(arg: &str) -> Result> { } fn invalid_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-argument", @@ -450,7 +459,7 @@ fn invalid_arg(arg: &str) -> Result> { } fn invalid_integer_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-integer-argument", @@ -478,6 +487,43 @@ fn parse_rows_cols(arg: &str) -> Option { None } +/// Parse a saved terminal state string in stty format. +/// +/// The format is colon-separated hexadecimal values: +/// `input_flags:output_flags:control_flags:local_flags:cc0:cc1:cc2:...` +/// +/// - Must have exactly 4 + NCCS parts (4 flags + platform-specific control characters) +/// - All parts must be non-empty valid hex values +/// - Control characters must fit in u8 (0-255) +/// - Returns `None` if format is invalid +fn parse_saved_state(arg: &str) -> Option> { + let parts: Vec<&str> = arg.split(':').collect(); + let expected_parts = 4 + nix::libc::NCCS; + + // GNU requires exactly the right number of parts for this platform + if parts.len() != expected_parts { + return None; + } + + // Validate all parts are non-empty valid hex + let mut values = Vec::with_capacity(expected_parts); + for (i, part) in parts.iter().enumerate() { + if part.is_empty() { + return None; // GNU rejects empty hex values + } + let val = u32::from_str_radix(part, 16).ok()?; + + // Control characters (indices 4+) must fit in u8 + if i >= 4 && val > 255 { + return None; + } + + values.push(val); + } + + Some(values) +} + fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } @@ -857,6 +903,39 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { termios.control_chars[mapping.0 as usize] = mapping.1; } +/// Apply a saved terminal state to the current termios. +/// +/// The state array contains: +/// - `state[0]`: input flags +/// - `state[1]`: output flags +/// - `state[2]`: control flags +/// - `state[3]`: local flags +/// - `state[4..]`: control characters (optional) +/// +/// If state has fewer than 4 elements, no changes are applied. This is a defensive +/// check that should never trigger since `parse_saved_state` rejects such states. +fn apply_saved_state(termios: &mut Termios, state: &[u32]) -> nix::Result<()> { + // Require at least 4 elements for the flags (defensive check) + if state.len() < 4 { + return Ok(()); // No-op for invalid state (already validated by parser) + } + + // Apply the four flag groups, done (as _) for MacOS size compatibility + termios.input_flags = InputFlags::from_bits_truncate(state[0] as _); + termios.output_flags = OutputFlags::from_bits_truncate(state[1] as _); + termios.control_flags = ControlFlags::from_bits_truncate(state[2] as _); + termios.local_flags = LocalFlags::from_bits_truncate(state[3] as _); + + // Apply control characters if present (stored as u32 but used as u8) + for (i, &cc_val) in state.iter().skip(4).enumerate() { + if i < termios.control_chars.len() { + termios.control_chars[i] = cc_val as u8; + } + } + + Ok(()) +} + fn apply_special_setting( _termios: &mut Termios, setting: &SpecialSetting, diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 9626c1406..f68de5daf 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -2,10 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed +// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed NCCS nonhex gstty -use uutests::new_ucmd; -use uutests::util::pty_path; +use uutests::util::{expected_result, pty_path}; +use uutests::{at_and_ts, new_ucmd, unwrap_or_return}; + +/// Normalize stderr by replacing the full binary path with just the utility name +/// This allows comparison between GNU (which shows "stty" or "gstty") and ours (which shows full path) +fn normalize_stderr(stderr: &str) -> String { + // Replace patterns like "Try 'gstty --help'" or "Try '/path/to/stty --help'" with "Try 'stty --help'" + let re = regex::Regex::new(r"Try '[^']*(?:g)?stty --help'").unwrap(); + re.replace_all(stderr, "Try 'stty --help'").to_string() +} #[test] fn test_invalid_arg() { @@ -349,3 +357,172 @@ fn non_negatable_combo() { .fails() .stderr_contains("invalid argument '-ek'"); } + +// Tests for saved state parsing and restoration +#[test] +#[cfg(unix)] +fn test_save_and_restore() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_with_g_flag() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["-g", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_restore_after_change() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + + new_ucmd!() + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); + + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_str_check(|s| !s.contains("intr = ^A")); +} + +// These tests both validate what we expect each input to return and their error codes +// and also use the GNU coreutils results to validate our results match expectations +#[test] +#[cfg(unix)] +fn test_saved_state_valid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Generate valid saved state from the actual terminal + let saved = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])).stdout_move_str(); + let saved = saved.trim(); + + let result = ts.ucmd().args(&["--file", &path, saved]).run(); + + result.success().no_stderr(); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, saved])); + 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_saved_state_invalid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + let num_cc = nix::libc::NCCS; + + // Build test strings with platform-specific counts + let cc_zeros = vec!["0"; num_cc].join(":"); + let cc_with_invalid = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "100"; // First control char > 255 + parts.join(":") + } else { + String::new() + }; + let cc_with_space = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "1c "; // Space in hex + parts.join(":") + } else { + String::new() + }; + let cc_with_nonhex = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "xyz"; // Non-hex + parts.join(":") + } else { + String::new() + }; + let cc_with_empty = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = ""; // Empty + parts.join(":") + } else { + String::new() + }; + + // Cannot test single value since it would be interpreted as baud rate + let invalid_states = vec![ + "500:5:4bf".to_string(), // fewer than expected parts + "500:5:4bf:8a3b".to_string(), // only 4 parts + format!("500:5:{}:8a3b:{}", cc_zeros, "extra"), // too many parts + format!("500::4bf:8a3b:{}", cc_zeros), // empty hex value in flags + format!("500:5:4bf:8a3b:{}", cc_with_empty), // empty hex value in cc + format!("500:5:4bf:8a3b:{}", cc_with_nonhex), // non-hex characters + format!("500:5:4bf:8a3b:{}", cc_with_space), // space in hex value + format!("500:5:4bf:8a3b:{}", cc_with_invalid), // control char > 255 + ]; + + for state in &invalid_states { + let result = ts.ucmd().args(&["--file", &path, state]).run(); + + result.failure().stderr_contains("invalid argument"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, state])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + let exp_normalized_stderr = normalize_stderr(exp_result.stderr_str()); + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_normalized_stderr); + } +} + +#[test] +#[cfg(unix)] +#[ignore = "Fails because the implementation of print state is not correctly printing flags on certain platforms"] +fn test_saved_state_with_control_chars() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Build a valid saved state with platform-specific number of control characters + let num_cc = nix::libc::NCCS; + let cc_values: Vec = (1..=num_cc).map(|_| format!("{:x}", 0)).collect(); + let saved_state = format!("500:5:4bf:8a3b:{}", cc_values.join(":")); + + ts.ucmd().args(&["--file", &path, &saved_state]).succeeds(); + + let result = ts.ucmd().args(&["-g", "--file", &path]).run(); + + result.success().stdout_contains(":"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); +}