Skip to content

Commit 59c3d7f

Browse files
committed
Adding comprehensive gnu suite baud parsing rules
1 parent d6eff24 commit 59c3d7f

File tree

2 files changed

+109
-4
lines changed

2 files changed

+109
-4
lines changed

src/uu/stty/src/stty.rs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,96 @@ fn string_to_combo(arg: &str) -> Option<&str> {
563563
.map(|_| arg)
564564
}
565565

566+
/// Parse and round a baud rate value using GNU stty's custom rounding algorithm.
567+
///
568+
/// Accepts decimal values with the following rounding rules:
569+
/// - If first digit after decimal > 5: round up
570+
/// - If first digit after decimal < 5: round down
571+
/// - If first digit after decimal == 5:
572+
/// - If followed by any non-zero digit: round up
573+
/// - If followed only by zeros (or nothing): banker's rounding (round to nearest even)
574+
///
575+
/// Examples: "9600.49" -> 9600, "9600.51" -> 9600, "9600.5" -> 9600 (even), "9601.5" -> 9602 (even)
576+
/// TODO: there are two special cases "exta" → B19200 and "extb" → B38400
577+
fn parse_baud_with_rounding(normalized: &str) -> Option<u32> {
578+
let mut chars = normalized.chars().peekable();
579+
let mut value = 0u32;
580+
581+
// Parse integer part
582+
while let Some(c) = chars.peek() {
583+
if c.is_ascii_digit() {
584+
value = value * 10 + (chars.next().unwrap() as u32 - '0' as u32);
585+
} else {
586+
break;
587+
}
588+
}
589+
590+
// Handle fractional part if present
591+
if let Some('.') = chars.next() {
592+
let d0 = chars.next()?;
593+
if !d0.is_ascii_digit() {
594+
return None;
595+
}
596+
let first_digit = d0 as u32 - '0' as u32;
597+
598+
if first_digit > 5 {
599+
value += 1;
600+
} else if first_digit == 5 {
601+
// Check if there are non-zero digits after the 5
602+
let mut has_nonzero = false;
603+
while let Some(c) = chars.next() {
604+
if !c.is_ascii_digit() {
605+
return None;
606+
}
607+
if c != '0' {
608+
has_nonzero = true;
609+
break;
610+
}
611+
}
612+
613+
if has_nonzero {
614+
value += 1;
615+
} else {
616+
// Banker's rounding: round to nearest even
617+
value += value & 1;
618+
}
619+
620+
// Consume remaining digits
621+
while let Some(c) = chars.next() {
622+
if !c.is_ascii_digit() {
623+
return None;
624+
}
625+
}
626+
} else {
627+
// first_digit < 5: round down, but still validate remaining digits
628+
while let Some(c) = chars.next() {
629+
if !c.is_ascii_digit() {
630+
return None;
631+
}
632+
}
633+
}
634+
} else if chars.peek().is_some() {
635+
// Non-digit character after integer part
636+
return None;
637+
}
638+
639+
Some(value)
640+
}
641+
566642
fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
643+
// Reject invalid formats
644+
if arg != arg.trim_end()
645+
|| arg.trim().starts_with('-')
646+
|| arg.trim().starts_with("++")
647+
|| arg.contains('E')
648+
|| arg.contains('e')
649+
{
650+
return None;
651+
}
652+
653+
let normalized = arg.trim().trim_start_matches('+');
654+
let value = parse_baud_with_rounding(normalized)?;
655+
567656
// BSDs use a u32 for the baud rate, so any decimal number applies.
568657
#[cfg(any(
569658
target_os = "freebsd",
@@ -573,9 +662,7 @@ fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
573662
target_os = "netbsd",
574663
target_os = "openbsd"
575664
))]
576-
if let Ok(n) = arg.parse::<u32>() {
577-
return Some(AllFlags::Baud(n));
578-
}
665+
return Some(AllFlags::Baud(value));
579666

580667
#[cfg(not(any(
581668
target_os = "freebsd",
@@ -586,7 +673,7 @@ fn string_to_baud(arg: &str) -> Option<AllFlags<'_>> {
586673
target_os = "openbsd"
587674
)))]
588675
for (text, baud_rate) in BAUD_RATES {
589-
if *text == arg {
676+
if text.parse::<u32>().ok() == Some(value) {
590677
return Some(AllFlags::Baud(*baud_rate));
591678
}
592679
}

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)