1
0
mirror of https://git.savannah.gnu.org/git/coreutils.git synced 2026-10-24 02:08:57 +03:00

stty: support save/restore of speeds not encoded in control flags

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
This commit is contained in:
Pádraig Brady
2026-10-20 15:07:19 +01:00
parent 6800b46c52
commit 60e7bf4121
4 changed files with 163 additions and 26 deletions

4
NEWS
View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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 \

101
tests/stty/stty-save-speed.sh Executable file
View File

@@ -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