mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-02 14:37:42 -03:00
Merge bitcoin/bitcoin#31061: refactor: Check translatable format strings at compile-time
Some checks failed
CI / test each commit (push) Has been cancelled
CI / macOS 14 native, arm64, no depends, sqlite only, gui (push) Has been cancelled
CI / macOS 14 native, arm64, fuzz (push) Has been cancelled
CI / Win64 native, VS 2022 (push) Has been cancelled
CI / Win64 native fuzz, VS 2022 (push) Has been cancelled
CI / ASan + LSan + UBSan + integer, no depends, USDT (push) Has been cancelled
Some checks failed
CI / test each commit (push) Has been cancelled
CI / macOS 14 native, arm64, no depends, sqlite only, gui (push) Has been cancelled
CI / macOS 14 native, arm64, fuzz (push) Has been cancelled
CI / Win64 native, VS 2022 (push) Has been cancelled
CI / Win64 native fuzz, VS 2022 (push) Has been cancelled
CI / ASan + LSan + UBSan + integer, no depends, USDT (push) Has been cancelled
fa3efb5729
refactor: Introduce struct to hold a runtime format string (MarcoFalke)fa6adb0134
lint: Remove unused and broken format string linter (MarcoFalke)fadc6b9bac
refactor: Check translatable format strings at compile-time (MarcoFalke)fa1d5acb8d
refactor: Use TranslateFn type consistently (MarcoFalke)eeee6cf2ff
refactor: Delay translation of _() literals (MarcoFalke) Pull request description: All translatable format strings are fixed. This change surfaces errors in them at compile-time. The implementation achieves this by allowing to delay the translation (or `std::string` construction) that previously happened in `_()` by returning a new type from this function. The new type can be converted to `bilingual_str` where needed. This can be tested by adding a format string error in an original string literal and observing a new compile-time failure. Fixes https://github.com/bitcoin/bitcoin/issues/30530 ACKs for top commit: stickies-v: re-ACKfa3efb5729
ryanofsky: Code review ACKfa3efb5729
. Since last review added TranslateFn commit, clarified FormatStringCheck documentation, dropped redundant `inline` keyword Tree-SHA512: 28fa1db11e85935d998031347bd519675d75c171c8323b0ed6cdd0b628c95250bb86b30876946cc48840ded541e95b8a152696f9f2b13a5f28f5673228ee0509
This commit is contained in:
commit
712cab3a8f
23 changed files with 122 additions and 468 deletions
|
@ -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)) {
|
||||
|
|
|
@ -50,7 +50,7 @@ using util::ToString;
|
|||
// just use a plain system_clock.
|
||||
using CliClock = std::chrono::system_clock;
|
||||
|
||||
const std::function<std::string(const char*)> 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;
|
||||
|
|
|
@ -41,7 +41,7 @@ static bool fCreateBlank;
|
|||
static std::map<std::string,UniValue> registers;
|
||||
static const int CONTINUE_EXECUTION=-1;
|
||||
|
||||
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
const TranslateFn G_TRANSLATION_FUN{nullptr};
|
||||
|
||||
static void SetupBitcoinTxArgs(ArgsManager &argsman)
|
||||
{
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
static const int CONTINUE_EXECUTION=-1;
|
||||
|
||||
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
const TranslateFn G_TRANSLATION_FUN{nullptr};
|
||||
|
||||
static void SetupBitcoinUtilArgs(ArgsManager &argsman)
|
||||
{
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
using util::Join;
|
||||
|
||||
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
const TranslateFn G_TRANSLATION_FUN{nullptr};
|
||||
|
||||
static void SetupWalletToolArgs(ArgsManager& argsman)
|
||||
{
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
using node::NodeContext;
|
||||
|
||||
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
const TranslateFn G_TRANSLATION_FUN{nullptr};
|
||||
|
||||
#if HAVE_DECL_FORK
|
||||
|
||||
|
|
|
@ -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", "<https://opensource.org/licenses/MIT>").translated +
|
||||
"\n";
|
||||
}
|
||||
|
|
12
src/init.cpp
12
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);
|
||||
|
|
|
@ -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 <util/translation.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
// Define G_TRANSLATION_FUN symbol in libbitcoinkernel library so users of the
|
||||
// library aren't required to export this symbol
|
||||
extern const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
extern const TranslateFn G_TRANSLATION_FUN{nullptr};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include <string>
|
||||
|
||||
/** Translate string to current locale using Qt. */
|
||||
extern const std::function<std::string(const char*)> G_TRANSLATION_FUN = [](const char* psz) {
|
||||
extern const TranslateFn G_TRANSLATION_FUN = [](const char* psz) {
|
||||
return QCoreApplication::translate("bitcoin-core", psz).toStdString();
|
||||
};
|
||||
|
||||
|
|
|
@ -14,11 +14,16 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
template <typename... Args>
|
||||
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<signed char>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<signed char>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<signed char>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<unsigned char>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<unsigned char>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<unsigned char>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<char>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<char>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<char>());
|
||||
},
|
||||
[&] {
|
||||
(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<float>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeFloatingPoint<float>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeFloatingPoint<float>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeFloatingPoint<double>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeFloatingPoint<double>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeFloatingPoint<double>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int16_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<int16_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<int16_t>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint16_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<uint16_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<uint16_t>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int32_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<int32_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<int32_t>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint32_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<uint32_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<uint32_t>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int64_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<int64_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<int64_t>());
|
||||
},
|
||||
[&] {
|
||||
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint64_t>());
|
||||
(void)tinyformat::format(bilingual_string, fuzzed_data_provider.ConsumeIntegral<uint64_t>());
|
||||
fuzz_fmt(format_string, fuzzed_data_provider.ConsumeIntegral<uint64_t>());
|
||||
});
|
||||
} catch (const tinyformat::format_error&) {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -72,7 +72,7 @@ using node::LoadChainstate;
|
|||
using node::RegenerateCommitments;
|
||||
using node::VerifyLoadedChainstate;
|
||||
|
||||
const std::function<std::string(const char*)> 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 */
|
||||
|
|
|
@ -16,7 +16,7 @@ template <unsigned NumArgs>
|
|||
void TfmFormatZeroes(const std::string& fmt)
|
||||
{
|
||||
std::apply([&](auto... args) {
|
||||
(void)tfm::format(fmt, args...);
|
||||
(void)tfm::format(tfm::RuntimeFormat{fmt}, args...);
|
||||
}, std::array<int, NumArgs>{});
|
||||
}
|
||||
|
||||
|
|
|
@ -142,6 +142,7 @@ namespace tfm = tinyformat;
|
|||
//------------------------------------------------------------------------------
|
||||
// Implementation details.
|
||||
#include <algorithm>
|
||||
#include <attributes.h> // Added for Bitcoin Core
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept> // 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 <unsigned num_params>
|
||||
struct FormatStringCheck {
|
||||
consteval FormatStringCheck(const char* str) : fmt{util::ConstevalFormatString<num_params>{str}.fmt} {}
|
||||
FormatStringCheck(const std::string& str) : fmt{str.c_str()} {}
|
||||
FormatStringCheck(LIFETIMEBOUND const RuntimeFormat& run) : fmt{run.fmt.c_str()} {}
|
||||
FormatStringCheck(util::ConstevalFormatString<num_params> str) : fmt{str.fmt} {}
|
||||
operator const char*() { return fmt; }
|
||||
const char* fmt;
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
#define BITCOIN_UTIL_TRANSLATION_H
|
||||
|
||||
#include <tinyformat.h>
|
||||
#include <util/string.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
/** Translate a message to the native language of the user. */
|
||||
const extern std::function<std::string(const char*)> G_TRANSLATION_FUN;
|
||||
using TranslateFn = std::function<std::string(const char*)>;
|
||||
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<typename T>
|
||||
T operator+(const T& lhs, const TranslatedLiteral& rhs) { return lhs + static_cast<T>(rhs); }
|
||||
template<typename T>
|
||||
T operator+(const TranslatedLiteral& lhs, const T& rhs) { return static_cast<T>(lhs) + rhs; }
|
||||
|
||||
template <unsigned num_params>
|
||||
struct BilingualFmt {
|
||||
const ConstevalFormatString<num_params> 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 <typename... Args>
|
||||
bilingual_str format(const bilingual_str& fmt, const Args&... args)
|
||||
bilingual_str format(util::BilingualFmt<sizeof...(Args)> 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<decltype(arg), const bilingual_str&>) {
|
||||
return translated ? arg.translated : arg.original;
|
||||
return arg.original;
|
||||
} else if constexpr (std::is_same_v<decltype(arg), const util::TranslatedLiteral&>) {
|
||||
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<decltype(arg), const bilingual_str&>) {
|
||||
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
|
||||
|
|
|
@ -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<CWallet> 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) {
|
||||
|
|
|
@ -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<std::tuple<CKey, int64_t, bool, std::string>> keys;
|
||||
std::vector<std::pair<CScript, int64_t>> scripts;
|
||||
while (file.good()) {
|
||||
|
|
|
@ -280,7 +280,7 @@ std::shared_ptr<CWallet> LoadWalletInternal(WalletContext& context, const std::s
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
context.chain->initMessage(_("Loading wallet…").translated);
|
||||
context.chain->initMessage(_("Loading wallet…"));
|
||||
std::shared_ptr<CWallet> 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<CWallet> CreateWallet(WalletContext& context, const std::string&
|
|||
}
|
||||
|
||||
// Make the wallet
|
||||
context.chain->initMessage(_("Loading wallet…").translated);
|
||||
context.chain->initMessage(_("Loading wallet…"));
|
||||
std::shared_ptr<CWallet> 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<CWallet>& 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);
|
||||
|
||||
{
|
||||
|
|
|
@ -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()
|
|
@ -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<std::codecvt_utf8_utf16<wchar_t>,wchar_t>().to_bytes(buf), err);')
|
||||
['strprintf(', '"%s (%d)",', ' std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,wchar_t>().to_bytes(buf),', ' err', ')']
|
||||
>>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo<wchar_t>().to_bytes(buf), err);')
|
||||
['strprintf(', '"%s (%d)",', ' foo<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 << 1, err);')
|
||||
['strprintf(', '"%s (%d)",', ' foo << 1,', ' err', ')']
|
||||
>>> parse_function_call_and_arguments("strprintf", 'strprintf("%s (%d)", foo<bar>() >> 1, err);')
|
||||
['strprintf(', '"%s (%d)",', ' foo<bar>() >> 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()
|
Loading…
Add table
Reference in a new issue