diff --git a/src/banman.cpp b/src/banman.cpp index 2dbfc76df2d..2964a37de01 100644 --- a/src/banman.cpp +++ b/src/banman.cpp @@ -30,7 +30,7 @@ void BanMan::LoadBanlist() { LOCK(m_banned_mutex); - if (m_client_interface) m_client_interface->InitMessage(_("Loading banlist…").translated); + if (m_client_interface) m_client_interface->InitMessage(_("Loading banlist…")); const auto start{SteadyClock::now()}; if (m_ban_db.Read(m_banned)) { diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 5c5965245bc..1b2d97faff5 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -50,7 +50,7 @@ using util::ToString; // just use a plain system_clock. using CliClock = std::chrono::system_clock; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; static const char DEFAULT_RPCCONNECT[] = "127.0.0.1"; static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900; diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index a627c24e868..e2b3a3b5545 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -41,7 +41,7 @@ static bool fCreateBlank; static std::map registers; static const int CONTINUE_EXECUTION=-1; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; static void SetupBitcoinTxArgs(ArgsManager &argsman) { diff --git a/src/bitcoin-util.cpp b/src/bitcoin-util.cpp index ff2e4fb8005..10967bd573a 100644 --- a/src/bitcoin-util.cpp +++ b/src/bitcoin-util.cpp @@ -26,7 +26,7 @@ static const int CONTINUE_EXECUTION=-1; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; static void SetupBitcoinUtilArgs(ArgsManager &argsman) { diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index 033cf7a0f39..c37f9d29958 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -26,7 +26,7 @@ using util::Join; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; static void SetupWalletToolArgs(ArgsManager& argsman) { diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp index 5d5f06127d2..d210e2c8ba1 100644 --- a/src/bitcoind.cpp +++ b/src/bitcoind.cpp @@ -34,7 +34,7 @@ using node::NodeContext; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; #if HAVE_DECL_FORK diff --git a/src/clientversion.cpp b/src/clientversion.cpp index af58c3023f5..10e9472b84b 100644 --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -95,7 +95,7 @@ std::string LicenseInfo() strprintf(_("The source code is available from %s."), URL_SOURCE_CODE).translated + "\n" + "\n" + - _("This is experimental software.").translated + "\n" + + _("This is experimental software.") + "\n" + strprintf(_("Distributed under the MIT software license, see the accompanying file %s or %s"), "COPYING", "").translated + "\n"; } diff --git a/src/init.cpp b/src/init.cpp index cb4ff733a50..9887c071653 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1244,8 +1244,8 @@ static ChainstateLoadResult InitAndLoadChainstate( _("Error reading from database, shutting down."), "", CClientUIInterface::MSG_ERROR); }; - uiInterface.InitMessage(_("Loading block index…").translated); - auto catch_exceptions = [](auto&& f) { + uiInterface.InitMessage(_("Loading block index…")); + auto catch_exceptions = [](auto&& f) -> ChainstateLoadResult { try { return f(); } catch (const std::exception& e) { @@ -1255,7 +1255,7 @@ static ChainstateLoadResult InitAndLoadChainstate( }; auto [status, error] = catch_exceptions([&] { return LoadChainstate(chainman, cache_sizes, options); }); if (status == node::ChainstateLoadStatus::SUCCESS) { - uiInterface.InitMessage(_("Verifying blocks…").translated); + uiInterface.InitMessage(_("Verifying blocks…")); if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) { LogWarning("pruned datadir may not have more than %d blocks; only checking available blocks\n", MIN_BLOCKS_TO_KEEP); @@ -1418,7 +1418,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // Initialize addrman assert(!node.addrman); - uiInterface.InitMessage(_("Loading P2P addresses…").translated); + uiInterface.InitMessage(_("Loading P2P addresses…")); auto addrman{LoadAddrman(*node.netgroupman, args)}; if (!addrman) return InitError(util::ErrorString(addrman)); node.addrman = std::move(*addrman); @@ -1703,7 +1703,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (chainman.m_blockman.m_blockfiles_indexed) { LOCK(cs_main); for (Chainstate* chainstate : chainman.GetAll()) { - uiInterface.InitMessage(_("Pruning blockstore…").translated); + uiInterface.InitMessage(_("Pruning blockstore…")); chainstate->PruneAndFlush(); } } @@ -1999,7 +1999,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // ChainstateManager's active tip. SetRPCWarmupFinished(); - uiInterface.InitMessage(_("Done loading").translated); + uiInterface.InitMessage(_("Done loading")); for (const auto& client : node.chain_clients) { client->start(scheduler); diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index bb101ba186a..9096b6f71de 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -1,10 +1,11 @@ // Copyright (c) 2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include // Define G_TRANSLATION_FUN symbol in libbitcoinkernel library so users of the // library aren't required to export this symbol -extern const std::function G_TRANSLATION_FUN = nullptr; +extern const TranslateFn G_TRANSLATION_FUN{nullptr}; diff --git a/src/net.cpp b/src/net.cpp index 8ea7f6ce445..47ef2d22d7d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3296,7 +3296,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) } if (m_client_interface) { - m_client_interface->InitMessage(_("Starting network threads…").translated); + m_client_interface->InitMessage(_("Starting network threads…")); } fAddressesInitialized = true; diff --git a/src/qt/main.cpp b/src/qt/main.cpp index 16befd99e89..649fead901d 100644 --- a/src/qt/main.cpp +++ b/src/qt/main.cpp @@ -13,7 +13,7 @@ #include /** Translate string to current locale using Qt. */ -extern const std::function G_TRANSLATION_FUN = [](const char* psz) { +extern const TranslateFn G_TRANSLATION_FUN = [](const char* psz) { return QCoreApplication::translate("bitcoin-core", psz).toStdString(); }; diff --git a/src/test/fuzz/strprintf.cpp b/src/test/fuzz/strprintf.cpp index 18de0e1960d..c7f24fbdb42 100644 --- a/src/test/fuzz/strprintf.cpp +++ b/src/test/fuzz/strprintf.cpp @@ -14,11 +14,16 @@ #include #include +template +void fuzz_fmt(const std::string& fmt, const Args&... args) +{ + (void)tfm::format(tfm::RuntimeFormat{fmt}, args...); +} + FUZZ_TARGET(str_printf) { FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); const std::string format_string = fuzzed_data_provider.ConsumeRandomLengthString(64); - const bilingual_str bilingual_string{format_string, format_string}; const int digits_in_format_specifier = std::count_if(format_string.begin(), format_string.end(), IsDigit); @@ -52,28 +57,22 @@ FUZZ_TARGET(str_printf) CallOneOf( fuzzed_data_provider, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32)); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeRandomLengthString(32)); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32)); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32).c_str()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeRandomLengthString(32).c_str()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32).c_str()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeBool()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeBool()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeBool()); }); } catch (const tinyformat::format_error&) { } @@ -98,36 +97,28 @@ FUZZ_TARGET(str_printf) CallOneOf( fuzzed_data_provider, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeFloatingPoint()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeFloatingPoint()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeFloatingPoint()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeFloatingPoint()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeFloatingPoint()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeFloatingPoint()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }, [&] { - (void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral()); - (void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral()); + fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral()); }); } catch (const tinyformat::format_error&) { } diff --git a/src/test/fuzz/util/wallet.h b/src/test/fuzz/util/wallet.h index 8b55b7a985a..1f04b5dbf1a 100644 --- a/src/test/fuzz/util/wallet.h +++ b/src/test/fuzz/util/wallet.h @@ -46,7 +46,7 @@ struct FuzzedWallet { for (const std::string& desc_fmt : DESCS) { for (bool internal : {true, false}) { - const auto descriptor{(strprintf)(desc_fmt, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})}; + const auto descriptor{strprintf(tfm::RuntimeFormat{desc_fmt}, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})}; FlatSigningProvider keys; std::string error; diff --git a/src/test/translation_tests.cpp b/src/test/translation_tests.cpp index bda5dfd0994..4935830cdc2 100644 --- a/src/test/translation_tests.cpp +++ b/src/test/translation_tests.cpp @@ -9,13 +9,26 @@ BOOST_AUTO_TEST_SUITE(translation_tests) +static TranslateFn translate{[](const char * str) { return strprintf("t(%s)", str); }}; + +// Custom translation function _t(), similar to _() but internal to this test. +consteval auto _t(util::TranslatedLiteral str) +{ + str.translate_fn = &translate; + return str; +} + BOOST_AUTO_TEST_CASE(translation_namedparams) { bilingual_str arg{"original", "translated"}; - bilingual_str format{"original [%s]", "translated [%s]"}; - bilingual_str result{strprintf(format, arg)}; + bilingual_str result{strprintf(_t("original [%s]"), arg)}; BOOST_CHECK_EQUAL(result.original, "original [original]"); - BOOST_CHECK_EQUAL(result.translated, "translated [translated]"); + BOOST_CHECK_EQUAL(result.translated, "t(original [translated])"); + + util::TranslatedLiteral arg2{"original", &translate}; + bilingual_str result2{strprintf(_t("original [%s]"), arg2)}; + BOOST_CHECK_EQUAL(result2.original, "original [original]"); + BOOST_CHECK_EQUAL(result2.translated, "t(original [t(original)])"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 1ebc3464d84..c66e000306e 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -72,7 +72,7 @@ using node::LoadChainstate; using node::RegenerateCommitments; using node::VerifyLoadedChainstate; -const std::function G_TRANSLATION_FUN = nullptr; +const TranslateFn G_TRANSLATION_FUN{nullptr}; constexpr inline auto TEST_DIR_PATH_ELEMENT{"test_common bitcoin"}; // Includes a space to catch possible path escape issues. /** Random context to get unique temp data dirs. Separate from m_rng, which can be seeded from a const env var */ diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp index 340c650fe36..65ee140b6e9 100644 --- a/src/test/util_string_tests.cpp +++ b/src/test/util_string_tests.cpp @@ -16,7 +16,7 @@ template void TfmFormatZeroes(const std::string& fmt) { std::apply([&](auto... args) { - (void)tfm::format(fmt, args...); + (void)tfm::format(tfm::RuntimeFormat{fmt}, args...); }, std::array{}); } diff --git a/src/tinyformat.h b/src/tinyformat.h index 0d0e9149ccd..29b0f9e3ea3 100644 --- a/src/tinyformat.h +++ b/src/tinyformat.h @@ -142,6 +142,7 @@ namespace tfm = tinyformat; //------------------------------------------------------------------------------ // Implementation details. #include +#include // Added for Bitcoin Core #include #include #include // Added for Bitcoin Core @@ -179,13 +180,19 @@ namespace tfm = tinyformat; namespace tinyformat { +// Added for Bitcoin Core. Similar to std::runtime_format from C++26. +struct RuntimeFormat { + const std::string& fmt; // Not a string view, because tinyformat requires a c_str + explicit RuntimeFormat(LIFETIMEBOUND const std::string& str) : fmt{str} {} +}; + // Added for Bitcoin Core. Wrapper for checking format strings at compile time. -// Unlike ConstevalFormatString this supports std::string for runtime string -// formatting without compile time checks. +// Unlike ConstevalFormatString this supports RunTimeFormat-wrapped std::string +// for runtime string formatting without compile time checks. template struct FormatStringCheck { consteval FormatStringCheck(const char* str) : fmt{util::ConstevalFormatString{str}.fmt} {} - FormatStringCheck(const std::string& str) : fmt{str.c_str()} {} + FormatStringCheck(LIFETIMEBOUND const RuntimeFormat& run) : fmt{run.fmt.c_str()} {} FormatStringCheck(util::ConstevalFormatString str) : fmt{str.fmt} {} operator const char*() { return fmt; } const char* fmt; diff --git a/src/util/translation.h b/src/util/translation.h index 7c734a17663..cc29eef660c 100644 --- a/src/util/translation.h +++ b/src/util/translation.h @@ -6,12 +6,15 @@ #define BITCOIN_UTIL_TRANSLATION_H #include +#include +#include #include #include /** Translate a message to the native language of the user. */ -const extern std::function G_TRANSLATION_FUN; +using TranslateFn = std::function; +const extern TranslateFn G_TRANSLATION_FUN; /** * Bilingual messages: @@ -47,39 +50,61 @@ inline bilingual_str operator+(bilingual_str lhs, const bilingual_str& rhs) return lhs; } +namespace util { +//! Compile-time literal string that can be translated with an optional translation function. +struct TranslatedLiteral { + const char* const original; + const TranslateFn* translate_fn; + + consteval TranslatedLiteral(const char* str, const TranslateFn* fn = &G_TRANSLATION_FUN) : original{str}, translate_fn{fn} { assert(original); } + operator std::string() const { return translate_fn && *translate_fn ? (*translate_fn)(original) : original; } + operator bilingual_str() const { return {original, std::string{*this}}; } +}; + +// TranslatedLiteral operators for formatting and adding to strings. +inline std::ostream& operator<<(std::ostream& os, const TranslatedLiteral& lit) { return os << std::string{lit}; } +template +T operator+(const T& lhs, const TranslatedLiteral& rhs) { return lhs + static_cast(rhs); } +template +T operator+(const TranslatedLiteral& lhs, const T& rhs) { return static_cast(lhs) + rhs; } + +template +struct BilingualFmt { + const ConstevalFormatString original; + TranslatedLiteral lit; + consteval BilingualFmt(TranslatedLiteral l) : original{l.original}, lit{l} {} +}; +} // namespace util + +consteval auto _(util::TranslatedLiteral str) { return str; } + /** Mark a bilingual_str as untranslated */ inline bilingual_str Untranslated(std::string original) { return {original, original}; } -// Provide an overload of tinyformat::format which can take bilingual_str arguments. +// Provide an overload of tinyformat::format for BilingualFmt format strings and bilingual_str or TranslatedLiteral args. namespace tinyformat { template -bilingual_str format(const bilingual_str& fmt, const Args&... args) +bilingual_str format(util::BilingualFmt fmt, const Args&... args) { - const auto translate_arg{[](const auto& arg, bool translated) -> const auto& { + const auto original_arg{[](const auto& arg) -> const auto& { if constexpr (std::is_same_v) { - return translated ? arg.translated : arg.original; + return arg.original; + } else if constexpr (std::is_same_v) { + return arg.original; } else { return arg; } }}; - return bilingual_str{tfm::format(fmt.original, translate_arg(args, false)...), - tfm::format(fmt.translated, translate_arg(args, true)...)}; + const auto translated_arg{[](const auto& arg) -> const auto& { + if constexpr (std::is_same_v) { + return arg.translated; + } else { + return arg; + } + }}; + return bilingual_str{tfm::format(fmt.original, original_arg(args)...), + tfm::format(RuntimeFormat{std::string{fmt.lit}}, translated_arg(args)...)}; } } // namespace tinyformat -struct ConstevalStringLiteral { - const char* const lit; - consteval ConstevalStringLiteral(const char* str) : lit{str} {} - consteval ConstevalStringLiteral(std::nullptr_t) = delete; -}; - -/** - * Translation function. - * If no translation function is set, simply return the input. - */ -inline bilingual_str _(ConstevalStringLiteral str) -{ - return bilingual_str{str.lit, G_TRANSLATION_FUN ? (G_TRANSLATION_FUN)(str.lit) : str.lit}; -} - #endif // BITCOIN_UTIL_TRANSLATION_H diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 2b5c021cda4..e77999b1115 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -52,7 +52,7 @@ bool VerifyWallets(WalletContext& context) LogPrintf("Using wallet directory %s\n", fs::PathToString(GetWalletDir())); - chain.initMessage(_("Verifying wallet(s)…").translated); + chain.initMessage(_("Verifying wallet(s)…")); // For backwards compatibility if an unnamed top level wallet exists in the // wallets directory, include it in the default list of wallets to load. @@ -135,7 +135,7 @@ bool LoadWallets(WalletContext& context) if (!database && status == DatabaseStatus::FAILED_NOT_FOUND) { continue; } - chain.initMessage(_("Loading wallet…").translated); + chain.initMessage(_("Loading wallet…")); std::shared_ptr pwallet = database ? CWallet::Create(context, name, std::move(database), options.create_flags, error, warnings) : nullptr; if (!warnings.empty()) chain.initWarning(Join(warnings, Untranslated("\n"))); if (!pwallet) { diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index bfd7249c046..79f1a40ec5a 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -534,7 +534,7 @@ RPCHelpMan importwallet() // Use uiInterface.ShowProgress instead of pwallet.ShowProgress because pwallet.ShowProgress has a cancel button tied to AbortRescan which // we don't want for this progress bar showing the import progress. uiInterface.ShowProgress does not have a cancel button. - pwallet->chain().showProgress(strprintf("%s %s", pwallet->GetDisplayName(), _("Importing…").translated), 0, false); // show progress dialog in GUI + pwallet->chain().showProgress(strprintf("%s %s", pwallet->GetDisplayName(), _("Importing…")), 0, false); // show progress dialog in GUI std::vector> keys; std::vector> scripts; while (file.good()) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e7e06aec01a..ea59f1a9d57 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -280,7 +280,7 @@ std::shared_ptr LoadWalletInternal(WalletContext& context, const std::s return nullptr; } - context.chain->initMessage(_("Loading wallet…").translated); + context.chain->initMessage(_("Loading wallet…")); std::shared_ptr wallet = CWallet::Create(context, name, std::move(database), options.create_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet loading failed.") + Untranslated(" ") + error; @@ -430,7 +430,7 @@ std::shared_ptr CreateWallet(WalletContext& context, const std::string& } // Make the wallet - context.chain->initMessage(_("Loading wallet…").translated); + context.chain->initMessage(_("Loading wallet…")); std::shared_ptr wallet = CWallet::Create(context, name, std::move(database), wallet_creation_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet creation failed.") + Untranslated(" ") + error; @@ -1903,7 +1903,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks"); fAbortRescan = false; - ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…").translated), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) + ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…")), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash()); uint256 end_hash = tip_hash; if (max_height) chain().findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash)); @@ -1918,7 +1918,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc m_scanning_progress = 0; } if (block_height % 100 == 0 && progress_end - progress_begin > 0.0) { - ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…").translated), std::max(1, std::min(99, (int)(m_scanning_progress * 100)))); + ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…")), std::max(1, std::min(99, (int)(m_scanning_progress * 100)))); } bool next_interval = reserver.now() >= current_time + INTERVAL_TIME; @@ -2015,7 +2015,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc WalletLogPrintf("Scanning current mempool transactions.\n"); WITH_LOCK(cs_wallet, chain().requestMempoolTransactions(*this)); } - ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…").translated), 100); // hide progress dialog in GUI + ShowProgress(strprintf("%s %s", GetDisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI if (block_height && fAbortRescan) { WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current); result.status = ScanResult::USER_ABORT; @@ -3353,7 +3353,7 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf } } - chain.initMessage(_("Rescanning…").translated); + chain.initMessage(_("Rescanning…")); walletInstance->WalletLogPrintf("Rescanning last %i blocks (from block %i)...\n", *tip_height - rescan_height, rescan_height); { diff --git a/test/lint/lint-format-strings.py b/test/lint/lint-format-strings.py deleted file mode 100755 index 86a17fb0f8c..00000000000 --- a/test/lint/lint-format-strings.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2018-2022 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -# - -""" -Lint format strings: This program checks that the number of arguments passed -to a variadic format string function matches the number of format specifiers -in the format string. -""" - -import subprocess -import re -import sys - -FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [ - 'tfm::format,1', # Assuming tfm::::format(std::ostream&, ... - 'strprintf,0', -] -RUN_LINT_FILE = 'test/lint/run-lint-format-strings.py' - -def check_doctest(): - command = [ - sys.executable, - '-m', - 'doctest', - RUN_LINT_FILE, - ] - try: - subprocess.run(command, check = True) - except subprocess.CalledProcessError: - sys.exit(1) - -def get_matching_files(function_name): - command = [ - 'git', - 'grep', - '--full-name', - '-l', - function_name, - '--', - '*.c', - '*.cpp', - '*.h', - ] - try: - return subprocess.check_output(command, stderr = subprocess.STDOUT).decode('utf-8').splitlines() - except subprocess.CalledProcessError as e: - if e.returncode > 1: # return code is 1 when match is empty - print(e.output.decode('utf-8'), end='') - sys.exit(1) - return [] - -def main(): - exit_code = 0 - check_doctest() - for s in FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS: - function_name, skip_arguments = s.split(',') - matching_files = get_matching_files(function_name) - - matching_files_filtered = [] - for matching_file in matching_files: - if not re.search('^src/(leveldb|secp256k1|minisketch|tinyformat|test/fuzz/strprintf.cpp)', matching_file): - matching_files_filtered.append(matching_file) - matching_files_filtered.sort() - - run_lint_args = [ - RUN_LINT_FILE, - '--skip-arguments', - skip_arguments, - function_name, - ] - run_lint_args.extend(matching_files_filtered) - - try: - subprocess.run(run_lint_args, check = True) - except subprocess.CalledProcessError: - exit_code = 1 - - sys.exit(exit_code) - -if __name__ == '__main__': - main() diff --git a/test/lint/run-lint-format-strings.py b/test/lint/run-lint-format-strings.py deleted file mode 100755 index 0e08c9f974e..00000000000 --- a/test/lint/run-lint-format-strings.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2018-2022 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -# -# Lint format strings: This program checks that the number of arguments passed -# to a variadic format string function matches the number of format specifiers -# in the format string. - -import argparse -import re -import sys - -FALSE_POSITIVES = [ - ("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION)"), - ("src/test/translation_tests.cpp", "strprintf(format, arg)"), - ("src/test/util_string_tests.cpp", 'tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi")'), - ("src/test/util_string_tests.cpp", 'tfm::format(ConstevalFormatString<2>{"%.*s"}, "hi", "hi")'), -] - - -def parse_function_calls(function_name, source_code): - """Return an array with all calls to function function_name in string source_code. - Preprocessor directives and C++ style comments ("//") in source_code are removed. - - >>> len(parse_function_calls("foo", "foo();bar();foo();bar();")) - 2 - >>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[0].startswith("foo(1);") - True - >>> parse_function_calls("foo", "foo(1);bar(1);foo(2);bar(2);")[1].startswith("foo(2);") - True - >>> len(parse_function_calls("foo", "foo();bar();// foo();bar();")) - 1 - >>> len(parse_function_calls("foo", "#define FOO foo();")) - 0 - """ - assert type(function_name) is str and type(source_code) is str and function_name - lines = [re.sub("// .*", " ", line).strip() - for line in source_code.split("\n") - if not line.strip().startswith("#")] - return re.findall(r"[^a-zA-Z_](?=({}\(.*).*)".format(function_name), " " + " ".join(lines)) - - -def normalize(s): - """Return a normalized version of string s with newlines, tabs and C style comments ("/* ... */") - replaced with spaces. Multiple spaces are replaced with a single space. - - >>> normalize(" /* nothing */ foo\tfoo /* bar */ foo ") - 'foo foo foo' - """ - assert type(s) is str - s = s.replace("\n", " ") - s = s.replace("\t", " ") - s = re.sub(r"/\*.*?\*/", " ", s) - s = re.sub(" {2,}", " ", s) - return s.strip() - - -ESCAPE_MAP = { - r"\n": "[escaped-newline]", - r"\t": "[escaped-tab]", - r'\"': "[escaped-quote]", -} - - -def escape(s): - """Return the escaped version of string s with "\\\"", "\\n" and "\\t" escaped as - "[escaped-backslash]", "[escaped-newline]" and "[escaped-tab]". - - >>> unescape(escape("foo")) == "foo" - True - >>> escape(r'foo \\t foo \\n foo \\\\ foo \\ foo \\"bar\\"') - 'foo [escaped-tab] foo [escaped-newline] foo \\\\\\\\ foo \\\\ foo [escaped-quote]bar[escaped-quote]' - """ - assert type(s) is str - for raw_value, escaped_value in ESCAPE_MAP.items(): - s = s.replace(raw_value, escaped_value) - return s - - -def unescape(s): - """Return the unescaped version of escaped string s. - Reverses the replacements made in function escape(s). - - >>> unescape(escape("bar")) - 'bar' - >>> unescape("foo [escaped-tab] foo [escaped-newline] foo \\\\\\\\ foo \\\\ foo [escaped-quote]bar[escaped-quote]") - 'foo \\\\t foo \\\\n foo \\\\\\\\ foo \\\\ foo \\\\"bar\\\\"' - """ - assert type(s) is str - for raw_value, escaped_value in ESCAPE_MAP.items(): - s = s.replace(escaped_value, raw_value) - return s - - -def parse_function_call_and_arguments(function_name, function_call): - """Split string function_call into an array of strings consisting of: - * the string function_call followed by "(" - * the function call argument #1 - * ... - * the function call argument #n - * a trailing ");" - - The strings returned are in escaped form. See escape(...). - - >>> parse_function_call_and_arguments("foo", 'foo("%s", "foo");') - ['foo(', '"%s",', ' "foo"', ')'] - >>> parse_function_call_and_arguments("foo", 'foo("%s", "foo");') - ['foo(', '"%s",', ' "foo"', ')'] - >>> parse_function_call_and_arguments("foo", 'foo("%s %s", "foo", "bar");') - ['foo(', '"%s %s",', ' "foo",', ' "bar"', ')'] - >>> parse_function_call_and_arguments("fooprintf", 'fooprintf("%050d", i);') - ['fooprintf(', '"%050d",', ' i', ')'] - >>> parse_function_call_and_arguments("foo", 'foo(bar(foobar(barfoo("foo"))), foobar); barfoo') - ['foo(', 'bar(foobar(barfoo("foo"))),', ' foobar', ')'] - >>> parse_function_call_and_arguments("foo", "foo()") - ['foo(', '', ')'] - >>> parse_function_call_and_arguments("foo", "foo(123)") - ['foo(', '123', ')'] - >>> parse_function_call_and_arguments("foo", 'foo("foo")') - ['foo(', '"foo"', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", std::wstring_convert,wchar_t>().to_bytes(buf), err);') - ['strprintf(', '"%s (%d)",', ' std::wstring_convert,wchar_t>().to_bytes(buf),', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo().to_bytes(buf), err);') - ['strprintf(', '"%s (%d)",', ' foo().to_bytes(buf),', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo().to_bytes(buf), err);') - ['strprintf(', '"%s (%d)",', ' foo().to_bytes(buf),', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo << 1, err);') - ['strprintf(', '"%s (%d)",', ' foo << 1,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo() >> 1, err);') - ['strprintf(', '"%s (%d)",', ' foo() >> 1,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo < 1 ? bar : foobar, err);') - ['strprintf(', '"%s (%d)",', ' foo < 1 ? bar : foobar,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo < 1, err);') - ['strprintf(', '"%s (%d)",', ' foo < 1,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo > 1 ? bar : foobar, err);') - ['strprintf(', '"%s (%d)",', ' foo > 1 ? bar : foobar,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo > 1, err);') - ['strprintf(', '"%s (%d)",', ' foo > 1,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo <= 1, err);') - ['strprintf(', '"%s (%d)",', ' foo <= 1,', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo <= bar<1, 2>(1, 2), err);') - ['strprintf(', '"%s (%d)",', ' foo <= bar<1, 2>(1, 2),', ' err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2)?bar:foobar,err)'); - ['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2)?bar:foobar,', 'err', ')'] - >>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo>foo<1,2>(1,2),err)'); - ['strprintf(', '"%s (%d)",', ' foo>foo<1,2>(1,2),', 'err', ')'] - """ - assert type(function_name) is str and type(function_call) is str and function_name - remaining = normalize(escape(function_call)) - expected_function_call = "{}(".format(function_name) - assert remaining.startswith(expected_function_call) - parts = [expected_function_call] - remaining = remaining[len(expected_function_call):] - open_parentheses = 1 - open_template_arguments = 0 - in_string = False - parts.append("") - for i, char in enumerate(remaining): - parts.append(parts.pop() + char) - if char == "\"": - in_string = not in_string - continue - if in_string: - continue - if char == "(": - open_parentheses += 1 - continue - if char == ")": - open_parentheses -= 1 - if open_parentheses > 1: - continue - if open_parentheses == 0: - parts.append(parts.pop()[:-1]) - parts.append(char) - break - prev_char = remaining[i - 1] if i - 1 >= 0 else None - next_char = remaining[i + 1] if i + 1 <= len(remaining) - 1 else None - if char == "<" and next_char not in [" ", "<", "="] and prev_char not in [" ", "<"]: - open_template_arguments += 1 - continue - if char == ">" and next_char not in [" ", ">", "="] and prev_char not in [" ", ">"] and open_template_arguments > 0: - open_template_arguments -= 1 - if open_template_arguments > 0: - continue - if char == ",": - parts.append("") - return parts - - -def parse_string_content(argument): - """Return the text within quotes in string argument. - - >>> parse_string_content('1 "foo %d bar" 2') - 'foo %d bar' - >>> parse_string_content('1 foobar 2') - '' - >>> parse_string_content('1 "bar" 2') - 'bar' - >>> parse_string_content('1 "foo" 2 "bar" 3') - 'foobar' - >>> parse_string_content('1 "foo" 2 " " "bar" 3') - 'foo bar' - >>> parse_string_content('""') - '' - >>> parse_string_content('') - '' - >>> parse_string_content('1 2 3') - '' - """ - assert type(argument) is str - string_content = "" - in_string = False - for char in normalize(escape(argument)): - if char == "\"": - in_string = not in_string - elif in_string: - string_content += char - return string_content - - -def count_format_specifiers(format_string): - """Return the number of format specifiers in string format_string. - - >>> count_format_specifiers("foo bar foo") - 0 - >>> count_format_specifiers("foo %d bar foo") - 1 - >>> count_format_specifiers("foo %d bar %i foo") - 2 - >>> count_format_specifiers("foo %d bar %i foo %% foo") - 2 - >>> count_format_specifiers("foo %d bar %i foo %% foo %d foo") - 3 - >>> count_format_specifiers("foo %d bar %i foo %% foo %*d foo") - 4 - >>> count_format_specifiers("foo %5$d") - 5 - >>> count_format_specifiers("foo %5$*7$d") - 7 - """ - assert type(format_string) is str - format_string = format_string.replace('%%', 'X') - n = max_pos = 0 - for m in re.finditer("%(.*?)[aAcdeEfFgGinopsuxX]", format_string, re.DOTALL): - # Increase the max position if the argument has a position number like - # "5$", otherwise increment the argument count. - pos_num, = re.match(r"(?:(^\d+)\$)?", m.group(1)).groups() - if pos_num is not None: - max_pos = max(max_pos, int(pos_num)) - else: - n += 1 - - # Increase the max position if there is a "*" width argument with a - # position like "*7$", and increment the argument count if there is a - # "*" width argument with no position. - star, star_pos_num = re.match(r"(?:.*?(\*(?:(\d+)\$)?)|)", m.group(1)).groups() - if star_pos_num is not None: - max_pos = max(max_pos, int(star_pos_num)) - elif star is not None: - n += 1 - return max(n, max_pos) - - -def main(): - parser = argparse.ArgumentParser(description="This program checks that the number of arguments passed " - "to a variadic format string function matches the number of format " - "specifiers in the format string.") - parser.add_argument("--skip-arguments", type=int, help="number of arguments before the format string " - "argument (e.g. 1 in the case of fprintf)", default=0) - parser.add_argument("function_name", help="function name (e.g. fprintf)", default=None) - parser.add_argument("file", nargs="*", help="C++ source code file (e.g. foo.cpp)") - args = parser.parse_args() - exit_code = 0 - for filename in args.file: - with open(filename, "r", encoding="utf-8") as f: - for function_call_str in parse_function_calls(args.function_name, f.read()): - parts = parse_function_call_and_arguments(args.function_name, function_call_str) - relevant_function_call_str = unescape("".join(parts))[:512] - if (f.name, relevant_function_call_str) in FALSE_POSITIVES: - continue - if len(parts) < 3 + args.skip_arguments: - exit_code = 1 - print("{}: Could not parse function call string \"{}(...)\": {}".format(f.name, args.function_name, relevant_function_call_str)) - continue - argument_count = len(parts) - 3 - args.skip_arguments - format_str = parse_string_content(parts[1 + args.skip_arguments]) - format_specifier_count = count_format_specifiers(format_str) - if format_specifier_count != argument_count: - exit_code = 1 - print("{}: Expected {} argument(s) after format string but found {} argument(s): {}".format(f.name, format_specifier_count, argument_count, relevant_function_call_str)) - continue - sys.exit(exit_code) - - -if __name__ == "__main__": - main()