chore: Refactor stores to use PersistableStore CRTP template (#2464)

This commit is contained in:
Uri Tauber
2026-07-08 12:44:52 +03:00
committed by GitHub
parent 5776911af0
commit c2c1badc7e
16 changed files with 277 additions and 667 deletions
+45
View File
@@ -0,0 +1,45 @@
#include "PersistableStore.h"
#include <HalStorage.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
bool PersistableStoreBase::writeDocToFile(const char* path, const JsonDocument& doc) {
Storage.mkdir("/.crosspoint");
String json;
serializeJson(doc, json);
if (!Storage.writeFile(path, json)) {
LOG_ERR("PERSIST", "Failed to write %s", path);
return false;
}
return true;
}
bool PersistableStoreBase::readDocFromFile(const char* path, JsonDocument& doc) {
if (!Storage.exists(path)) {
return false; // Expected on first boot — not an error.
}
String json = Storage.readFile(path);
if (json.isEmpty()) {
LOG_ERR("PERSIST", "Failed to read %s (empty)", path);
return false;
}
auto error = deserializeJson(doc, json);
if (error) {
LOG_ERR("PERSIST", "JSON parse error in %s: %s", path, error.c_str());
return false;
}
return true;
}
std::string PersistableStoreBase::extractPassword(JsonVariantConst doc, bool& needsResave) {
bool ok = false;
std::string pass = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
if (!ok) {
// Deobfuscation failed — fall back to legacy plaintext password.
pass = doc["password"] | "";
if (!pass.empty()) needsResave = true;
}
// A successfully decoded empty string is a legitimate value; preserve as-is.
return pass;
}
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <string>
/**
* @brief Non-template core of PersistableStore.
*
* All ArduinoJson parse/serialize machinery is instantiated once here (in
* PersistableStore.cpp) instead of in every store's translation unit. GCC
* emits the JSON serializer/parser templates as local .isra clones per TU
* (~0.5KB each), so keeping serializeJson/deserializeJson out of the stores
* is what makes the abstraction flash-neutral.
*/
class PersistableStoreBase {
protected:
PersistableStoreBase() = default;
~PersistableStoreBase() = default;
// Serializes doc and writes it to path (ensures /.crosspoint exists). Logs on failure.
static bool writeDocToFile(const char* path, const JsonDocument& doc);
// Reads path and parses it into doc. Returns false silently when the file
// does not exist (expected on first boot); logs on read/parse failure.
static bool readDocFromFile(const char* path, JsonDocument& doc);
/**
* Helper function for extracting an obfuscated password from a JSON value.
* Accepts JsonVariantConst so callers can pass either a whole JsonDocument
* or a JsonObject element (e.g. inside an array iteration).
* If the decoded password requires a resave (e.g. from plaintext fallback), `needsResave` is set to true.
*/
static std::string extractPassword(JsonVariantConst doc, bool& needsResave);
};
/**
* @brief Base class for persistable singletons using CRTP.
*
* Derived classes must provide:
* - A private default constructor
* - friend class PersistableStore<Derived>;
* - static const char* getFilePath();
* - void toJson(JsonDocument& doc) const;
* - bool fromJson(JsonVariantConst doc);
*
* Note for implementers: read string values as `const char*` (e.g.
* `obj["name"] | ""`), never as `| std::string("")` — ArduinoJson's
* std::string converter drags a per-TU copy of the whole JSON serializer
* into flash via its serializeJson fallback.
*/
template <typename T>
class PersistableStore : public PersistableStoreBase {
protected:
PersistableStore() = default;
~PersistableStore() = default;
public:
// Delete copy constructor and assignment
PersistableStore(const PersistableStore&) = delete;
PersistableStore& operator=(const PersistableStore&) = delete;
static T& getInstance() {
static T instance;
return instance;
}
bool saveToFile() const {
JsonDocument doc;
static_cast<const T*>(this)->toJson(doc);
return writeDocToFile(T::getFilePath(), doc);
}
bool loadFromFile() {
JsonDocument doc;
if (!readDocFromFile(T::getFilePath(), doc)) {
return false;
}
return static_cast<T*>(this)->fromJson(doc.as<JsonVariantConst>());
}
};