Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
uses: actions/cache@v4
with:
path: build/test/_deps/googletest-src
key: ${{ runner.os }}-googletest-v1.15.2
key: ${{ runner.os }}-googletest-${{ hashFiles('test/CMakeLists.txt') }}

- name: Configure
run: cmake -S test -B build/test -G Ninja -DCMAKE_BUILD_TYPE=Release
Expand Down
41 changes: 40 additions & 1 deletion lib/Epub/Epub.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,38 @@ bool Epub::parseTocNavFile() const {
return true;
}

void Epub::discoverCssFilesFromZip() {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
LOG_ERR("EBP", "Cannot discover CSS from ZIP because book metadata cache is not loaded");
return;
}

ZipFile zf(filepath);

if (!zf.loadAllFileStatSlims()) {
LOG_ERR("EBP", "Failed to load ZIP file stat slims for CSS discovery");
return;
}

size_t lastSlash = contentBasePath.find_last_of('/');

std::string opfDir = (lastSlash != std::string::npos) ? contentBasePath.substr(0, lastSlash + 1) : "";

zf.enumerateFilePaths([&](std::string_view filePath) {
if (!opfDir.empty() && filePath.find(opfDir) != 0) {
return; // Skip files that are not in the same directory as OPF manifest, as CSS files are typically located
// there or in subfolders
}

if (FsHelpers::hasCssExtension(filePath)) {
if (std::find(cssFiles.begin(), cssFiles.end(), filePath) == cssFiles.end()) {
LOG_DBG("EBP", "Discovered CSS file via ZIP enumeration: %.*s", (int)filePath.size(), filePath.data());
cssFiles.push_back(std::string{filePath});
}
}
});
}

void Epub::parseCssFiles() const {
// Maximum CSS file size we'll attempt to parse (uncompressed)
// Larger files risk memory exhaustion on ESP32
Expand Down Expand Up @@ -330,9 +362,9 @@ void Epub::parseCssFiles() const {
if (!cssParser->saveToCache()) {
LOG_ERR("EBP", "Failed to save CSS rules to cache");
}
cssParser->clear();

LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
cssParser->clear();
}

// load in the meta data for the epub file
Expand All @@ -355,6 +387,10 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
// continue anyway - book will work without CSS and we'll still load any inline style CSS
} else {
// Handle case where CSS files are not listed in OPF manifest
// but are still referenced by HTML files - discover and parse them too
discoverCssFilesFromZip();
}
parseCssFiles();
// Invalidate section caches so they are rebuilt with the new CSS
Expand Down Expand Up @@ -458,6 +494,9 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
}

if (!skipLoadingCss) {
// Handle case where CSS files are not listed in OPF manifest
// but are still referenced by HTML files - discover and parse them too
discoverCssFilesFromZip();
// Parse CSS files after cache reload
parseCssFiles();
Storage.removeDir((cachePath + "/sections").c_str());
Expand Down
1 change: 1 addition & 0 deletions lib/Epub/Epub.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Epub {
bool parseTocNcxFile() const;
bool parseTocNavFile() const;
void parseCssFiles() const;
void discoverCssFilesFromZip();

public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
Expand Down
2 changes: 2 additions & 0 deletions lib/FsHelpers/FsHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(file

bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); }

bool hasCssExtension(std::string_view fileName) { return checkFileExtension(fileName, ".css"); }

std::string extractFolderPath(const std::string& filePath) {
const auto lastSlash = filePath.find_last_of('/');
if (lastSlash == std::string::npos || lastSlash == 0) {
Expand Down
3 changes: 3 additions & 0 deletions lib/FsHelpers/FsHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ bool hasTxtExtension(std::string_view fileName);
// Check for .md extension (case-insensitive)
bool hasMarkdownExtension(std::string_view fileName);

// Check for .css extension (case-insensitive)
bool hasCssExtension(std::string_view fileName);

std::string extractFolderPath(const std::string& filePath);

/**
Expand Down
8 changes: 8 additions & 0 deletions lib/ZipFile/ZipFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <deque>
#include <string>
#include <string_view>
#include <unordered_map>

class ZipFile {
Expand Down Expand Up @@ -69,4 +70,11 @@ class ZipFile {
// These functions will open and close the zip as needed
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
bool readFileToStream(const char* filename, Print& out, size_t chunkSize);

template <typename F>
void enumerateFilePaths(F&& callback) const {
for (const auto& entry : fileStatSlimCache) {
callback(std::string_view{entry.first});
}
}
};
2 changes: 1 addition & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
GIT_TAG v1.17.0
)

set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
Expand Down
5 changes: 3 additions & 2 deletions test/README
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ Host-side gtest unit tests for crosspoint-reader.
Build and run:

cmake -S test -B build/test
cmake --build build/test
ctest --test-dir build/test --output-on-failure -j

Run a single suite directly:

cmake --build build/test --target StreamingJsonParserTest
build/test/streaming_json_parser/StreamingJsonParserTest --gtest_filter='*'

Google Test is fetched via CMake FetchContent (pinned to v1.15.2 in
test/CMakeLists.txt) on first configure.
Google Test is fetched via CMake FetchContent on first configure; the pinned
version lives in test/CMakeLists.txt.
8 changes: 4 additions & 4 deletions test/differential_rounding/DifferentialRoundingTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ TEST(EpdFont, KernLookup) {
}

TEST(EpdFont, GlyphLookup) {
EXPECT_NE(testFont().getGlyph('T'), nullptr);
EXPECT_NE(testFont().getGlyph('a'), nullptr);
EXPECT_NE(testFont().getGlyph('o'), nullptr);
EXPECT_NE(testFont().getGlyph('x'), nullptr);
ASSERT_NE(testFont().getGlyph('T'), nullptr);
ASSERT_NE(testFont().getGlyph('a'), nullptr);
ASSERT_NE(testFont().getGlyph('o'), nullptr);
ASSERT_NE(testFont().getGlyph('x'), nullptr);
EXPECT_EQ(testFont().getGlyph('T')->advanceX, 137);
EXPECT_EQ(testFont().getGlyph('a')->advanceX, 130);
EXPECT_EQ(testFont().getGlyph('o')->advanceX, 145);
Expand Down
7 changes: 7 additions & 0 deletions test/release_json_parser/ReleaseJsonParserTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ TEST(ReleaseJsonParser, PrettyAndMinifiedAgree) {
ReleaseJsonParser minified;
minified.feed(kRealisticMinified, strlen(kRealisticMinified));

ASSERT_TRUE(pretty.foundTag());
ASSERT_TRUE(pretty.foundFirmware());
ASSERT_TRUE(minified.foundTag());
ASSERT_TRUE(minified.foundFirmware());

EXPECT_STREQ(pretty.getTagName(), minified.getTagName());
EXPECT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl());
EXPECT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize());
Expand Down Expand Up @@ -282,6 +287,8 @@ TEST(ReleaseJsonParser, ChunkedFeedingVariousChunkSizes) {
EXPECT_TRUE(p.foundTag()) << "chunkSize=" << chunkSize;
EXPECT_TRUE(p.foundFirmware()) << "chunkSize=" << chunkSize;
EXPECT_STREQ(p.getTagName(), "v2.4.1") << "chunkSize=" << chunkSize;
EXPECT_STREQ(p.getFirmwareUrl(), "https://github.com/znelson/lightpoint/releases/download/v2.4.1/firmware.bin")
<< "chunkSize=" << chunkSize;
EXPECT_EQ(p.getFirmwareSize(), 1572864u) << "chunkSize=" << chunkSize;
}
}
Expand Down
22 changes: 11 additions & 11 deletions test/streaming_json_parser/StreamingJsonParserTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ TEST(StreamingJsonParser, UnicodeEscapeAscii) {
// \u escapes for ASCII codepoints (1-byte UTF-8) decode to the raw bytes.
auto events = parse(R"({"u": "\u0041\u0042"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, "AB");
}
Expand All @@ -163,7 +163,7 @@ TEST(StreamingJsonParser, UnicodeEscapeTwoByteUtf8) {
// U+00E9 (e-acute) decodes to the 2-byte UTF-8 sequence C3 A9.
auto events = parse(R"({"k": "caf\u00E9"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("caf\xC3\xA9"));
}
Expand All @@ -172,7 +172,7 @@ TEST(StreamingJsonParser, UnicodeEscapeThreeByteUtf8) {
// U+20AC (euro sign) decodes to the 3-byte UTF-8 sequence E2 82 AC.
auto events = parse(R"({"k": "\u20AC"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("\xE2\x82\xAC"));
}
Expand All @@ -181,7 +181,7 @@ TEST(StreamingJsonParser, UnicodeEscapeMixedHexCase) {
// Lower and upper case hex digits must both decode.
auto events = parse(R"({"k": "\u00e9\u00E9"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("\xC3\xA9\xC3\xA9"));
}
Expand All @@ -190,7 +190,7 @@ TEST(StreamingJsonParser, UnicodeEscapeSurrogateBecomesReplacement) {
// Bare surrogate values (no pair handling) emit U+FFFD (EF BF BD).
auto events = parse(R"({"k": "\uD800"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("\xEF\xBF\xBD"));
}
Expand All @@ -200,7 +200,7 @@ TEST(StreamingJsonParser, UnicodeEscapeInvalidHexBecomesReplacement) {
// reprocessed as a normal string char so the rest of the string parses.
auto events = parse(R"({"k": "\u00ZZok"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("\xEF\xBF\xBD"
"ZZok"));
Expand All @@ -209,7 +209,7 @@ TEST(StreamingJsonParser, UnicodeEscapeInvalidHexBecomesReplacement) {
TEST(StreamingJsonParser, UnicodeEscapeMixedWithLiterals) {
auto events = parse(R"({"k": "a\u00E9b"})");

ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("a\xC3\xA9"
"b"));
Expand Down Expand Up @@ -247,7 +247,7 @@ TEST(StreamingJsonParser, UnicodeEscapeSplitAcrossChunks) {
TEST(StreamingJsonParser, Numbers) {
auto events = parse(R"({"int": 42, "neg": -7, "flt": 3.14, "exp": 1e10, "nexp": -2.5E-3})");

ASSERT_GE(events.size(), 11u);
ASSERT_EQ(events.size(), 12u);
EXPECT_EQ(events[2].type, EventType::NUMBER);
EXPECT_EQ(events[2].value, "42");
EXPECT_EQ(events[4].type, EventType::NUMBER);
Expand All @@ -263,7 +263,7 @@ TEST(StreamingJsonParser, Numbers) {
TEST(StreamingJsonParser, BooleansAndNull) {
auto events = parse(R"({"t": true, "f": false, "n": null})");

ASSERT_GE(events.size(), 7u);
ASSERT_EQ(events.size(), 8u);
EXPECT_EQ(events[2].type, EventType::BOOL_TRUE);
EXPECT_EQ(events[4].type, EventType::BOOL_FALSE);
EXPECT_EQ(events[6].type, EventType::NULL_VAL);
Expand Down Expand Up @@ -454,7 +454,7 @@ TEST(StreamingJsonParser, TruncatedInputNoCrash) {

TEST(StreamingJsonParser, AllEscapeSequences) {
auto events = parse(R"({"e": "\b\f\n\r\t\"\\\/"})");
ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::STRING);
EXPECT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/"));
}
Expand Down Expand Up @@ -509,7 +509,7 @@ TEST(StreamingJsonParser, NestingOverflow) {

TEST(StreamingJsonParser, NumberZero) {
auto events = parse(R"({"z": 0})");
ASSERT_GE(events.size(), 3u);
ASSERT_EQ(events.size(), 4u);
EXPECT_EQ(events[2].type, EventType::NUMBER);
EXPECT_EQ(events[2].value, "0");
}
Expand Down
Loading