From 469b0e59a29ac48364c20268feeb4477b8ec9192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 15 Aug 2026 12:11:51 -0700 Subject: [PATCH] util: make `ReplaceAll` literal `ReplaceAll()` substitutes fixed tokens in notification commands and other strings. PR #25803 replaced the Boost helper with `std::regex_replace()`, treating searches as regular expressions and substitutes as replacement-format syntax. Restore literal, non-recursive replacement so callers match fixed tokens and preserve replacement bytes exactly, while avoiding a new string when the search text is absent. Co-authored-by: Rob Hamilton <6456095+Rob1Ham@users.noreply.github.com> --- src/test/util_tests.cpp | 4 ++-- src/util/string.cpp | 15 +++++++++++++-- src/util/string.h | 1 + test/functional/feature_notifications.py | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 2ce1ef6463e..a7ba38b6d04 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -311,8 +311,8 @@ BOOST_AUTO_TEST_CASE(util_ReplaceAll) test_replaceall(original, "%s", "foo", "A test \"foo\" string 'foo'."); test_replaceall(original, "\"", "foo", "A test foo%sfoo string '%s'."); test_replaceall(original, "'", "foo", "A test \"%s\" string foo%sfoo."); - test_replaceall("a.b", ".", "x", "xxx"); // TODO: Search text must remain literal. - test_replaceall("%w and %w", "%w", "$&$`$'$1$$", "%w and %w$ and %w and $"); // TODO: Replacement bytes must remain literal. + test_replaceall("a.b", ".", "x", "axb"); + test_replaceall("%w and %w", "%w", "$&$`$'$1$$", "$&$`$'$1$$ and $&$`$'$1$$"); test_replaceall("x", "x", "xx", "xx"); } diff --git a/src/util/string.cpp b/src/util/string.cpp index d9d59ef5717..c197076715d 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -14,7 +13,19 @@ namespace util { void ReplaceAll(std::string& in_out, const std::string& search, const std::string& substitute) { if (search.empty()) return; - in_out = std::regex_replace(in_out, std::regex(search), substitute); + auto pos{in_out.find(search)}; + if (pos == std::string::npos) return; + + // Build separately because repeated std::string::replace() calls move the remaining suffix when sizes differ + std::string result; + result.reserve(in_out.size()); + std::string::size_type start{0}; + for (; pos != std::string::npos; pos = in_out.find(search, start)) { + result.append(in_out, start, pos - start).append(substitute); + start = pos + search.size(); + } + result.append(in_out, start); + in_out.swap(result); } LineReader::LineReader(std::string_view str, size_t max_line_length) diff --git a/src/util/string.h b/src/util/string.h index c984960a5d6..0963c12d698 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -98,6 +98,7 @@ struct ConstevalFormatString { consteval ConstevalFormatString(const char* str) : fmt{str} { detail::CheckNumFormatSpecifiers(fmt); } }; +/// Replace every non-overlapping occurrence of `search` with `substitute`, treating both literally; the replacement text is not searched again. void ReplaceAll(std::string& in_out, const std::string& search, const std::string& substitute); /** Split a string on any char found in separators, returning a vector. diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index fb573d3af98..11d22356895 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -186,8 +186,8 @@ class NotificationsTest(BitcoinTestFramework): self.sync_mempools() notify_path = os.path.join(self.walletnotify_dir, notify_outputname(wallet_name, txid)) self.wait_until(lambda: os.path.exists(command_marker) or os.path.exists(notify_path), timeout=10) - assert os.path.exists(command_marker) # TODO: Wallet names must not inject shell commands. - assert not os.path.exists(notify_path) # TODO: Wallet names must remain literal in notification paths. + assert not os.path.exists(command_marker) + assert os.path.exists(notify_path) self.log.info("test -alertnotify with large work invalid chain") # create a bunch of invalid blocks