Skip to content

Commit 0e13402

Browse files
committed
Implemented hex and octal parsing for row columns for stty
1 parent b8edd13 commit 0e13402

File tree

5 files changed

+110
-12
lines changed

5 files changed

+110
-12
lines changed

src/uu/stty/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ path = "src/stty.rs"
1919

2020
[dependencies]
2121
clap = { workspace = true }
22-
uucore = { workspace = true }
22+
uucore = { workspace = true, features = ["parser"] }
2323
nix = { workspace = true, features = ["term", "ioctl"] }
2424
fluent = { workspace = true }
2525

src/uu/stty/src/stty.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use std::os::unix::fs::OpenOptionsExt;
3232
use std::os::unix::io::{AsRawFd, RawFd};
3333
use uucore::error::{UError, UResult, USimpleError, UUsageError};
3434
use uucore::format_usage;
35+
use uucore::parser::parse_int::parse_u16_wrapped;
3536
use uucore::translate;
3637

3738
#[cfg(not(any(
@@ -323,7 +324,7 @@ fn stty(opts: &Options) -> UResult<()> {
323324
},
324325
"rows" => {
325326
if let Some(rows) = args_iter.next() {
326-
if let Some(n) = parse_rows_cols(rows) {
327+
if let Some(n) = parse_u16_wrapped(rows) {
327328
valid_args.push(ArgOptions::Special(SpecialSetting::Rows(n)));
328329
} else {
329330
return invalid_integer_arg(rows);
@@ -334,7 +335,7 @@ fn stty(opts: &Options) -> UResult<()> {
334335
}
335336
"columns" | "cols" => {
336337
if let Some(cols) = args_iter.next() {
337-
if let Some(n) = parse_rows_cols(cols) {
338+
if let Some(n) = parse_u16_wrapped(cols) {
338339
valid_args.push(ArgOptions::Special(SpecialSetting::Cols(n)));
339340
} else {
340341
return invalid_integer_arg(cols);
@@ -478,15 +479,6 @@ fn parse_u8_or_err(arg: &str) -> Result<u8, String> {
478479
})
479480
}
480481

481-
/// GNU uses an unsigned 32-bit integer for row/col sizes, but then wraps around 16 bits
482-
/// 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
483-
fn parse_rows_cols(arg: &str) -> Option<u16> {
484-
if let Ok(n) = arg.parse::<u32>() {
485-
return Some((n % (u16::MAX as u32 + 1)) as u16);
486-
}
487-
None
488-
}
489-
490482
/// Parse a saved terminal state string in stty format.
491483
///
492484
/// The format is colon-separated hexadecimal values:

src/uucore/src/lib/features/parser/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
pub mod num_parser;
88
pub mod parse_glob;
9+
pub mod parse_int;
910
pub mod parse_size;
1011
pub mod parse_time;
1112
pub mod shortcut_value_parser;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
//! Parser for integers with support for decimal, hexadecimal, and octal formats.
7+
8+
/// Parse an integer string with support for hex (0x/0X) and octal (0) prefixes.
9+
///
10+
/// Returns `None` if parsing fails or the value exceeds `u32::MAX`.
11+
///
12+
pub fn parse_u32_with_radix(arg: &str) -> Option<u32> {
13+
if let Some(hex) = arg.strip_prefix("0x").or_else(|| arg.strip_prefix("0X")) {
14+
u32::from_str_radix(hex, 16).ok()
15+
} else if let Some(octal) = arg.strip_prefix('0') {
16+
if octal.is_empty() {
17+
Some(0)
18+
} else {
19+
u32::from_str_radix(octal, 8).ok()
20+
}
21+
} else {
22+
arg.parse::<u32>().ok()
23+
}
24+
}
25+
26+
/// Parse an integer string and wrap to u16 range.
27+
///
28+
/// Supports hex (0x/0X) and octal (0) prefixes. Values are wrapped using modulo arithmetic.
29+
pub fn parse_u16_wrapped(arg: &str) -> Option<u16> {
30+
let n = parse_u32_with_radix(arg)?;
31+
Some((n % (u16::MAX as u32 + 1)) as u16)
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
use super::*;
37+
38+
#[test]
39+
fn test_parse_u32_decimal() {
40+
assert_eq!(parse_u32_with_radix("0"), Some(0));
41+
assert_eq!(parse_u32_with_radix("123"), Some(123));
42+
assert_eq!(parse_u32_with_radix("4294967295"), Some(u32::MAX));
43+
}
44+
45+
#[test]
46+
fn test_parse_u32_hex() {
47+
assert_eq!(parse_u32_with_radix("0x0"), Some(0));
48+
assert_eq!(parse_u32_with_radix("0x1E"), Some(30));
49+
assert_eq!(parse_u32_with_radix("0X1E"), Some(30));
50+
assert_eq!(parse_u32_with_radix("0xFFFFFFFF"), Some(u32::MAX));
51+
}
52+
53+
#[test]
54+
fn test_parse_u32_octal() {
55+
assert_eq!(parse_u32_with_radix("00"), Some(0));
56+
assert_eq!(parse_u32_with_radix("036"), Some(30));
57+
assert_eq!(parse_u32_with_radix("037777777777"), Some(u32::MAX));
58+
}
59+
60+
#[test]
61+
fn test_parse_u32_invalid() {
62+
assert_eq!(parse_u32_with_radix(""), None);
63+
assert_eq!(parse_u32_with_radix("abc"), None);
64+
assert_eq!(parse_u32_with_radix("0xGGG"), None);
65+
assert_eq!(parse_u32_with_radix("4294967296"), None); // overflow
66+
}
67+
68+
#[test]
69+
fn test_parse_u16_wrapped() {
70+
assert_eq!(parse_u16_wrapped("30"), Some(30));
71+
assert_eq!(parse_u16_wrapped("0x1E"), Some(30));
72+
assert_eq!(parse_u16_wrapped("036"), Some(30));
73+
assert_eq!(parse_u16_wrapped("65535"), Some(u16::MAX));
74+
assert_eq!(parse_u16_wrapped("65536"), Some(0));
75+
assert_eq!(parse_u16_wrapped("65537"), Some(1));
76+
}
77+
}

tests/by-util/test_stty.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,34 @@ fn row_column_sizes() {
290290
.stderr_contains("missing argument to 'rows'");
291291
}
292292

293+
#[test]
294+
#[cfg(unix)]
295+
fn test_row_column_hex_octal() {
296+
let (path, _controller, _replica) = pty_path();
297+
let (_at, ts) = at_and_ts!();
298+
299+
// Test various numeric formats: hex (0x1E), octal (036), uppercase hex (0X1E), decimal (30), and zero
300+
let test_cases = [
301+
("rows", "0x1E"), // hexadecimal = 30
302+
("rows", "036"), // octal = 30
303+
("cols", "0X1E"), // uppercase hex = 30
304+
("columns", "30"), // decimal = 30
305+
("rows", "0"), // zero (not octal prefix)
306+
];
307+
308+
for (setting, value) in test_cases {
309+
let result = ts.ucmd().args(&["--file", &path, setting, value]).run();
310+
let exp_result =
311+
unwrap_or_return!(expected_result(&ts, &["--file", &path, setting, value]));
312+
let normalized_stderr = normalize_stderr(result.stderr_str());
313+
314+
result
315+
.stdout_is(exp_result.stdout_str())
316+
.code_is(exp_result.code());
317+
assert_eq!(normalized_stderr, exp_result.stderr_str());
318+
}
319+
}
320+
293321
#[test]
294322
#[cfg(any(target_os = "linux", target_os = "android"))]
295323
fn line() {

0 commit comments

Comments
 (0)