Skip to content

Commit e0cf588

Browse files
ChrisDrydenmartinkunkel2
authored andcommitted
stty: baud parsing integration tests and validation (uutils#9454)
* Adding comprehensive gnu suite baud parsing rules * Adding missing spellcheck words * Fixed clippy errors and simplified rounding logic
1 parent 195e44a commit e0cf588

File tree

2 files changed

+89
-8
lines changed

2 files changed

+89
-8
lines changed

src/uu/stty/src/stty.rs

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc
1111
// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase
1212
// spell-checker:ignore sigquit sigtstp
13-
// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain
13+
// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb
1414

1515
mod flags;
1616

@@ -23,6 +23,7 @@ use nix::sys::termios::{
2323
Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr,
2424
};
2525
use nix::{ioctl_read_bad, ioctl_write_ptr_bad};
26+
use std::cmp::Ordering;
2627
use std::fs::File;
2728
use std::io::{self, Stdout, stdout};
2829
use std::num::IntErrorKind;
@@ -563,7 +564,69 @@ fn string_to_combo(arg: &str) -> Option<&str> {
563564
.map(|_| arg)
564565
}
565566

567+
/// Parse and round a baud rate value using GNU stty's custom rounding algorithm.
568+
///
569+
/// Accepts decimal values with the following rounding rules:
570+
/// - If first digit after decimal > 5: round up
571+
/// - If first digit after decimal < 5: round down
572+
/// - If first digit after decimal == 5:
573+
/// - If followed by any non-zero digit: round up
574+
/// - If followed only by zeros (or nothing): banker's rounding (round to nearest even)
575+
///
576+
/// Examples: "9600.49" -> 9600, "9600.51" -> 9600, "9600.5" -> 9600 (even), "9601.5" -> 9602 (even)
577+
/// TODO: there are two special cases "exta" → B19200 and "extb" → B38400
578+
fn parse_baud_with_rounding(normalized: &str) -> Option<u32> {
579+
let (int_part, frac_part) = match normalized.split_once('.') {
580+
Some((i, f)) => (i, Some(f)),
581+
None => (normalized, None),
582+
};
583+
584+
let mut value = int_part.parse::<u32>().ok()?;
585+
586+
if let Some(frac) = frac_part {
587+
let mut chars = frac.chars();
588+
let first_digit = chars.next()?.to_digit(10)?;
589+
590+
// Validate all remaining chars are digits
591+
let rest: Vec<_> = chars.collect();
592+
if !rest.iter().all(|c| c.is_ascii_digit()) {
593+
return None;
594+
}
595+
596+
match first_digit.cmp(&5) {
597+
Ordering::Greater => value += 1,
598+
Ordering::Equal => {
599+
// Check if any non-zero digit follows
600+
if rest.iter().any(|&c| c != '0') {
601+
value += 1;
602+
} else {
603+
// Banker's rounding: round to nearest even
604+
value += value & 1;
605+
}
606+
}
607+
Ordering::Less => {} // Round down, already validated
608+
}
609+
}
610+
611+
Some(value)
612+
}
613+
566614
fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
615+
// Reject invalid formats
616+
if arg != arg.trim_end()
617+
|| arg.trim().starts_with('-')
618+
|| arg.trim().starts_with("++")
619+
|| arg.contains('E')
620+
|| arg.contains('e')
621+
|| arg.matches('.').count() > 1
622+
{
623+
return None;
624+
}
625+
626+
let normalized = arg.trim().trim_start_matches('+');
627+
let normalized = normalized.strip_suffix('.').unwrap_or(normalized);
628+
let value = parse_baud_with_rounding(normalized)?;
629+
567630
// BSDs use a u32 for the baud rate, so any decimal number applies.
568631
#[cfg(any(
569632
target_os = "freebsd",
@@ -573,9 +636,7 @@ fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
573636
target_os = "netbsd",
574637
target_os = "openbsd"
575638
))]
576-
if let Ok(n) = arg.parse::<u32>() {
577-
return Some(AllFlags::Baud(n));
578-
}
639+
return Some(AllFlags::Baud(value));
579640

580641
#[cfg(not(any(
581642
target_os = "freebsd",
@@ -585,12 +646,14 @@ fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
585646
target_os = "netbsd",
586647
target_os = "openbsd"
587648
)))]
588-
for (text, baud_rate) in BAUD_RATES {
589-
if *text == arg {
590-
return Some(AllFlags::Baud(*baud_rate));
649+
{
650+
for (text, baud_rate) in BAUD_RATES {
651+
if text.parse::<u32>().ok() == Some(value) {
652+
return Some(AllFlags::Baud(*baud_rate));
653+
}
591654
}
655+
None
592656
}
593-
None
594657
}
595658

596659
/// return `Some(flag)` if the input is a valid flag, `None` if not

tests/by-util/test_stty.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,24 @@ fn invalid_baud_setting() {
194194
.args(&["ospeed", "995"])
195195
.fails()
196196
.stderr_contains("invalid ospeed '995'");
197+
198+
for speed in &[
199+
"9599..", "9600..", "9600.5.", "9600.50.", "9600.0.", "++9600", "0x2580", "96E2", "9600,0",
200+
"9600.0 ",
201+
] {
202+
new_ucmd!().args(&["ispeed", speed]).fails();
203+
}
204+
}
205+
206+
#[test]
207+
#[cfg(unix)]
208+
fn valid_baud_formats() {
209+
let (path, _controller, _replica) = pty_path();
210+
for speed in &[" +9600", "9600.49", "9600.50", "9599.51", " 9600."] {
211+
new_ucmd!()
212+
.args(&["--file", &path, "ispeed", speed])
213+
.succeeds();
214+
}
197215
}
198216

199217
#[test]

0 commit comments

Comments
 (0)