From 7179ded826ee18cfb88642b2f56155623c791015 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 28 Nov 2025 21:47:54 +0000 Subject: [PATCH 1/2] Implemented hex and octal parsing for row columns for stty --- src/uu/stty/Cargo.toml | 2 +- src/uu/stty/src/stty.rs | 14 +--- src/uucore/src/lib/features/parser/mod.rs | 1 + .../src/lib/features/parser/parse_int.rs | 77 +++++++++++++++++++ tests/by-util/test_stty.rs | 28 +++++++ 5 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 src/uucore/src/lib/features/parser/parse_int.rs 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..329363cd68f 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::parse_int::parse_u16_wrapped; use uucore::translate; #[cfg(not(any( @@ -323,7 +324,7 @@ fn stty(opts: &Options) -> UResult<()> { }, "rows" => { if let Some(rows) = args_iter.next() { - if let Some(n) = parse_rows_cols(rows) { + if let Some(n) = parse_u16_wrapped(rows) { valid_args.push(ArgOptions::Special(SpecialSetting::Rows(n))); } else { return invalid_integer_arg(rows); @@ -334,7 +335,7 @@ fn stty(opts: &Options) -> UResult<()> { } "columns" | "cols" => { if let Some(cols) = args_iter.next() { - if let Some(n) = parse_rows_cols(cols) { + if let Some(n) = parse_u16_wrapped(cols) { valid_args.push(ArgOptions::Special(SpecialSetting::Cols(n))); } else { return invalid_integer_arg(cols); @@ -478,15 +479,6 @@ 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 -fn parse_rows_cols(arg: &str) -> Option { - if let Ok(n) = arg.parse::() { - return Some((n % (u16::MAX as u32 + 1)) as u16); - } - None -} - /// Parse a saved terminal state string in stty format. /// /// The format is colon-separated hexadecimal values: diff --git a/src/uucore/src/lib/features/parser/mod.rs b/src/uucore/src/lib/features/parser/mod.rs index 800fe6e8ca1..3ae9d06bb0c 100644 --- a/src/uucore/src/lib/features/parser/mod.rs +++ b/src/uucore/src/lib/features/parser/mod.rs @@ -6,6 +6,7 @@ pub mod num_parser; pub mod parse_glob; +pub mod parse_int; pub mod parse_size; pub mod parse_time; pub mod shortcut_value_parser; diff --git a/src/uucore/src/lib/features/parser/parse_int.rs b/src/uucore/src/lib/features/parser/parse_int.rs new file mode 100644 index 00000000000..586de00f816 --- /dev/null +++ b/src/uucore/src/lib/features/parser/parse_int.rs @@ -0,0 +1,77 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Parser for integers with support for decimal, hexadecimal, and octal formats. + +/// Parse an integer string with support for hex (0x/0X) and octal (0) prefixes. +/// +/// Returns `None` if parsing fails or the value exceeds `u32::MAX`. +/// +pub fn parse_u32_with_radix(arg: &str) -> Option { + if let Some(hex) = arg.strip_prefix("0x").or_else(|| arg.strip_prefix("0X")) { + u32::from_str_radix(hex, 16).ok() + } else if let Some(octal) = arg.strip_prefix('0') { + if octal.is_empty() { + Some(0) + } else { + u32::from_str_radix(octal, 8).ok() + } + } else { + arg.parse::().ok() + } +} + +/// Parse an integer string and wrap to u16 range. +/// +/// Supports hex (0x/0X) and octal (0) prefixes. Values are wrapped using modulo arithmetic. +pub fn parse_u16_wrapped(arg: &str) -> Option { + let n = parse_u32_with_radix(arg)?; + Some((n % (u16::MAX as u32 + 1)) as u16) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_u32_decimal() { + assert_eq!(parse_u32_with_radix("0"), Some(0)); + assert_eq!(parse_u32_with_radix("123"), Some(123)); + assert_eq!(parse_u32_with_radix("4294967295"), Some(u32::MAX)); + } + + #[test] + fn test_parse_u32_hex() { + assert_eq!(parse_u32_with_radix("0x0"), Some(0)); + assert_eq!(parse_u32_with_radix("0x1E"), Some(30)); + assert_eq!(parse_u32_with_radix("0X1E"), Some(30)); + assert_eq!(parse_u32_with_radix("0xFFFFFFFF"), Some(u32::MAX)); + } + + #[test] + fn test_parse_u32_octal() { + assert_eq!(parse_u32_with_radix("00"), Some(0)); + assert_eq!(parse_u32_with_radix("036"), Some(30)); + assert_eq!(parse_u32_with_radix("037777777777"), Some(u32::MAX)); + } + + #[test] + fn test_parse_u32_invalid() { + assert_eq!(parse_u32_with_radix(""), None); + assert_eq!(parse_u32_with_radix("abc"), None); + assert_eq!(parse_u32_with_radix("0xGGG"), None); + assert_eq!(parse_u32_with_radix("4294967296"), None); // overflow + } + + #[test] + fn test_parse_u16_wrapped() { + assert_eq!(parse_u16_wrapped("30"), Some(30)); + assert_eq!(parse_u16_wrapped("0x1E"), Some(30)); + assert_eq!(parse_u16_wrapped("036"), Some(30)); + assert_eq!(parse_u16_wrapped("65535"), Some(u16::MAX)); + assert_eq!(parse_u16_wrapped("65536"), Some(0)); + assert_eq!(parse_u16_wrapped("65537"), Some(1)); + } +} diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index f68de5daf5b..6b80898a2b0 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -290,6 +290,34 @@ 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", "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() { From 9da9baa891b888a313f3b7fe45dc3f2a96107b12 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Wed, 3 Dec 2025 05:35:38 +0000 Subject: [PATCH 2/2] Adding more test cases with different upper and lowercase values --- tests/by-util/test_stty.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 6b80898a2b0..56d9f15f3f2 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -299,6 +299,8 @@ fn test_row_column_hex_octal() { // 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