diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 615774d..aa056f8 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -177,7 +177,8 @@ bool Settings::load(const std::string& path) for (const auto& c : j["blocked_chat_convs"]) if (c.is_object() && c.contains("cid") && c["cid"].is_string()) blocked_chat_convs_.push_back({c["cid"].get(), - c.value("name", std::string())}); + (c.contains("name") && c["name"].is_string()) + ? c["name"].get() : std::string()}); } // Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range). loadScalar(j, "chat_emoji_color", chat_emoji_color_); @@ -194,10 +195,12 @@ bool Settings::load(const std::string& path) loadScalar(j, "blur_multiplier", blur_multiplier_); loadScalar(j, "noise_opacity", noise_opacity_); loadScalar(j, "gradient_background", gradient_background_); - // Migrate legacy reduced_transparency bool -> ui_opacity float - if (j.contains("ui_opacity")) { - ui_opacity_ = j["ui_opacity"].get(); - } else if (j.contains("reduced_transparency") && j["reduced_transparency"].get()) { + // Migrate legacy reduced_transparency bool -> ui_opacity float (type-tolerant: a hand-edited or + // inf->null re-saved value must not quarantine the whole settings file). + if (j.contains("ui_opacity") && j["ui_opacity"].is_number()) { + ui_opacity_ = std::clamp(j["ui_opacity"].get(), 0.3f, 1.0f); // matches setUIOpacity() + } else if (j.contains("reduced_transparency") && j["reduced_transparency"].is_boolean() + && j["reduced_transparency"].get()) { ui_opacity_ = 1.0f; // legacy: reduced = fully opaque } loadScalar(j, "window_opacity", window_opacity_); @@ -421,7 +424,7 @@ bool Settings::load(const std::string& path) entry.showSparkline = e["show_sparkline"].get(); if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer()) entry.sparklineInterval = e["sparkline_interval"].get(); - entry.scope = e.value("scope", ""); + if (e.contains("scope") && e["scope"].is_string()) entry.scope = e["scope"].get(); if (e.contains("grid_col") && e["grid_col"].is_number_integer()) entry.gridCol = e["grid_col"].get(); if (e.contains("grid_row") && e["grid_row"].is_number_integer()) diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 5d1be0a..e9bd1d2 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -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(1)); + EXPECT_EQ(s.getPortfolioEntries().size(), static_cast(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();