fix(i18n): reject format-incompatible translations
Many strings are used directly as printf/ImGui format strings, and translations are loaded from user/installer-modifiable JSON with no validation. A translated value that drops or changes a conversion specifier would be passed to printf with mismatched varargs (undefined behavior) on a wallet screen. overlayTranslations() now compares each translated value's argument signature against the English source and keeps English on mismatch. Also adds the send_status_unconfirmed string used by the deferred-send-result path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,38 @@ namespace util {
|
|||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Map a printf conversion char to its varargs argument class: 'i' (int-promoted), 'f'
|
||||||
|
// (double), 'p' (pointer/string). This is what matters for safety — reading a double slot
|
||||||
|
// as an int (or vice-versa) is undefined behavior; interchanging d/x is merely cosmetic.
|
||||||
|
char conversionClass(char c)
|
||||||
|
{
|
||||||
|
switch (c) {
|
||||||
|
case 'd': case 'i': case 'o': case 'u': case 'x': case 'X': case 'c': return 'i';
|
||||||
|
case 'f': case 'F': case 'e': case 'E': case 'g': case 'G': case 'a': case 'A': return 'f';
|
||||||
|
case 's': case 'p': return 'p';
|
||||||
|
default: return '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered argument signature of a printf-style format string (e.g. "%d / %.0f%%" -> "if").
|
||||||
|
// "%%" is a literal and contributes nothing.
|
||||||
|
std::string formatSignature(const std::string& s)
|
||||||
|
{
|
||||||
|
std::string sig;
|
||||||
|
for (size_t i = 0; i < s.size(); ++i) {
|
||||||
|
if (s[i] != '%') continue;
|
||||||
|
if (++i >= s.size()) break;
|
||||||
|
if (s[i] == '%') continue; // literal "%%"
|
||||||
|
while (i < s.size() && std::strchr("-+ #0123456789.*lhLzjtq", s[i])) ++i;
|
||||||
|
if (i < s.size()) sig.push_back(conversionClass(s[i]));
|
||||||
|
}
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
I18n::I18n()
|
I18n::I18n()
|
||||||
{
|
{
|
||||||
// Register built-in languages
|
// Register built-in languages
|
||||||
@@ -51,6 +83,27 @@ I18n& I18n::instance()
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void I18n::overlayTranslations(const nlohmann::json& translations)
|
||||||
|
{
|
||||||
|
for (auto& [key, value] : translations.items()) {
|
||||||
|
if (!value.is_string()) continue;
|
||||||
|
const std::string translated = value.get<std::string>();
|
||||||
|
// If the English base for this key carries printf specifiers, the translation must
|
||||||
|
// keep the same argument signature — otherwise it would be passed to printf/ImGui
|
||||||
|
// with mismatched varargs (UB). On mismatch, keep the safe English string.
|
||||||
|
const auto base = strings_.find(key);
|
||||||
|
if (base != strings_.end()) {
|
||||||
|
const std::string baseSig = formatSignature(base->second);
|
||||||
|
if (!baseSig.empty() && baseSig != formatSignature(translated)) {
|
||||||
|
DEBUG_LOGF("i18n: dropping '%s' — format mismatch (\"%s\" vs English)\n",
|
||||||
|
key.c_str(), translated.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strings_[key] = translated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool I18n::loadLanguage(const std::string& locale)
|
bool I18n::loadLanguage(const std::string& locale)
|
||||||
{
|
{
|
||||||
// Try to load from file: CWD-relative first, then exe-relative
|
// Try to load from file: CWD-relative first, then exe-relative
|
||||||
@@ -72,11 +125,7 @@ bool I18n::loadLanguage(const std::string& locale)
|
|||||||
} else {
|
} else {
|
||||||
strings_.clear();
|
strings_.clear();
|
||||||
}
|
}
|
||||||
for (auto& [key, value] : j.items()) {
|
overlayTranslations(j);
|
||||||
if (value.is_string()) {
|
|
||||||
strings_[key] = value.get<std::string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current_locale_ = locale;
|
current_locale_ = locale;
|
||||||
DEBUG_LOGF("Loaded language file: %s (%zu strings)\n", lang_file.c_str(), strings_.size());
|
DEBUG_LOGF("Loaded language file: %s (%zu strings)\n", lang_file.c_str(), strings_.size());
|
||||||
@@ -128,12 +177,8 @@ bool I18n::loadLanguage(const std::string& locale)
|
|||||||
} else {
|
} else {
|
||||||
strings_.clear();
|
strings_.clear();
|
||||||
}
|
}
|
||||||
for (auto& [key, value] : j.items()) {
|
overlayTranslations(j);
|
||||||
if (value.is_string()) {
|
|
||||||
strings_[key] = value.get<std::string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current_locale_ = locale;
|
current_locale_ = locale;
|
||||||
DEBUG_LOGF("Loaded embedded language: %s (%zu strings)\n", locale.c_str(), strings_.size());
|
DEBUG_LOGF("Loaded embedded language: %s (%zu strings)\n", locale.c_str(), strings_.size());
|
||||||
return true;
|
return true;
|
||||||
@@ -1199,6 +1244,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_tx_failed"] = "Transaction failed";
|
strings_["send_tx_failed"] = "Transaction failed";
|
||||||
strings_["send_tx_sent"] = "Transaction sent!";
|
strings_["send_tx_sent"] = "Transaction sent!";
|
||||||
strings_["send_tx_success"] = "Transaction sent successfully!";
|
strings_["send_tx_success"] = "Transaction sent successfully!";
|
||||||
|
strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed";
|
||||||
strings_["send_txid_copied"] = "TxID copied to clipboard";
|
strings_["send_txid_copied"] = "TxID copied to clipboard";
|
||||||
strings_["send_txid_label"] = "TxID: %s";
|
strings_["send_txid_label"] = "TxID: %s";
|
||||||
strings_["send_valid_shielded"] = "Valid shielded address";
|
strings_["send_valid_shielded"] = "Valid shielded address";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace util {
|
namespace util {
|
||||||
@@ -58,7 +59,12 @@ public:
|
|||||||
private:
|
private:
|
||||||
I18n();
|
I18n();
|
||||||
void loadBuiltinEnglish();
|
void loadBuiltinEnglish();
|
||||||
|
// Overlay translated strings onto the English base, but reject any value whose printf
|
||||||
|
// format-specifier signature differs from the English source (keep English instead).
|
||||||
|
// Many strings are used directly as printf/ImGui format strings, so a mismatched
|
||||||
|
// translation (dropped/changed specifier) is undefined behavior — this prevents it.
|
||||||
|
void overlayTranslations(const nlohmann::json& translations);
|
||||||
|
|
||||||
std::string current_locale_ = "en";
|
std::string current_locale_ = "en";
|
||||||
std::unordered_map<std::string, std::string> strings_;
|
std::unordered_map<std::string, std::string> strings_;
|
||||||
std::vector<std::pair<std::string, std::string>> available_languages_;
|
std::vector<std::pair<std::string, std::string>> available_languages_;
|
||||||
|
|||||||
Reference in New Issue
Block a user