Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/uu/stty/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
14 changes: 3 additions & 11 deletions src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -478,15 +479,6 @@ fn parse_u8_or_err(arg: &str) -> Result<u8, String> {
})
}

/// 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<u16> {
if let Ok(n) = arg.parse::<u32>() {
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:
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/features/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
77 changes: 77 additions & 0 deletions src/uucore/src/lib/features/parser/parse_int.rs
Original file line number Diff line number Diff line change
@@ -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<u32> {
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::<u32>().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<u16> {
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));
}
}
28 changes: 28 additions & 0 deletions tests/by-util/test_stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading