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:
@@ -177,7 +177,8 @@ bool Settings::load(const std::string& path)
|
|||||||
for (const auto& c : j["blocked_chat_convs"])
|
for (const auto& c : j["blocked_chat_convs"])
|
||||||
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||||
blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
|
blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||||
c.value("name", std::string())});
|
(c.contains("name") && c["name"].is_string())
|
||||||
|
? c["name"].get<std::string>() : std::string()});
|
||||||
}
|
}
|
||||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
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, "blur_multiplier", blur_multiplier_);
|
||||||
loadScalar(j, "noise_opacity", noise_opacity_);
|
loadScalar(j, "noise_opacity", noise_opacity_);
|
||||||
loadScalar(j, "gradient_background", gradient_background_);
|
loadScalar(j, "gradient_background", gradient_background_);
|
||||||
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
// Migrate legacy reduced_transparency bool -> ui_opacity float (type-tolerant: a hand-edited or
|
||||||
if (j.contains("ui_opacity")) {
|
// inf->null re-saved value must not quarantine the whole settings file).
|
||||||
ui_opacity_ = j["ui_opacity"].get<float>();
|
if (j.contains("ui_opacity") && j["ui_opacity"].is_number()) {
|
||||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
ui_opacity_ = std::clamp(j["ui_opacity"].get<float>(), 0.3f, 1.0f); // matches setUIOpacity()
|
||||||
|
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].is_boolean()
|
||||||
|
&& j["reduced_transparency"].get<bool>()) {
|
||||||
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
||||||
}
|
}
|
||||||
loadScalar(j, "window_opacity", window_opacity_);
|
loadScalar(j, "window_opacity", window_opacity_);
|
||||||
@@ -421,7 +424,7 @@ bool Settings::load(const std::string& path)
|
|||||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||||
entry.scope = e.value("scope", "");
|
if (e.contains("scope") && e["scope"].is_string()) entry.scope = e["scope"].get<std::string>();
|
||||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||||
entry.gridCol = e["grid_col"].get<int>();
|
entry.gridCol = e["grid_col"].get<int>();
|
||||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||||
|
|||||||
@@ -7918,6 +7918,47 @@ void testPortfolioScopeVisibility()
|
|||||||
std::error_code ec; fs::remove_all(dir, ec);
|
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
|
// Market price-series math (data/market_series.h) — minute-resampling + time-window bucketing
|
||||||
// that feed the price chart and the portfolio-group sparklines.
|
// that feed the price chart and the portfolio-group sparklines.
|
||||||
void testMarketSeries()
|
void testMarketSeries()
|
||||||
@@ -18669,6 +18710,7 @@ int main()
|
|||||||
testHushChatTransport();
|
testHushChatTransport();
|
||||||
testHushChatShuffledReceive();
|
testHushChatShuffledReceive();
|
||||||
testWalletFileProbe();
|
testWalletFileProbe();
|
||||||
|
testSettingsLoadTypeTolerance();
|
||||||
testAddressChecksumValidation();
|
testAddressChecksumValidation();
|
||||||
testPrivateKeyImportRecognition();
|
testPrivateKeyImportRecognition();
|
||||||
testSeedPhraseHelpers();
|
testSeedPhraseHelpers();
|
||||||
|
|||||||
Reference in New Issue
Block a user