fix(persistence): atomic + owner-only settings/address-book writes
settings.json and addressbook.json were written in place with a bare ofstream — a crash or power loss mid-write truncated the file, and on the next launch the parse failure silently reset every preference (hidden/favorite addresses, labels, pool workers, language, theme, lite-server list) because the next save overwrote the corrupt file with defaults. Add Platform::writeFileAtomically() (temp file -> fsync -> atomic rename; dir fsync on POSIX, MoveFileEx on Windows; optional owner-only 0600) and route both saves through it. On a parse failure, quarantine the unreadable settings file to settings.json.corrupt-<ts> instead of clobbering it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <ctime>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
@@ -291,6 +293,17 @@ bool Settings::load(const std::string& path)
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
||||
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
|
||||
// next save() doesn't silently overwrite it with defaults — the user's data stays
|
||||
// recoverable. Proceed with in-memory defaults.
|
||||
file.close();
|
||||
std::error_code ec;
|
||||
const std::string quarantine =
|
||||
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
|
||||
fs::rename(path, quarantine, ec);
|
||||
if (!ec) {
|
||||
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -411,17 +424,11 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure directory exists
|
||||
fs::path p(path);
|
||||
fs::create_directories(p.parent_path());
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file << j.dump(4);
|
||||
return true;
|
||||
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
|
||||
// A crash mid-write can no longer truncate settings.json (which would silently
|
||||
// reset every preference on the next launch). Owner-only (0600) — it carries the
|
||||
// lite-server list and address metadata.
|
||||
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user