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>
This commit is contained in:
Lőrinc
2026-08-15 12:11:51 -07:00
parent 604d7e8fdd
commit 469b0e59a2
4 changed files with 18 additions and 6 deletions

View File

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

View File

@@ -6,7 +6,6 @@
#include <iterator>
#include <memory>
#include <regex>
#include <stdexcept>
#include <string>
@@ -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)

View File

@@ -98,6 +98,7 @@ struct ConstevalFormatString {
consteval ConstevalFormatString(const char* str) : fmt{str} { detail::CheckNumFormatSpecifiers<num_params>(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.

View File

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