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"])
|
||||
if (c.is_object() && c.contains("cid") && c["cid"].is_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).
|
||||
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<float>();
|
||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
||||
// 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<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
|
||||
}
|
||||
loadScalar(j, "window_opacity", window_opacity_);
|
||||
@@ -421,7 +424,7 @@ bool Settings::load(const std::string& path)
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||
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())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
|
||||
Reference in New Issue
Block a user