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