diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index a2e705656a5..f05a4cc5b02 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -19,7 +19,7 @@ path = "src/stty.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["parser"] } nix = { workspace = true, features = ["term", "ioctl"] } fluent = { workspace = true } diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 8b8da5135a2..2d274f6015a 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -32,6 +32,7 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; +use uucore::parser::num_parser::ExtendedParser; use uucore::translate; #[cfg(not(any( @@ -478,13 +479,15 @@ fn parse_u8_or_err(arg: &str) -> Result { }) } -/// 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 +/// Parse an integer with hex (0x/0X) and octal (0) prefix support, wrapping to u16. +/// +/// GNU stty uses an unsigned 32-bit integer for row/col sizes, then wraps to 16 bits. +/// Returns `None` if parsing fails or value exceeds u32::MAX. fn parse_rows_cols(arg: &str) -> Option { - if let Ok(n) = arg.parse::() { - return Some((n % (u16::MAX as u32 + 1)) as u16); - } - None + u64::extended_parse(arg) + .ok() + .filter(|&n| u32::try_from(n).is_ok()) + .map(|n| (n % (u16::MAX as u64 + 1)) as u16) } /// Parse a saved terminal state string in stty format. diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index f68de5daf5b..56d9f15f3f2 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -290,6 +290,36 @@ fn row_column_sizes() { .stderr_contains("missing argument to 'rows'"); } +#[test] +#[cfg(unix)] +fn test_row_column_hex_octal() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Test various numeric formats: hex (0x1E), octal (036), uppercase hex (0X1E), decimal (30), and zero + let test_cases = [ + ("rows", "0x1E"), // hexadecimal = 30 + ("rows", "0x1e"), // lowercase hexadecimal = 30 + ("rows", "0X1e"), // upper and lowercase hexadecimal = 30 + ("rows", "036"), // octal = 30 + ("cols", "0X1E"), // uppercase hex = 30 + ("columns", "30"), // decimal = 30 + ("rows", "0"), // zero (not octal prefix) + ]; + + for (setting, value) in test_cases { + let result = ts.ucmd().args(&["--file", &path, setting, value]).run(); + let exp_result = + unwrap_or_return!(expected_result(&ts, &["--file", &path, setting, value])); + 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(any(target_os = "linux", target_os = "android"))] fn line() {