fix(settings): tolerate wrongly-typed values instead of quarantining

Several settings were read with json's value()/get<T>() helpers that
assume the stored type. A hand-edited settings file, or one re-saved
after an inf/NaN float was written back as JSON null, could therefore
hold a value of the wrong type and throw during load. The whole load is
wrapped so a throw quarantines the entire settings file to a
.corrupt- copy, meaning one bad field reset every unrelated setting.

- ui_opacity is accepted only when it is actually a number, and it is
  clamped to the same 0.3..1.0 range the setter enforces; the legacy
  reduced_transparency migration likewise runs only when that key is a
  boolean.
- A non-string name in a blocked chat conversation, and a non-string
  scope in a portfolio entry, now fall back to an empty string instead
  of throwing.

The file loads with the offending field defaulted, and the rest of the
settings survive.

Tests cover ui_opacity as a string/null/int/float/bool, a non-string
blocked-conversation name and portfolio scope, and the legacy
reduced_transparency migration, asserting no quarantine file is written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 22:02:59 -05:00
co-authored by Claude Opus 4.8
parent f47528de53
commit 2396ed33db
2 changed files with 51 additions and 6 deletions
+42
View File
@@ -7918,6 +7918,47 @@ void testPortfolioScopeVisibility()
std::error_code ec; fs::remove_all(dir, ec);
}
// Settings::load must tolerate wrongly-typed values without quarantining the whole file.
void testSettingsLoadTypeTolerance()
{
namespace fs = std::filesystem;
const fs::path dir = fs::temp_directory_path() / "dragonx_settings_type_tolerance_test";
std::error_code ec; fs::remove_all(dir, ec); fs::create_directories(dir, ec);
const std::string p = (dir / "settings.json").string();
auto writeAndLoad = [&](const std::string& json, dragonx::config::Settings& s) {
{ std::ofstream o(p, std::ios::binary | std::ios::trunc); o << json; }
return s.load(p);
};
auto noQuarantine = [&]() {
for (const auto& de : fs::directory_iterator(dir, ec))
if (de.path().filename().string().find(".corrupt-") != std::string::npos) return false;
return true;
};
for (const char* bad : {"\"x\"", "null", "5", "0.01", "true"}) {
dragonx::config::Settings s;
const std::string js = std::string("{\"ui_opacity\":") + bad + ",\"chat_density\":1}";
EXPECT_TRUE(writeAndLoad(js, s));
EXPECT_TRUE(noQuarantine());
EXPECT_TRUE(s.getUIOpacity() >= 0.3f && s.getUIOpacity() <= 1.0f);
}
{
dragonx::config::Settings s;
EXPECT_TRUE(writeAndLoad(
"{\"blocked_chat_convs\":[{\"cid\":\"abc\",\"name\":7}],"
"\"portfolio_entries\":[{\"label\":\"L\",\"scope\":7}]}", s));
EXPECT_TRUE(noQuarantine());
EXPECT_EQ(s.blockedChatConversations().size(), static_cast<size_t>(1));
EXPECT_EQ(s.getPortfolioEntries().size(), static_cast<size_t>(1));
EXPECT_EQ(s.getPortfolioEntries()[0].scope, std::string());
}
{
dragonx::config::Settings s;
EXPECT_TRUE(writeAndLoad("{\"reduced_transparency\":true}", s));
EXPECT_NEAR(s.getUIOpacity(), 1.0f, 1e-6f);
}
fs::remove_all(dir, ec);
}
// Market price-series math (data/market_series.h) — minute-resampling + time-window bucketing
// that feed the price chart and the portfolio-group sparklines.
void testMarketSeries()
@@ -18669,6 +18710,7 @@ int main()
testHushChatTransport();
testHushChatShuffledReceive();
testWalletFileProbe();
testSettingsLoadTypeTolerance();
testAddressChecksumValidation();
testPrivateKeyImportRecognition();
testSeedPhraseHelpers();