diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index d344ad73..2688f334 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -22,6 +22,13 @@ constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t PARSE_BUFFER_SIZE = 1024; +// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation +// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per +// chapter. A runaway count usually means a converter injected machine-generated IDs on +// every text fragment (e.g. Kobo KePub spans). The cap prevents unbounded heap growth +// on resource-constrained devices (~380KB heap). TOC anchors bypass this cap. +constexpr size_t MAX_ANCHORS_PER_CHAPTER = 1024; + constexpr const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr const char* BOLD_TAGS[] = {"b", "strong"}; @@ -49,6 +56,14 @@ const char* getAttribute(const XML_Char** atts, const char* attrName) { return nullptr; } +// Returns true if the HTML element is a purely inline, non-navigable wrapper. +// IDs on these elements are never meaningful navigation targets in epub content. +// Reading-system converters (Kobo KePub, Calibre, etc.) frequently inject thousands +// of such IDs for progress tracking or internal bookkeeping, and recording each one +// as a navigation anchor exhausts the heap on memory-constrained devices. +// Block-level, sectioning, and structural elements are always considered navigable. +bool isNonNavigableInlineElement(const char* name) { return strcmp(name, "span") == 0; } + bool isInternalEpubLink(const char* href) { if (!href || href[0] == '\0') return false; if (strncmp(href, "http://", 7) == 0 || strncmp(href, "https://", 8) == 0) return false; @@ -293,7 +308,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } else if (strcmp(atts[i], "id") == 0) { // Defer both anchor recording and TOC page breaks until startNewTextBlock, // after the previous block is flushed to pages via makePages(). - self->pendingAnchorId = atts[i + 1]; + // + // Skip IDs on non-navigable inline elements (e.g. ): these are never + // link targets in epub content, but reading-system converters can inject tens + // of thousands of them per chapter, exhausting the heap. TOC anchors are + // always recorded regardless of element type, since they drive page breaks. + const char* idValue = atts[i + 1]; + const bool isTocAnchor = + std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end(); + if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) { + self->pendingAnchorId = idValue; + } } else if (strcmp(atts[i], "dir") == 0) { dirAttr = atts[i + 1]; }