Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
16 kB · 407 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408#include "JsonSettingsIO.h"
#include <ArduinoJson.h>#include <HalStorage.h>#include <Logging.h>#include <ObfuscationUtils.h>
#include <cstring>#include <string>
#include "CrossPointSettings.h"#include "CrossPointState.h"#include "KOReaderCredentialStore.h"#include "OpdsServerStore.h"#include "RecentBooksStore.h"#include "SettingsList.h"#include "WifiCredentialStore.h"
// Convert legacy settings.void applyLegacyStatusBarSettings(CrossPointSettings& settings) { switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) { case CrossPointSettings::NONE: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::NO_PROGRESS: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::BOOK_PROGRESS_BAR: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::CHAPTER_PROGRESS_BAR: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::FULL: default: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; }}
// ---- CrossPointState ----
bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { JsonDocument doc; doc["openEpubPath"] = s.openEpubPath; JsonArray recentArr = doc["recentSleepImages"].to<JsonArray>(); for (int i = 0; i < CrossPointState::SLEEP_RECENT_COUNT; i++) recentArr.add(s.recentSleepImages[i]); doc["recentSleepPos"] = s.recentSleepPos; doc["recentSleepFill"] = s.recentSleepFill; doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader;
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); return false; }
s.openEpubPath = doc["openEpubPath"] | std::string(""); memset(s.recentSleepImages, 0, sizeof(s.recentSleepImages)); JsonArrayConst recentArr = doc["recentSleepImages"]; const int actualCount = recentArr.isNull() ? 0 : std::min(static_cast<int>(recentArr.size()), static_cast<int>(CrossPointState::SLEEP_RECENT_COUNT)); for (int i = 0; i < actualCount; i++) s.recentSleepImages[i] = recentArr[i] | static_cast<uint16_t>(0); s.recentSleepPos = doc["recentSleepPos"] | static_cast<uint8_t>(0); if (s.recentSleepPos >= CrossPointState::SLEEP_RECENT_COUNT) s.recentSleepPos = actualCount > 0 ? s.recentSleepPos % CrossPointState::SLEEP_RECENT_COUNT : 0; s.recentSleepFill = doc["recentSleepFill"] | static_cast<uint8_t>(0); s.recentSleepFill = static_cast<uint8_t>(std::min(static_cast<int>(s.recentSleepFill), actualCount)); // Migrate legacy single-image field from old state.json (pre-recency-buffer). // Only seeds the buffer if the new buffer is empty (fresh migration, not a resave). if (s.recentSleepFill == 0 && !doc["lastSleepImage"].isNull()) { const uint8_t legacy = doc["lastSleepImage"] | static_cast<uint8_t>(UINT8_MAX); if (legacy != UINT8_MAX) s.pushRecentSleep(static_cast<uint16_t>(legacy)); } s.readerActivityLoadCount = doc["readerActivityLoadCount"] | static_cast<uint8_t>(0); s.lastSleepFromReader = doc["lastSleepFromReader"] | false; return true;}
// ---- CrossPointSettings ----
bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path) { JsonDocument doc;
for (const auto& info : getSettingsList()) { if (!info.key) continue; // Dynamic entries (KOReader etc.) are stored in their own files — skip. if (!info.valuePtr && !info.stringOffset) continue;
if (info.stringOffset) { const char* strPtr = (const char*)&s + info.stringOffset; if (info.obfuscated) { doc[std::string(info.key) + "_obf"] = obfuscation::obfuscateToBase64(strPtr); } else { doc[info.key] = strPtr; } } else { doc[info.key] = s.*(info.valuePtr); } }
// Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. doc["frontButtonBack"] = s.frontButtonBack; doc["frontButtonConfirm"] = s.frontButtonConfirm; doc["frontButtonLeft"] = s.frontButtonLeft; doc["frontButtonRight"] = s.frontButtonRight;
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); return false; }
auto clamp = [](uint8_t val, uint8_t maxVal, uint8_t def) -> uint8_t { return val < maxVal ? val : def; };
// Legacy migration: if statusBarChapterPageCount is absent this is a pre-refactor settings file. // Populate s with migrated values now so the generic loop below picks them up as defaults and clamps them. if (doc["statusBarChapterPageCount"].isNull()) { applyLegacyStatusBarSettings(s); }
for (const auto& info : getSettingsList()) { if (!info.key) continue; // Dynamic entries (KOReader etc.) are stored in their own files — skip. if (!info.valuePtr && !info.stringOffset) continue;
if (info.stringOffset) { const char* strPtr = (const char*)&s + info.stringOffset; const std::string fieldDefault = strPtr; // current buffer = struct-initializer default std::string val; if (info.obfuscated) { bool ok = false; val = obfuscation::deobfuscateFromBase64(doc[std::string(info.key) + "_obf"] | "", &ok); if (!ok || val.empty()) { val = doc[info.key] | fieldDefault; if (val != fieldDefault && needsResave) *needsResave = true; } } else { val = doc[info.key] | fieldDefault; } char* destPtr = (char*)&s + info.stringOffset; if (info.stringMaxLen == 0) { LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key); destPtr[0] = '\0'; if (needsResave) *needsResave = true; continue; } strncpy(destPtr, val.c_str(), info.stringMaxLen - 1); destPtr[info.stringMaxLen - 1] = '\0'; } else { const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it uint8_t v = doc[info.key] | fieldDefault; if (info.type == SettingType::ENUM) { v = clamp(v, (uint8_t)info.enumValues.size(), fieldDefault); } else if (info.type == SettingType::TOGGLE) { v = clamp(v, (uint8_t)2, fieldDefault); } else if (info.type == SettingType::VALUE) { if (v < info.valueRange.min) v = info.valueRange.min; else if (v > info.valueRange.max) v = info.valueRange.max; } s.*(info.valuePtr) = v; } }
// Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. using S = CrossPointSettings; s.frontButtonBack = clamp(doc["frontButtonBack"] | (uint8_t)S::FRONT_HW_BACK, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_BACK); s.frontButtonConfirm = clamp(doc["frontButtonConfirm"] | (uint8_t)S::FRONT_HW_CONFIRM, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_CONFIRM); s.frontButtonLeft = clamp(doc["frontButtonLeft"] | (uint8_t)S::FRONT_HW_LEFT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_LEFT); s.frontButtonRight = clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); CrossPointSettings::validateFrontButtonMapping(s);
LOG_DBG("CPS", "Settings loaded from file");
return true;}
// ---- KOReaderCredentialStore ----
bool JsonSettingsIO::saveKOReader(const KOReaderCredentialStore& store, const char* path) { JsonDocument doc; doc["username"] = store.getUsername(); doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword()); doc["serverUrl"] = store.getServerUrl(); doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadKOReader(KOReaderCredentialStore& store, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("KRS", "JSON parse error: %s", error.c_str()); return false; }
store.username = doc["username"] | std::string(""); bool ok = false; store.password = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok); if (!ok || store.password.empty()) { store.password = doc["password"] | std::string(""); if (!store.password.empty() && needsResave) *needsResave = true; } store.serverUrl = doc["serverUrl"] | std::string(""); uint8_t method = doc["matchMethod"] | (uint8_t)0; store.matchMethod = static_cast<DocumentMatchMethod>(method);
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", store.username.c_str()); return true;}
// ---- WifiCredentialStore ----
bool JsonSettingsIO::saveWifi(const WifiCredentialStore& store, const char* path) { JsonDocument doc; doc["lastConnectedSsid"] = store.getLastConnectedSsid();
JsonArray arr = doc["credentials"].to<JsonArray>(); for (const auto& cred : store.getCredentials()) { JsonObject obj = arr.add<JsonObject>(); obj["ssid"] = cred.ssid; obj["password_obf"] = obfuscation::obfuscateToBase64(cred.password); }
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadWifi(WifiCredentialStore& store, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("WCS", "JSON parse error: %s", error.c_str()); return false; }
store.lastConnectedSsid = doc["lastConnectedSsid"] | std::string("");
store.credentials.clear(); JsonArray arr = doc["credentials"].as<JsonArray>(); for (JsonObject obj : arr) { if (store.credentials.size() >= store.MAX_NETWORKS) break; WifiCredential cred; cred.ssid = obj["ssid"] | std::string(""); bool ok = false; cred.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", &ok); if (!ok || cred.password.empty()) { cred.password = obj["password"] | std::string(""); if (!cred.password.empty() && needsResave) *needsResave = true; } store.credentials.push_back(cred); }
LOG_DBG("WCS", "Loaded %zu WiFi credentials from file", store.credentials.size()); return true;}
// ---- RecentBooksStore ----
bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char* path) { JsonDocument doc; JsonArray arr = doc["books"].to<JsonArray>(); for (const auto& book : store.getBooks()) { JsonObject obj = arr.add<JsonObject>(); obj["path"] = book.path; obj["title"] = book.title; obj["author"] = book.author; obj["coverBmpPath"] = book.coverBmpPath; }
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) { JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("RBS", "JSON parse error: %s", error.c_str()); return false; }
store.recentBooks.clear(); JsonArray arr = doc["books"].as<JsonArray>(); for (JsonObject obj : arr) { if (store.getCount() >= 10) break; RecentBook book; book.path = obj["path"] | std::string(""); book.title = obj["title"] | std::string(""); book.author = obj["author"] | std::string(""); book.coverBmpPath = obj["coverBmpPath"] | std::string(""); store.recentBooks.push_back(book); }
LOG_DBG("RBS", "Recent books loaded from file (%d entries)", store.getCount()); return true;}
// ---- OpdsServerStore ----// Follows the same save/load pattern as WifiCredentialStore above.// Passwords are XOR-obfuscated with the device MAC and base64-encoded ("password_obf" key).
bool JsonSettingsIO::saveOpds(const OpdsServerStore& store, const char* path) { JsonDocument doc;
JsonArray arr = doc["servers"].to<JsonArray>(); for (const auto& server : store.getServers()) { JsonObject obj = arr.add<JsonObject>(); obj["name"] = server.name; obj["url"] = server.url; obj["username"] = server.username; obj["password_obf"] = obfuscation::obfuscateToBase64(server.password); }
String json; serializeJson(doc, json); return Storage.writeFile(path, json);}
bool JsonSettingsIO::loadOpds(OpdsServerStore& store, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("OPS", "JSON parse error: %s", error.c_str()); return false; }
store.servers.clear(); JsonArray arr = doc["servers"].as<JsonArray>(); for (JsonObject obj : arr) { if (store.servers.size() >= OpdsServerStore::MAX_SERVERS) break; OpdsServer server; server.name = obj["name"] | std::string(""); server.url = obj["url"] | std::string(""); server.username = obj["username"] | std::string(""); // Try the obfuscated key first; fall back to plaintext "password" for // files written before obfuscation was added (or hand-edited JSON). bool ok = false; server.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", &ok); if (!ok || server.password.empty()) { server.password = obj["password"] | std::string(""); if (!server.password.empty() && needsResave) *needsResave = true; } store.servers.push_back(std::move(server)); }
LOG_DBG("OPS", "Loaded %zu OPDS servers from file", store.servers.size()); return true;}