From: Sylvestre Ledru <[email protected]> 'ptx -W "a*"' would never terminate. SKIP_SOMETHING advanced the cursor by the length of the match, which is zero when the word regexp matches the empty string, and no other value in the surrounding loop conditions changes. find_occurs_in_text already skips zero length matches, and a zero length --context-regexp match is rejected outright; only this macro lacked the forward progress guarantee.
* src/ptx.c (SKIP_SOMETHING): Advance by at least one byte on a zero length match, as the other branches of the macro already do. * tests/ptx/word-regex-loop.sh: New test. * tests/local.mk (all_tests): Reference it. * NEWS: Mention the fix. Link: https://github.com/coreutils/coreutils/pull/328 --- NEWS | 10 +++++++--- src/ptx.c | 5 ++++- tests/local.mk | 1 + tests/ptx/word-regex-loop.sh | 37 ++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100755 tests/ptx/word-regex-loop.sh diff --git a/NEWS b/NEWS index dbf6d2abb..67ffc1bf2 100644 --- a/NEWS +++ b/NEWS @@ -34,9 +34,13 @@ GNU coreutils NEWS -*- outline -*- like when processing large tab stops. [This bug was present in "the beginning".] - 'ptx -G' no longer hangs when the output width is smaller than twice the - gap size, as with 'ptx -G -w4', or when a long reference leaves that - little room, as with 'ptx -G -r'. + 'ptx -G' no longer loops forever when the output width is smaller than + twice the gap size, as with 'ptx -G -w4', or when a long reference + leaves that little room, as with 'ptx -G -r'. + [This bug was present in "the beginning".] + + 'ptx -W' no longer loops forever with a word regular expression that can + match the empty string, like: echo ab | ptx -W 'a*'. [This bug was present in "the beginning".] 'shred' no longer blocks when opening a FIFO that has no readers. diff --git a/src/ptx.c b/src/ptx.c index 6ef19ee15..4290d900c 100644 --- a/src/ptx.c +++ b/src/ptx.c @@ -176,6 +176,9 @@ static BLOCK *text_buffers; /* files to study */ while (cursor > start && isspace (to_uchar (cursor[-1]))) \ cursor-- +/* Always advance by at least one byte, lest a nullable word regexp + such as 'a*' match the empty string and loop forever. */ + #define SKIP_SOMETHING(cursor, limit) \ if (word_regex.string) \ { \ @@ -184,7 +187,7 @@ static BLOCK *text_buffers; /* files to study */ 0, NULL); \ if (count == -2) \ matcher_error (); \ - cursor += count == -1 ? 1 : count; \ + cursor += count <= 0 ? 1 : count; \ } \ else if (word_fastmap[to_uchar (*cursor)]) \ while (cursor < limit && word_fastmap[to_uchar (*cursor)]) \ diff --git a/tests/local.mk b/tests/local.mk index a002904a7..a6ee702e5 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -280,6 +280,7 @@ all_tests = \ tests/date/date.pl \ tests/date/date-next-dow.pl \ tests/ptx/ptx-overrun.sh \ + tests/ptx/word-regex-loop.sh \ tests/misc/xstrtol.pl \ tests/tail/overlay-headers.sh \ tests/tail/pid.sh \ diff --git a/tests/ptx/word-regex-loop.sh b/tests/ptx/word-regex-loop.sh new file mode 100755 index 000000000..6c89ae206 --- /dev/null +++ b/tests/ptx/word-regex-loop.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Ensure a nullable --word-regexp does not make ptx loop forever + +# 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_ ptx timeout + +# A nullable word regexp matching the empty string left the cursor +# unchanged in SKIP_SOMETHING, so define_all_fields() spun forever. + +echo 'aa bb cc' > in || framework_failure_ + +for re in 'a*' '[a-z]*' '[ab]*' '\(a\)*'; do + timeout 10 ptx -W "$re" in >/dev/null \ + || { warn_ "ptx -W '$re' failed or hung"; fail=1; } +done + +# Verify match semantics +timeout 10 ptx -W '[ab]*' in > out && +timeout 10 ptx -W '[ab]+' in > exp && +compare exp out || fail=1 + +Exit $fail -- 2.55.0
