fix: skip <span> anchors (#2303)
## Summary * **What is the goal of this PR?** Prevent heap memory exhaustion caused by thousands of machine-generated anchor IDs injected by epub converters like Kobo KePub. * Fix #2292. * **What changes are included?** * **Element-type filter (Layer 1):** Introduced the `isNonNavigableInlineElement()` function in `ChapterHtmlSlimParser.cpp` to automatically skip recording IDs on `<span>` elements, as they are purely inline wrappers used for tracking and lack navigable meaning. * **Hard cap (Layer 2):** Added the `MAX_ANCHORS_PER_CHAPTER = 1024` constant to act as a fallback safety net against unbounded heap growth from unknown future ID-injection patterns on non-span elements. * **TOC Safety net:** Ensured that IDs matching known Table of Contents (TOC) entries explicitly bypass both the `<span>` filter and the 1024-anchor cap, guaranteeing that chapter page-break and core navigation logic are never compromised. --- ### 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? _**< PARTIALLY >**_
This commit is contained in:
@@ -22,6 +22,13 @@
|
|||||||
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||||
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
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* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||||
@@ -49,6 +56,14 @@ const char* getAttribute(const XML_Char** atts, const char* attrName) {
|
|||||||
return nullptr;
|
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) {
|
bool isInternalEpubLink(const char* href) {
|
||||||
if (!href || href[0] == '\0') return false;
|
if (!href || href[0] == '\0') return false;
|
||||||
if (strncmp(href, "http://", 7) == 0 || strncmp(href, "https://", 8) == 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) {
|
} else if (strcmp(atts[i], "id") == 0) {
|
||||||
// Defer both anchor recording and TOC page breaks until startNewTextBlock,
|
// Defer both anchor recording and TOC page breaks until startNewTextBlock,
|
||||||
// after the previous block is flushed to pages via makePages().
|
// after the previous block is flushed to pages via makePages().
|
||||||
self->pendingAnchorId = atts[i + 1];
|
//
|
||||||
|
// Skip IDs on non-navigable inline elements (e.g. <span>): 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) {
|
} else if (strcmp(atts[i], "dir") == 0) {
|
||||||
dirAttr = atts[i + 1];
|
dirAttr = atts[i + 1];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user