Note this changes the output format of -g. POSIX states that the -g format only needs to be compatible with the stty implementation that produced it. However we improve compatibility on Linux by only outputting the explicit speeds when they're not encoded in the control flags.
* NEWS: Mention the change in behavior. * src/stty.c (display_recoverable): Output the speeds if needed. (recover_mode): Read the speeds if present. * tests/stty/stty-save-speed.sh: Add a new test. * tests/local.mk: Reference the new test. Link: https://github.com/coreutils/coreutils/issues/384 --- NEWS | 4 ++ src/stty.c | 83 +++++++++++++++++++--------- tests/local.mk | 1 + tests/stty/stty-save-speed.sh | 101 ++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 26 deletions(-) create mode 100755 tests/stty/stty-save-speed.sh 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 <https://www.gnu.org/licenses/>. + +. "${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 -- 2.55.0
