From 8f3df7e10ea82f4b22373f27ef05e92074803d71 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 12 Jan 2026 23:57:34 +1000 Subject: [PATCH] fix: Handle EPUB 3 TOC to spine mapping when nav file in subdirectory (#332) ## Summary - Nav file in EPUB 3 file is a HTML file with relative hrefs - If this file exists anywhere but in the same location as the content.opf file, navigating in the book will fail - Bump the book cache version to rebuild potentially broken books ## Additional Context - Fixes https://github.com/daveallie/crosspoint-reader/issues/264 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? - [ ] Yes - [ ] Partially - [x] No --- lib/Epub/Epub.cpp | 5 ++++- lib/Epub/Epub/BookMetadataCache.cpp | 2 +- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 4 ++-- lib/Epub/Epub/parsers/TocNavParser.cpp | 3 ++- lib/Epub/Epub/parsers/TocNcxParser.cpp | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 9c4b058..64727bc 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -167,7 +167,10 @@ bool Epub::parseTocNavFile() const { } const auto navSize = tempNavFile.size(); - TocNavParser navParser(contentBasePath, navSize, bookMetadataCache.get()); + // Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf + // and the HTMLX nav file will have hrefs relative to itself + const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1); + TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get()); if (!navParser.setup()) { Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis()); diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 06b4f45..52e4809 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -9,7 +9,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 3; +constexpr uint8_t BOOK_CACHE_VERSION = 4; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 2c90d01..aee7e57 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -167,7 +167,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (strcmp(atts[i], "id") == 0) { itemId = atts[i + 1]; } else if (strcmp(atts[i], "href") == 0) { - href = self->baseContentPath + atts[i + 1]; + href = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]); } else if (strcmp(atts[i], "media-type") == 0) { mediaType = atts[i + 1]; } else if (strcmp(atts[i], "properties") == 0) { @@ -243,7 +243,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name break; } } else if (strcmp(atts[i], "href") == 0) { - textHref = self->baseContentPath + atts[i + 1]; + textHref = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]); } } if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) { diff --git a/lib/Epub/Epub/parsers/TocNavParser.cpp b/lib/Epub/Epub/parsers/TocNavParser.cpp index b8a4e7f..454b243 100644 --- a/lib/Epub/Epub/parsers/TocNavParser.cpp +++ b/lib/Epub/Epub/parsers/TocNavParser.cpp @@ -1,5 +1,6 @@ #include "TocNavParser.h" +#include #include #include "../BookMetadataCache.h" @@ -140,7 +141,7 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) { if (strcmp(name, "a") == 0 && self->state == IN_ANCHOR) { // Create TOC entry when closing anchor tag (we have all data now) if (!self->currentLabel.empty() && !self->currentHref.empty()) { - std::string href = self->baseContentPath + self->currentHref; + std::string href = FsHelpers::normalisePath(self->baseContentPath + self->currentHref); std::string anchor; const size_t pos = href.find('#'); diff --git a/lib/Epub/Epub/parsers/TocNcxParser.cpp b/lib/Epub/Epub/parsers/TocNcxParser.cpp index b1fbb2f..3e59451 100644 --- a/lib/Epub/Epub/parsers/TocNcxParser.cpp +++ b/lib/Epub/Epub/parsers/TocNcxParser.cpp @@ -1,5 +1,6 @@ #include "TocNcxParser.h" +#include #include #include "../BookMetadataCache.h" @@ -159,7 +160,7 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) { // This is the safest place to push the data, assuming always comes before . // NCX spec says navLabel comes before content. if (!self->currentLabel.empty() && !self->currentSrc.empty()) { - std::string href = self->baseContentPath + self->currentSrc; + std::string href = FsHelpers::normalisePath(self->baseContentPath + self->currentSrc); std::string anchor; const size_t pos = href.find('#');