205 lines
6.8 KiB
C++
205 lines
6.8 KiB
C++
#include "GlobalBookmarkIndex.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
|
|
GlobalBookmarkIndex GlobalBookmarkIndex::instance;
|
|
|
|
namespace {
|
|
void writeString(FsFile& f, const std::string& s) {
|
|
const uint16_t len = static_cast<uint16_t>(std::min<size_t>(s.size(), UINT16_MAX));
|
|
f.write(reinterpret_cast<const uint8_t*>(&len), sizeof(len));
|
|
if (len > 0) {
|
|
f.write(reinterpret_cast<const uint8_t*>(s.data()), len);
|
|
}
|
|
}
|
|
|
|
bool readString(FsFile& f, std::string& out) {
|
|
uint16_t len = 0;
|
|
if (f.read(reinterpret_cast<uint8_t*>(&len), sizeof(len)) != sizeof(len)) return false;
|
|
out.clear();
|
|
if (len == 0) return true;
|
|
out.resize(len);
|
|
return f.read(reinterpret_cast<uint8_t*>(&out[0]), len) == len;
|
|
}
|
|
|
|
constexpr uint16_t MAX_ENTRIES = 1000;
|
|
constexpr uint16_t MAX_BOOKMARKS_PER_ENTRY = 1000;
|
|
} // namespace
|
|
|
|
std::vector<GlobalBookmarkIndex::Entry>::iterator GlobalBookmarkIndex::findBySourcePath(const std::string& sourcePath) {
|
|
return std::find_if(entries.begin(), entries.end(),
|
|
[&sourcePath](const Entry& e) { return e.sourcePath == sourcePath; });
|
|
}
|
|
|
|
void GlobalBookmarkIndex::load() {
|
|
entries.clear();
|
|
loaded = true;
|
|
|
|
FsFile f;
|
|
if (!Storage.openFileForRead("GBI", FILE_PATH, f)) {
|
|
LOG_DBG("GBI", "No existing global bookmarks file");
|
|
return;
|
|
}
|
|
|
|
uint8_t version = 0;
|
|
if (f.read(&version, 1) != 1 || version != FILE_VERSION) {
|
|
LOG_ERR("GBI", "Bad version: %u", version);
|
|
f.close();
|
|
return;
|
|
}
|
|
|
|
uint16_t entryCount = 0;
|
|
if (f.read(reinterpret_cast<uint8_t*>(&entryCount), sizeof(entryCount)) != sizeof(entryCount) ||
|
|
entryCount > MAX_ENTRIES) {
|
|
LOG_ERR("GBI", "Invalid entry count: %u", static_cast<unsigned>(entryCount));
|
|
f.close();
|
|
return;
|
|
}
|
|
|
|
entries.reserve(entryCount);
|
|
for (uint16_t i = 0; i < entryCount; i++) {
|
|
Entry e;
|
|
uint8_t isTxtByte = 0;
|
|
if (!readString(f, e.sourcePath) || !readString(f, e.cacheDir) || !readString(f, e.title) ||
|
|
f.read(&isTxtByte, 1) != 1) {
|
|
LOG_ERR("GBI", "Truncated entry %u", i);
|
|
entries.clear();
|
|
f.close();
|
|
return;
|
|
}
|
|
e.isTxt = (isTxtByte != 0);
|
|
|
|
uint16_t bmCount = 0;
|
|
if (f.read(reinterpret_cast<uint8_t*>(&bmCount), sizeof(bmCount)) != sizeof(bmCount) ||
|
|
bmCount > MAX_BOOKMARKS_PER_ENTRY) {
|
|
LOG_ERR("GBI", "Invalid bookmark count for entry %u: %u", i, static_cast<unsigned>(bmCount));
|
|
entries.clear();
|
|
f.close();
|
|
return;
|
|
}
|
|
e.bookmarks.reserve(bmCount);
|
|
for (uint16_t j = 0; j < bmCount; j++) {
|
|
BookmarkStore::Bookmark bm;
|
|
if (f.read(reinterpret_cast<uint8_t*>(&bm.spineIndex), sizeof(bm.spineIndex)) != sizeof(bm.spineIndex) ||
|
|
f.read(reinterpret_cast<uint8_t*>(&bm.pageNumber), sizeof(bm.pageNumber)) != sizeof(bm.pageNumber) ||
|
|
!readString(f, bm.name)) {
|
|
LOG_ERR("GBI", "Truncated bookmark");
|
|
entries.clear();
|
|
f.close();
|
|
return;
|
|
}
|
|
e.bookmarks.push_back(std::move(bm));
|
|
}
|
|
entries.push_back(std::move(e));
|
|
}
|
|
f.close();
|
|
LOG_DBG("GBI", "Loaded %u entries", static_cast<unsigned>(entries.size()));
|
|
}
|
|
|
|
void GlobalBookmarkIndex::save() const {
|
|
Storage.mkdir("/.crosspoint");
|
|
FsFile f;
|
|
if (!Storage.openFileForWrite("GBI", FILE_PATH, f)) {
|
|
LOG_ERR("GBI", "Failed to open for write");
|
|
return;
|
|
}
|
|
|
|
const uint8_t version = FILE_VERSION;
|
|
f.write(&version, 1);
|
|
const uint16_t entryCount = static_cast<uint16_t>(std::min<size_t>(entries.size(), UINT16_MAX));
|
|
f.write(reinterpret_cast<const uint8_t*>(&entryCount), sizeof(entryCount));
|
|
|
|
for (uint16_t i = 0; i < entryCount; i++) {
|
|
const Entry& e = entries[i];
|
|
writeString(f, e.sourcePath);
|
|
writeString(f, e.cacheDir);
|
|
writeString(f, e.title);
|
|
const uint8_t isTxtByte = e.isTxt ? 1 : 0;
|
|
f.write(&isTxtByte, 1);
|
|
const uint16_t bmCount = static_cast<uint16_t>(std::min<size_t>(e.bookmarks.size(), UINT16_MAX));
|
|
f.write(reinterpret_cast<const uint8_t*>(&bmCount), sizeof(bmCount));
|
|
for (const auto& bm : e.bookmarks) {
|
|
f.write(reinterpret_cast<const uint8_t*>(&bm.spineIndex), sizeof(bm.spineIndex));
|
|
f.write(reinterpret_cast<const uint8_t*>(&bm.pageNumber), sizeof(bm.pageNumber));
|
|
writeString(f, bm.name);
|
|
}
|
|
}
|
|
f.close();
|
|
LOG_DBG("GBI", "Saved %u entries", static_cast<unsigned>(entryCount));
|
|
}
|
|
|
|
void GlobalBookmarkIndex::upsertFromStore(const std::string& sourcePath, const std::string& cacheDir,
|
|
const std::string& title, bool isTxt,
|
|
const std::vector<BookmarkStore::Bookmark>& bookmarks) {
|
|
if (!loaded) load();
|
|
|
|
auto it = findBySourcePath(sourcePath);
|
|
if (bookmarks.empty()) {
|
|
if (it != entries.end()) {
|
|
entries.erase(it);
|
|
save();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (it == entries.end()) {
|
|
Entry e;
|
|
e.sourcePath = sourcePath;
|
|
e.cacheDir = cacheDir;
|
|
e.title = title;
|
|
e.isTxt = isTxt;
|
|
e.bookmarks = bookmarks;
|
|
entries.push_back(std::move(e));
|
|
} else {
|
|
it->cacheDir = cacheDir;
|
|
it->title = title;
|
|
it->isTxt = isTxt;
|
|
it->bookmarks = bookmarks;
|
|
}
|
|
save();
|
|
}
|
|
|
|
void GlobalBookmarkIndex::syncFromStore(const BookmarkStore& store, const std::string& sourcePath,
|
|
const std::string& cacheDir, const std::string& title, bool isTxt) {
|
|
upsertFromStore(sourcePath, cacheDir, title, isTxt, store.getAll());
|
|
}
|
|
|
|
void GlobalBookmarkIndex::removeBySourcePath(const std::string& sourcePath) {
|
|
if (!loaded) load();
|
|
auto it = findBySourcePath(sourcePath);
|
|
if (it != entries.end()) {
|
|
entries.erase(it);
|
|
save();
|
|
}
|
|
}
|
|
|
|
bool GlobalBookmarkIndex::reconcile() {
|
|
if (!loaded) load();
|
|
const size_t before = entries.size();
|
|
entries.erase(std::remove_if(entries.begin(), entries.end(),
|
|
[](const Entry& e) {
|
|
if (!Storage.exists(e.sourcePath.c_str())) {
|
|
LOG_DBG("GBI", "Dropping orphan entry: missing sourcePath=%s", e.sourcePath.c_str());
|
|
return true;
|
|
}
|
|
if (!Storage.exists(e.cacheDir.c_str())) {
|
|
LOG_DBG("GBI", "Dropping orphan entry: missing cacheDir=%s", e.cacheDir.c_str());
|
|
return true;
|
|
}
|
|
return false;
|
|
}),
|
|
entries.end());
|
|
const bool changed = entries.size() != before;
|
|
if (changed) save();
|
|
return changed;
|
|
}
|
|
|
|
size_t GlobalBookmarkIndex::totalBookmarkCount() const {
|
|
return std::accumulate(entries.begin(), entries.end(), static_cast<size_t>(0),
|
|
[](size_t sum, const Entry& e) { return sum + e.bookmarks.size(); });
|
|
}
|