diff --git a/NEWS b/NEWS index d6c824f50..49c2b29d2 100644 --- a/NEWS +++ b/NEWS @@ -87,6 +87,10 @@ GNU coreutils NEWS -*- outline -*- whole width of a previous character, unless the --characters option is used. This aligns with POSIX and terminal behavior. + 'stty -g' now saves input and output speeds explicitly on systems + that store them separately from the control flags, such as BSD, + or on GNU/Linux with non-standard speeds. + ** Improvements 'basenc --base58' encoding speed is 20x faster when built without libgmp. diff --git a/src/stty.c b/src/stty.c index d7f12252c..b02bcc457 100644 --- a/src/stty.c +++ b/src/stty.c @@ -2156,35 +2156,45 @@ display_recoverable (struct termios *mode) (unsigned long int) mode->c_lflag); for (size_t i = 0; i < NCCS; ++i) printf (":%lx", (unsigned long int) mode->c_cc[i]); + + /* Without CBAUD, speeds are stored separately from c_cflag (e.g., + on BSD). Otherwise preserve the old format unless BOTHER is used. */ + bool separate_speeds = true; +#ifdef CBAUD + separate_speeds = false; +# ifdef BOTHER + separate_speeds = (mode->c_cflag & CBAUD) == BOTHER; +# if defined CIBAUD && defined IBSHIFT + separate_speeds |= ((mode->c_cflag & CIBAUD) >> IBSHIFT) == BOTHER; +# endif +# endif +#endif + if (separate_speeds) + printf (":%lx:%lx", (unsigned long int) cfgetispeed (mode), + (unsigned long int) cfgetospeed (mode)); + putchar ('\n'); } -/* NOTE: identical to below, modulo use of tcflag_t */ -static int -strtoul_tcflag_t (char const *s, int base, char **p, tcflag_t *result, - char delim) -{ - unsigned long ul; - errno = 0; - ul = strtoul (s, p, base); - if (errno || **p != delim || *p == s || (tcflag_t) ul != ul) - return -1; - *result = ul; - return 0; -} +/* Define a strtoul wrapper that checks the delimiter and result range. */ +#define STRTOUL_TYPE(TYPE) \ + static int \ + strtoul_##TYPE (char const *s, int base, char **p, TYPE *result, \ + char delim) \ + { \ + unsigned long ul; \ + errno = 0; \ + ul = strtoul (s, p, base); \ + if (errno || **p != delim || *p == s || (TYPE) ul != ul) \ + return -1; \ + *result = ul; \ + return 0; \ + } -/* NOTE: identical to above, modulo use of cc_t */ -static int -strtoul_cc_t (char const *s, int base, char **p, cc_t *result, char delim) -{ - unsigned long ul; - errno = 0; - ul = strtoul (s, p, base); - if (errno || **p != delim || *p == s || (cc_t) ul != ul) - return -1; - *result = ul; - return 0; -} +STRTOUL_TYPE (tcflag_t) +STRTOUL_TYPE (cc_t) +STRTOUL_TYPE (speed_t) +#undef STRTOUL_TYPE /* Parse the output of display_recoverable. Return false if any part of it is invalid. */ @@ -2208,12 +2218,33 @@ recover_mode (char const *arg, struct termios *mode) for (size_t i = 0; i < NCCS; ++i) { char *p; - char delim = i < NCCS - 1 ? ':' : '\0'; + char delim = i < NCCS - 1 || strchr (s, ':') ? ':' : '\0'; if (strtoul_cc_t (s, 16, &p, mode->c_cc + i, delim) != 0) return false; + /* Accept the old format, which has no separate speed fields. */ + if (! *p) + return true; s = p + 1; } + speed_t speed[2]; + for (size_t i = 0; i < 2; i++) + { + char *p; + char delim = i == 0 ? ':' : '\0'; + if (strtoul_speed_t (s, 16, &p, speed + i, delim) != 0) + return false; + s = p + (i == 0); + } + + /* Set output first, since an input speed of zero can mean to use + the output speed. */ + if (cfsetospeed (mode, speed[1]) != 0 + || cfsetispeed (mode, speed[0]) != 0) + return false; + + last_ibaud.baud = speed[0] == 0 ? cfgetispeed (mode) : speed[0]; + last_obaud.baud = speed[1]; return true; } diff --git a/tests/local.mk b/tests/local.mk index e8927a014..7d75b4b5a 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -504,6 +504,7 @@ all_tests = \ tests/stty/stty-invalid.sh \ tests/stty/stty-pairs.sh \ tests/stty/stty-row-col.sh \ + tests/stty/stty-save-speed.sh \ tests/cksum/sum.pl \ tests/cksum/sum-sysv.sh \ tests/misc/sync.sh \ diff --git a/tests/stty/stty-save-speed.sh b/tests/stty/stty-save-speed.sh new file mode 100755 index 000000000..202500849 --- /dev/null +++ b/tests/stty/stty-save-speed.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# Ensure saved terminal settings restore input and output speeds. + +# Copyright (C) 2026 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ stty +getlimits_ +require_controlling_input_terminal_ +require_trap_signame_ + +# Linux encodes standard rates in c_cflag; BSD systems store all speeds +# separately. Limit the format checks to these known layouts. +case $(uname -s) in + Linux) separate_speeds=no;; + FreeBSD | NetBSD | OpenBSD | DragonFly | Darwin) separate_speeds=yes;; + *) skip_ 'requires Linux or BSD terminal speed layout';; +esac + +trap '' TTOU # Ignore SIGTTOU + +saved_state=$(stty -g) || framework_failure_ +cleanup_ () { stty "$saved_state"; } + +# Standard rates use the same number of fields regardless of their value. +stty 9600 || framework_failure_ +state9600=$(stty -g) || framework_failure_ +fields=$(echo "$state9600" | tr -cd : | wc -c) || framework_failure_ +stty 19200 || fail=1 +state=$(stty -g) || framework_failure_ +test "$(echo "$state" | tr -cd : | wc -c)" = "$fields" || fail=1 +stty "$state" || fail=1 + +# On BSD, verify standard rates are restored after a speed change too. +# Retain an old-format string to test backward compatibility below. +legacy=$state9600 +if test "$separate_speeds" = yes; then + stty "$state9600" || fail=1 + stty speed > out || fail=1 + echo 9600 > exp || framework_failure_ + compare exp out || fail=1 + + # Strip the last 2 fields + fields=$((fields - 2)) + legacy=$(echo "$legacy" | cut -d: -f1-$(($fields + 1))) +fi + +# Arbitrary rates require two additional fields. +for speeds in '51 51' '51 9600' '9600 51'; do + set -- $speeds + + stty ospeed "$2" ispeed "$1" 2>/dev/null || continue + stty speed > exp || fail=1 + state=$(stty -g) || framework_failure_ + + # Ensure additional fields + test "$(echo "$state" | tr -cd : | wc -c)" = $(($fields + 2)) || fail=1 + + # Ensure speed restored from state + stty 9600 || fail=1 + stty "$state" || fail=1 + stty speed > out || fail=1 + compare exp out || fail=1 + + # The last specified speed takes precedence + stty 19200 "$state" || fail=1 + stty speed > out || fail=1 + compare exp out || fail=1 + stty "$state" 9600 || fail=1 + stty speed > out || fail=1 + echo 9600 > exp || framework_failure_ + compare exp out || fail=1 +done + +# Reset to basic +stty 9600 || fail=1 +stty "$legacy" || fail=1 + +# Test various invalid speeds. +# Both speed fields must be present, valid, and in range. +# Append a 0 (multiply by 16) to ULONG_MAX to ensure overflow. +overflow=$(printf '%x0' "$ULONG_MAX") || framework_failure_ +for suffix in '' : 0 0: :0 0:0:0 "$overflow:0" \ + "0:$overflow" xyz:0 0:xyz; do + returns_ 1 stty "$legacy:$suffix" 2>/dev/null || fail=1 +done + +Exit $fail