From 1107590b5676dbf8091be0d264a3eb6c8a1add98 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 23 Dec 2025 14:14:10 +1100 Subject: [PATCH] Standardize File handling with FsHelpers (#110) ## Summary * Standardize File handling with FsHelpers * Better central place to manage to logic of if files exist/open for reading/writing --- lib/Epub/Epub.cpp | 81 +++++-------- lib/Epub/Epub/FsHelpers.cpp | 36 ------ lib/Epub/Epub/FsHelpers.h | 6 - lib/Epub/Epub/Page.cpp | 36 +++--- lib/Epub/Epub/Page.h | 12 +- lib/Epub/Epub/Section.cpp | 50 ++++---- lib/Epub/Epub/blocks/TextBlock.cpp | 32 ++--- lib/Epub/Epub/blocks/TextBlock.h | 5 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 20 ++-- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 4 +- lib/FsHelpers/FsHelpers.cpp | 112 ++++++++++++++++++ lib/FsHelpers/FsHelpers.h | 14 +++ lib/Serialization/Serialization.h | 25 ++++ src/CrossPointSettings.cpp | 18 +-- src/CrossPointState.cpp | 17 ++- src/WifiCredentialStore.cpp | 20 +--- src/activities/boot_sleep/SleepActivity.cpp | 13 +- src/activities/reader/EpubReaderActivity.cpp | 24 ++-- src/main.cpp | 7 +- src/network/CrossPointWebServer.cpp | 4 +- 20 files changed, 315 insertions(+), 221 deletions(-) delete mode 100644 lib/Epub/Epub/FsHelpers.cpp delete mode 100644 lib/Epub/Epub/FsHelpers.h create mode 100644 lib/FsHelpers/FsHelpers.cpp create mode 100644 lib/FsHelpers/FsHelpers.h diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index d959cb7..010f000 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -1,5 +1,6 @@ #include "Epub.h" +#include #include #include #include @@ -7,7 +8,6 @@ #include -#include "Epub/FsHelpers.h" #include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContentOpfParser.h" #include "Epub/parsers/TocNcxParser.h" @@ -95,10 +95,15 @@ bool Epub::parseTocNcxFile() { Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str()); const auto tmpNcxPath = getCachePath() + "/toc.ncx"; - File tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_WRITE); + File tempNcxFile; + if (!FsHelpers::openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { + return false; + } readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); tempNcxFile.close(); - tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_READ); + if (!FsHelpers::openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { + return false; + } const auto ncxSize = tempNcxFile.size(); TocNcxParser ncxParser(contentBasePath, ncxSize); @@ -235,16 +240,28 @@ bool Epub::generateCoverBmp() const { if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" || coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") { Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis()); - File coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_WRITE, true); + const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; + + File coverJpg; + if (!FsHelpers::openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + return false; + } readItemContentsToStream(coverImageItem, coverJpg, 1024); coverJpg.close(); - coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_READ); - File coverBmp = SD.open(getCoverBmpPath().c_str(), FILE_WRITE, true); + if (!FsHelpers::openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + return false; + } + + File coverBmp; + if (!FsHelpers::openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) { + coverJpg.close(); + return false; + } const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp); coverJpg.close(); coverBmp.close(); - SD.remove((getCachePath() + "/.cover.jpg").c_str()); + SD.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis()); @@ -259,45 +276,9 @@ bool Epub::generateCoverBmp() const { return false; } -std::string normalisePath(const std::string& path) { - std::vector components; - std::string component; - - for (const auto c : path) { - if (c == '/') { - if (!component.empty()) { - if (component == "..") { - if (!components.empty()) { - components.pop_back(); - } - } else { - components.push_back(component); - } - component.clear(); - } - } else { - component += c; - } - } - - if (!component.empty()) { - components.push_back(component); - } - - std::string result; - for (const auto& c : components) { - if (!result.empty()) { - result += "/"; - } - result += c; - } - - return result; -} - uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, bool trailingNullByte) const { const ZipFile zip("/sd" + filepath); - const std::string path = normalisePath(itemHref); + const std::string path = FsHelpers::normalisePath(itemHref); const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte); if (!content) { @@ -310,7 +291,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const { const ZipFile zip("/sd" + filepath); - const std::string path = normalisePath(itemHref); + const std::string path = FsHelpers::normalisePath(itemHref); return zip.readFileToStream(path.c_str(), out, chunkSize); } @@ -321,7 +302,7 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { } bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size) { - const std::string path = normalisePath(itemHref); + const std::string path = FsHelpers::normalisePath(itemHref); return zip.getInflatedFileSize(path.c_str(), size); } @@ -349,18 +330,18 @@ std::string& Epub::getSpineItem(const int spineIndex) { return spine.at(spineIndex).second; } -EpubTocEntry& Epub::getTocItem(const int tocTndex) { +EpubTocEntry& Epub::getTocItem(const int tocIndex) { static EpubTocEntry emptyEntry = {}; if (toc.empty()) { Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis()); return emptyEntry; } - if (tocTndex < 0 || tocTndex >= static_cast(toc.size())) { - Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex); + if (tocIndex < 0 || tocIndex >= static_cast(toc.size())) { + Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex); return toc.at(0); } - return toc.at(tocTndex); + return toc.at(tocIndex); } int Epub::getTocItemsCount() const { return toc.size(); } diff --git a/lib/Epub/Epub/FsHelpers.cpp b/lib/Epub/Epub/FsHelpers.cpp deleted file mode 100644 index 5287252..0000000 --- a/lib/Epub/Epub/FsHelpers.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "FsHelpers.h" - -#include - -bool FsHelpers::removeDir(const char* path) { - // 1. Open the directory - File dir = SD.open(path); - if (!dir) { - return false; - } - if (!dir.isDirectory()) { - return false; - } - - File file = dir.openNextFile(); - while (file) { - String filePath = path; - if (!filePath.endsWith("/")) { - filePath += "/"; - } - filePath += file.name(); - - if (file.isDirectory()) { - if (!removeDir(filePath.c_str())) { - return false; - } - } else { - if (!SD.remove(filePath.c_str())) { - return false; - } - } - file = dir.openNextFile(); - } - - return SD.rmdir(path); -} diff --git a/lib/Epub/Epub/FsHelpers.h b/lib/Epub/Epub/FsHelpers.h deleted file mode 100644 index bc5204b..0000000 --- a/lib/Epub/Epub/FsHelpers.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -class FsHelpers { - public: - static bool removeDir(const char* path); -}; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 01bb3ac..b41dd3c 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -9,21 +9,21 @@ constexpr uint8_t PAGE_FILE_VERSION = 3; void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } -void PageLine::serialize(std::ostream& os) { - serialization::writePod(os, xPos); - serialization::writePod(os, yPos); +void PageLine::serialize(File& file) { + serialization::writePod(file, xPos); + serialization::writePod(file, yPos); // serialize TextBlock pointed to by PageLine - block->serialize(os); + block->serialize(file); } -std::unique_ptr PageLine::deserialize(std::istream& is) { +std::unique_ptr PageLine::deserialize(File& file) { int16_t xPos; int16_t yPos; - serialization::readPod(is, xPos); - serialization::readPod(is, yPos); + serialization::readPod(file, xPos); + serialization::readPod(file, yPos); - auto tb = TextBlock::deserialize(is); + auto tb = TextBlock::deserialize(file); return std::unique_ptr(new PageLine(std::move(tb), xPos, yPos)); } @@ -33,22 +33,22 @@ void Page::render(GfxRenderer& renderer, const int fontId) const { } } -void Page::serialize(std::ostream& os) const { - serialization::writePod(os, PAGE_FILE_VERSION); +void Page::serialize(File& file) const { + serialization::writePod(file, PAGE_FILE_VERSION); const uint32_t count = elements.size(); - serialization::writePod(os, count); + serialization::writePod(file, count); for (const auto& el : elements) { // Only PageLine exists currently - serialization::writePod(os, static_cast(TAG_PageLine)); - el->serialize(os); + serialization::writePod(file, static_cast(TAG_PageLine)); + el->serialize(file); } } -std::unique_ptr Page::deserialize(std::istream& is) { +std::unique_ptr Page::deserialize(File& file) { uint8_t version; - serialization::readPod(is, version); + serialization::readPod(file, version); if (version != PAGE_FILE_VERSION) { Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version); return nullptr; @@ -57,14 +57,14 @@ std::unique_ptr Page::deserialize(std::istream& is) { auto page = std::unique_ptr(new Page()); uint32_t count; - serialization::readPod(is, count); + serialization::readPod(file, count); for (uint32_t i = 0; i < count; i++) { uint8_t tag; - serialization::readPod(is, tag); + serialization::readPod(file, tag); if (tag == TAG_PageLine) { - auto pl = PageLine::deserialize(is); + auto pl = PageLine::deserialize(file); page->elements.push_back(std::move(pl)); } else { Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag); diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 59333ce..1026653 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -1,4 +1,6 @@ #pragma once +#include + #include #include @@ -16,7 +18,7 @@ class PageElement { explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} virtual ~PageElement() = default; virtual void render(GfxRenderer& renderer, int fontId) = 0; - virtual void serialize(std::ostream& os) = 0; + virtual void serialize(File& file) = 0; }; // a line from a block element @@ -27,8 +29,8 @@ class PageLine final : public PageElement { PageLine(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), block(std::move(block)) {} void render(GfxRenderer& renderer, int fontId) override; - void serialize(std::ostream& os) override; - static std::unique_ptr deserialize(std::istream& is); + void serialize(File& file) override; + static std::unique_ptr deserialize(File& file); }; class Page { @@ -36,6 +38,6 @@ class Page { // the list of block index and line numbers on this page std::vector> elements; void render(GfxRenderer& renderer, int fontId) const; - void serialize(std::ostream& os) const; - static std::unique_ptr deserialize(std::istream& is); + void serialize(File& file) const; + static std::unique_ptr deserialize(File& file); }; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7c9d241..ec2993f 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -1,11 +1,9 @@ #include "Section.h" +#include #include #include -#include - -#include "FsHelpers.h" #include "Page.h" #include "parsers/ChapterHtmlSlimParser.h" @@ -16,7 +14,10 @@ constexpr uint8_t SECTION_FILE_VERSION = 5; void Section::onPageComplete(std::unique_ptr page) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; - std::ofstream outputFile("/sd" + filePath); + File outputFile; + if (!FsHelpers::openFileForWrite("SCT", filePath, outputFile)) { + return; + } page->serialize(outputFile); outputFile.close(); @@ -28,7 +29,10 @@ void Section::onPageComplete(std::unique_ptr page) { void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, const bool extraParagraphSpacing) const { - std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str()); + File outputFile; + if (!FsHelpers::openFileForWrite("SCT", cachePath + "/section.bin", outputFile)) { + return; + } serialization::writePod(outputFile, SECTION_FILE_VERSION); serialization::writePod(outputFile, fontId); serialization::writePod(outputFile, lineCompression); @@ -44,17 +48,12 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression, bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, const bool extraParagraphSpacing) { - if (!SD.exists(cachePath.c_str())) { - return false; - } - const auto sectionFilePath = cachePath + "/section.bin"; - if (!SD.exists(sectionFilePath.c_str())) { + File inputFile; + if (!FsHelpers::openFileForRead("SCT", sectionFilePath, inputFile)) { return false; } - std::ifstream inputFile(("/sd" + sectionFilePath).c_str()); - // Match parameters { uint8_t version; @@ -119,13 +118,13 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const bool extraParagraphSpacing) { const auto localPath = epub->getSpineItem(spineIndex); - // TODO: Should we get rid of this file all together? - // It currently saves us a bit of memory by allowing for all the inflation bits to be released - // before loading the XML parser const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; - File f = SD.open(tmpHtmlPath.c_str(), FILE_WRITE, true); - bool success = epub->readItemContentsToStream(localPath, f, 1024); - f.close(); + File tmpHtml; + if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { + return false; + } + bool success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); + tmpHtml.close(); if (!success) { Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis()); @@ -134,10 +133,8 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str()); - const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; - - ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, - marginBottom, marginLeft, extraParagraphSpacing, + ChapterHtmlSlimParser visitor(tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, + marginLeft, extraParagraphSpacing, [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }); success = visitor.parseAndBuildPages(); @@ -153,13 +150,12 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, } std::unique_ptr Section::loadPageFromSD() const { - const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin"; - if (!SD.exists(filePath.c_str() + 3)) { - Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str()); + const auto filePath = cachePath + "/page_" + std::to_string(currentPage) + ".bin"; + + File inputFile; + if (!FsHelpers::openFileForRead("SCT", filePath, inputFile)) { return nullptr; } - - std::ifstream inputFile(filePath); auto page = Page::deserialize(inputFile); inputFile.close(); return page; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index cc3cb60..bb8b14e 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -17,27 +17,27 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int } } -void TextBlock::serialize(std::ostream& os) const { +void TextBlock::serialize(File& file) const { // words const uint32_t wc = words.size(); - serialization::writePod(os, wc); - for (const auto& w : words) serialization::writeString(os, w); + serialization::writePod(file, wc); + for (const auto& w : words) serialization::writeString(file, w); // wordXpos const uint32_t xc = wordXpos.size(); - serialization::writePod(os, xc); - for (auto x : wordXpos) serialization::writePod(os, x); + serialization::writePod(file, xc); + for (auto x : wordXpos) serialization::writePod(file, x); // wordStyles const uint32_t sc = wordStyles.size(); - serialization::writePod(os, sc); - for (auto s : wordStyles) serialization::writePod(os, s); + serialization::writePod(file, sc); + for (auto s : wordStyles) serialization::writePod(file, s); // style - serialization::writePod(os, style); + serialization::writePod(file, style); } -std::unique_ptr TextBlock::deserialize(std::istream& is) { +std::unique_ptr TextBlock::deserialize(File& file) { uint32_t wc, xc, sc; std::list words; std::list wordXpos; @@ -45,22 +45,22 @@ std::unique_ptr TextBlock::deserialize(std::istream& is) { BLOCK_STYLE style; // words - serialization::readPod(is, wc); + serialization::readPod(file, wc); words.resize(wc); - for (auto& w : words) serialization::readString(is, w); + for (auto& w : words) serialization::readString(file, w); // wordXpos - serialization::readPod(is, xc); + serialization::readPod(file, xc); wordXpos.resize(xc); - for (auto& x : wordXpos) serialization::readPod(is, x); + for (auto& x : wordXpos) serialization::readPod(file, x); // wordStyles - serialization::readPod(is, sc); + serialization::readPod(file, sc); wordStyles.resize(sc); - for (auto& s : wordStyles) serialization::readPod(is, s); + for (auto& s : wordStyles) serialization::readPod(file, s); // style - serialization::readPod(is, style); + serialization::readPod(file, style); return std::unique_ptr(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style)); } diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 4b2b031..46e320e 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -35,6 +36,6 @@ class TextBlock final : public Block { // given a renderer works out where to break the words into lines void render(const GfxRenderer& renderer, int fontId, int x, int y) const; BlockType getType() override { return TEXT_BLOCK; } - void serialize(std::ostream& os) const; - static std::unique_ptr deserialize(std::istream& is); + void serialize(File& file) const; + static std::unique_ptr deserialize(File& file); }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index a629707..4a9b86c 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -1,5 +1,6 @@ #include "ChapterHtmlSlimParser.h" +#include #include #include #include @@ -214,9 +215,8 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } - FILE* file = fopen(filepath, "r"); - if (!file) { - Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath); + File file; + if (!FsHelpers::openFileForRead("EHP", filepath, file)) { XML_ParserFree(parser); return false; } @@ -233,23 +233,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - fclose(file); + file.close(); return false; } - const size_t len = fread(buf, 1, 1024, file); + const size_t len = file.read(static_cast(buf), 1024); - if (ferror(file)) { + if (len == 0) { Serial.printf("[%lu] [EHP] File read error\n", millis()); XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - fclose(file); + file.close(); return false; } - done = feof(file); + done = file.available() == 0; if (XML_ParseBuffer(parser, static_cast(len), done) == XML_STATUS_ERROR) { Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser), @@ -258,7 +258,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - fclose(file); + file.close(); return false; } } while (!done); @@ -267,7 +267,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - fclose(file); + file.close(); // Process last page if there is still text if (currentTextBlock) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index f656b4a..7f74602 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -15,7 +15,7 @@ class GfxRenderer; #define MAX_WORD_SIZE 200 class ChapterHtmlSlimParser { - const char* filepath; + const std::string& filepath; GfxRenderer& renderer; std::function)> completePageFn; int depth = 0; @@ -45,7 +45,7 @@ class ChapterHtmlSlimParser { static void XMLCALL endElement(void* userData, const XML_Char* name); public: - explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId, + explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, const bool extraParagraphSpacing, const std::function)>& completePageFn) diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp new file mode 100644 index 0000000..06f3dfe --- /dev/null +++ b/lib/FsHelpers/FsHelpers.cpp @@ -0,0 +1,112 @@ +#include "FsHelpers.h" + +#include + +#include + +bool FsHelpers::openFileForRead(const char* moduleName, const char* path, File& file) { + if (!SD.exists(path)) { + return false; + } + + file = SD.open(path, FILE_READ); + if (!file) { + Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), moduleName, path); + return false; + } + return true; +} + +bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path, File& file) { + return openFileForRead(moduleName, path.c_str(), file); +} + +bool FsHelpers::openFileForRead(const char* moduleName, const String& path, File& file) { + return openFileForRead(moduleName, path.c_str(), file); +} + +bool FsHelpers::openFileForWrite(const char* moduleName, const char* path, File& file) { + file = SD.open(path, FILE_WRITE, true); + if (!file) { + Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path); + return false; + } + return true; +} + +bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) { + return openFileForWrite(moduleName, path.c_str(), file); +} + +bool FsHelpers::openFileForWrite(const char* moduleName, const String& path, File& file) { + return openFileForWrite(moduleName, path.c_str(), file); +} + +bool FsHelpers::removeDir(const char* path) { + // 1. Open the directory + File dir = SD.open(path); + if (!dir) { + return false; + } + if (!dir.isDirectory()) { + return false; + } + + File file = dir.openNextFile(); + while (file) { + String filePath = path; + if (!filePath.endsWith("/")) { + filePath += "/"; + } + filePath += file.name(); + + if (file.isDirectory()) { + if (!removeDir(filePath.c_str())) { + return false; + } + } else { + if (!SD.remove(filePath.c_str())) { + return false; + } + } + file = dir.openNextFile(); + } + + return SD.rmdir(path); +} + +std::string FsHelpers::normalisePath(const std::string& path) { + std::vector components; + std::string component; + + for (const auto c : path) { + if (c == '/') { + if (!component.empty()) { + if (component == "..") { + if (!components.empty()) { + components.pop_back(); + } + } else { + components.push_back(component); + } + component.clear(); + } + } else { + component += c; + } + } + + if (!component.empty()) { + components.push_back(component); + } + + std::string result; + for (const auto& c : components) { + if (!result.empty()) { + result += "/"; + } + result += c; + } + + return result; +} diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h new file mode 100644 index 0000000..0dff145 --- /dev/null +++ b/lib/FsHelpers/FsHelpers.h @@ -0,0 +1,14 @@ +#pragma once +#include + +class FsHelpers { + public: + static bool openFileForRead(const char* moduleName, const char* path, File& file); + static bool openFileForRead(const char* moduleName, const std::string& path, File& file); + static bool openFileForRead(const char* moduleName, const String& path, File& file); + static bool openFileForWrite(const char* moduleName, const char* path, File& file); + static bool openFileForWrite(const char* moduleName, const std::string& path, File& file); + static bool openFileForWrite(const char* moduleName, const String& path, File& file); + static bool removeDir(const char* path); + static std::string normalisePath(const std::string& path); +}; diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index 20eb4a4..e6bcbf2 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -1,4 +1,6 @@ #pragma once +#include + #include namespace serialization { @@ -7,21 +9,44 @@ static void writePod(std::ostream& os, const T& value) { os.write(reinterpret_cast(&value), sizeof(T)); } +template +static void writePod(File& file, const T& value) { + file.write(reinterpret_cast(&value), sizeof(T)); +} + template static void readPod(std::istream& is, T& value) { is.read(reinterpret_cast(&value), sizeof(T)); } +template +static void readPod(File& file, T& value) { + file.read(reinterpret_cast(&value), sizeof(T)); +} + static void writeString(std::ostream& os, const std::string& s) { const uint32_t len = s.size(); writePod(os, len); os.write(s.data(), len); } +static void writeString(File& file, const std::string& s) { + const uint32_t len = s.size(); + writePod(file, len); + file.write(reinterpret_cast(s.data()), len); +} + static void readString(std::istream& is, std::string& s) { uint32_t len; readPod(is, len); s.resize(len); is.read(&s[0], len); } + +static void readString(File& file, std::string& s) { + uint32_t len; + readPod(file, len); + s.resize(len); + file.read(reinterpret_cast(&s[0]), len); +} } // namespace serialization diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index fe5e2a0..467ee9c 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -1,26 +1,28 @@ #include "CrossPointSettings.h" +#include #include #include #include -#include -#include - // Initialize the static instance CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_COUNT = 3; -constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; +constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace bool CrossPointSettings::saveToFile() const { // Make sure the directory exists SD.mkdir("/.crosspoint"); - std::ofstream outputFile(SETTINGS_FILE); + File outputFile; + if (!FsHelpers::openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { + return false; + } + serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, sleepScreen); @@ -33,13 +35,11 @@ bool CrossPointSettings::saveToFile() const { } bool CrossPointSettings::loadFromFile() { - if (!SD.exists(SETTINGS_FILE + 3)) { // +3 to skip "/sd" prefix - Serial.printf("[%lu] [CPS] Settings file does not exist, using defaults\n", millis()); + File inputFile; + if (!FsHelpers::openFileForRead("CPS", SETTINGS_FILE, inputFile)) { return false; } - std::ifstream inputFile(SETTINGS_FILE); - uint8_t version; serialization::readPod(inputFile, version); if (version != SETTINGS_FILE_VERSION) { diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index dd96593..9010822 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -1,20 +1,22 @@ #include "CrossPointState.h" +#include #include -#include #include -#include - namespace { constexpr uint8_t STATE_FILE_VERSION = 1; -constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin"; +constexpr char STATE_FILE[] = "/.crosspoint/state.bin"; } // namespace CrossPointState CrossPointState::instance; bool CrossPointState::saveToFile() const { - std::ofstream outputFile(STATE_FILE); + File outputFile; + if (!FsHelpers::openFileForWrite("CPS", STATE_FILE, outputFile)) { + return false; + } + serialization::writePod(outputFile, STATE_FILE_VERSION); serialization::writeString(outputFile, openEpubPath); outputFile.close(); @@ -22,7 +24,10 @@ bool CrossPointState::saveToFile() const { } bool CrossPointState::loadFromFile() { - std::ifstream inputFile(STATE_FILE); + File inputFile; + if (!FsHelpers::openFileForRead("CPS", STATE_FILE, inputFile)) { + return false; + } uint8_t version; serialization::readPod(inputFile, version); diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index 7df9e2f..856098f 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -1,11 +1,10 @@ #include "WifiCredentialStore.h" +#include #include #include #include -#include - // Initialize the static instance WifiCredentialStore WifiCredentialStore::instance; @@ -14,7 +13,7 @@ namespace { constexpr uint8_t WIFI_FILE_VERSION = 1; // WiFi credentials file path -constexpr char WIFI_FILE[] = "/sd/.crosspoint/wifi.bin"; +constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin"; // Obfuscation key - "CrossPoint" in ASCII // This is NOT cryptographic security, just prevents casual file reading @@ -33,9 +32,8 @@ bool WifiCredentialStore::saveToFile() const { // Make sure the directory exists SD.mkdir("/.crosspoint"); - std::ofstream file(WIFI_FILE, std::ios::binary); - if (!file) { - Serial.printf("[%lu] [WCS] Failed to open wifi.bin for writing\n", millis()); + File file; + if (!FsHelpers::openFileForWrite("WCS", WIFI_FILE, file)) { return false; } @@ -62,14 +60,8 @@ bool WifiCredentialStore::saveToFile() const { } bool WifiCredentialStore::loadFromFile() { - if (!SD.exists(WIFI_FILE + 3)) { // +3 to skip "/sd" prefix - Serial.printf("[%lu] [WCS] WiFi credentials file does not exist\n", millis()); - return false; - } - - std::ifstream file(WIFI_FILE, std::ios::binary); - if (!file) { - Serial.printf("[%lu] [WCS] Failed to open wifi.bin for reading\n", millis()); + File file; + if (!FsHelpers::openFileForRead("WCS", WIFI_FILE, file)) { return false; } diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index ca72aeb..4bc70f5 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -1,6 +1,7 @@ #include "SleepActivity.h" #include +#include #include #include @@ -76,8 +77,8 @@ void SleepActivity::renderCustomSleepScreen() const { // Generate a random number between 1 and numFiles const auto randomFileIndex = random(numFiles); const auto filename = "/sleep/" + files[randomFileIndex]; - auto file = SD.open(filename.c_str()); - if (file) { + File file; + if (FsHelpers::openFileForRead("SLP", filename, file)) { Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); delay(100); Bitmap bitmap(file); @@ -93,8 +94,8 @@ void SleepActivity::renderCustomSleepScreen() const { // Look for sleep.bmp on the root of the sd card to determine if we should // render a custom sleep screen instead of the default. - auto file = SD.open("/sleep.bmp"); - if (file) { + File file; + if (FsHelpers::openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); @@ -186,8 +187,8 @@ void SleepActivity::renderCoverSleepScreen() const { return renderDefaultSleepScreen(); } - auto file = SD.open(lastEpub.getCoverBmpPath().c_str(), FILE_READ); - if (file) { + File file; + if (FsHelpers::openFileForRead("SLP", lastEpub.getCoverBmpPath(), file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index fd9a813..7843f5b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1,9 +1,9 @@ #include "EpubReaderActivity.h" #include +#include #include #include -#include #include "Battery.h" #include "CrossPointSettings.h" @@ -37,8 +37,8 @@ void EpubReaderActivity::onEnter() { epub->setupCacheDir(); - File f = SD.open((epub->getCachePath() + "/progress.bin").c_str()); - if (f) { + File f; + if (FsHelpers::openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentSpineIndex = data[0] + (data[1] << 8); @@ -282,14 +282,16 @@ void EpubReaderActivity::renderScreen() { Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); } - File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE); - uint8_t data[4]; - data[0] = currentSpineIndex & 0xFF; - data[1] = (currentSpineIndex >> 8) & 0xFF; - data[2] = section->currentPage & 0xFF; - data[3] = (section->currentPage >> 8) & 0xFF; - f.write(data, 4); - f.close(); + File f; + if (FsHelpers::openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { + uint8_t data[4]; + data[0] = currentSpineIndex & 0xFF; + data[1] = (currentSpineIndex >> 8) & 0xFF; + data[2] = section->currentPage & 0xFF; + data[3] = (section->currentPage >> 8) & 0xFF; + f.write(data, 4); + f.close(); + } } void EpubReaderActivity::renderContents(std::unique_ptr page) { diff --git a/src/main.cpp b/src/main.cpp index cf74479..03f5150 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -160,7 +160,12 @@ void onGoHome() { void setup() { t1 = millis(); - Serial.begin(115200); + + // Only start serial if USB connected + pinMode(UART0_RXD, INPUT); + if (digitalRead(UART0_RXD) == HIGH) { + Serial.begin(115200); + } Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index f14081f..041273f 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1,6 +1,7 @@ #include "CrossPointWebServer.h" #include +#include #include #include @@ -339,8 +340,7 @@ void CrossPointWebServer::handleUpload() const { } // Open file for writing - uploadFile = SD.open(filePath.c_str(), FILE_WRITE); - if (!uploadFile) { + if (!FsHelpers::openFileForWrite("WEB", filePath, uploadFile)) { uploadError = "Failed to create file on SD card"; Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str()); return;