Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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