From 5709f540811f80fc138fb0e084a3e5dd633b1873 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Fri, 10 Jul 2026 18:13:27 -0500 Subject: [PATCH] Validate regular expressions before reading input --- meson.build | 9 ++++++++- scripts/test-pipe-regex-fallback.sh | 22 ++++++++++++++++++++++ src/Compiler.cpp | 23 +++++++++++++++++++++++ src/Options.cpp | 2 +- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100755 scripts/test-pipe-regex-fallback.sh diff --git a/meson.build b/meson.build index be418c8..8552b5b 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'sedpp', 'cpp', - version: '1.0.0', + version: '1.0.1', default_options: [ 'cpp_std=c++20', 'warning_level=3', @@ -24,3 +24,10 @@ test( depends: sed_exe, timeout: 240, ) + +test( + 'pipe-regex-fallback', + find_program('scripts/test-pipe-regex-fallback.sh'), + args: [sed_exe.full_path()], + depends: sed_exe, +) diff --git a/scripts/test-pipe-regex-fallback.sh b/scripts/test-pipe-regex-fallback.sh new file mode 100755 index 0000000..2205d0f --- /dev/null +++ b/scripts/test-pipe-regex-fallback.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -eu + +sed_bin=$1 + +# Limine uses this portability probe in gensyms.sh. The first sed must reject +# the unsupported word-boundary character classes without consuming the pipe, +# leaving the input available to the GNU-compatible fallback expression. +actual="$({ + printf '%s\n' \ + '00000000 l d .text discarded' \ + '00000010 g F .text retained' +} | ("$sed_bin" '/[[:<:]]d[[:>:]]/d' 2>/dev/null || + "$sed_bin" '/\bd\b/d'))" + +expected='00000010 g F .text retained' + +if [ "$actual" != "$expected" ]; then + printf 'expected: %s\nactual: %s\n' "$expected" "$actual" >&2 + exit 1 +fi diff --git a/src/Compiler.cpp b/src/Compiler.cpp index 884b8c2..2c3f76c 100644 --- a/src/Compiler.cpp +++ b/src/Compiler.cpp @@ -1,4 +1,5 @@ #include "sedpp/Compiler.h" +#include "sedpp/Regex.h" #include #include @@ -103,6 +104,28 @@ Program Compiler::compile(const std::vector &scripts) { if (braceDepth_ > 0) { throw std::runtime_error("-e expression #1, char 0: unmatched '{'"); } + + // Validate every literal regular expression before Runner opens or reads an + // input stream. Besides matching GNU sed's diagnostic timing, this matters + // for portability probes such as `(sed non_gnu_re || sed fallback_re)`: the + // rejected first expression must leave the shared pipe untouched so the + // fallback process can consume it. Empty expressions are runtime references + // to the previously used regex and therefore cannot be compiled here. + auto validateAddress = [this](const std::optional
&address) { + if (address && address->kind == AddressKind::Regex && + !address->text.empty()) { + Regex regex(address->text, options_); + } + }; + for (const Command &command : program_.commands) { + validateAddress(command.firstAddress); + validateAddress(command.secondAddress); + if (command.substitute && !command.substitute->pattern.empty()) { + Regex regex(command.substitute->pattern, options_, + command.substitute->ignoreCase, + command.substitute->multiline); + } + } return program_; } diff --git a/src/Options.cpp b/src/Options.cpp index ba00cbc..d662999 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -7,7 +7,7 @@ #include namespace sedpp { -int SEDPP_VERSION = 1; +constexpr const char *SEDPP_VERSION = "1.0.1"; void OptionParser::printVersion() { // Version output intentionally names this implementation, not GNU sed.