Skip to content

Commit 4786619

Browse files
kjkclaude
andcommitted
gumbo: make remaining parse-tree walkers iterative
Convert the rest of the recursive walks over gumbo parse trees to iterative form (explicit work-list / frame stack), so a pathologically deep document can't overflow the stack: GumboFindDescendantByTag, FindHttpCharsetInNode, ComicInfoVisitNode, AppendDeepText, MobiTocWalker::Walk and the CHM ToC walkers. Document order and per-node levels are preserved. Also build the ToC in -bench's CHM path so CHM ToC parsing is reproducible headlessly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e63571e commit 4786619

6 files changed

Lines changed: 232 additions & 164 deletions

File tree

src/ChmFile.cpp

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,38 @@ static bool VisitChmIndexItem(EbookTocVisitor* visitor, const GumboNode* objNode
459459
// broken CHM ToCs do. Walking every such <ul> at level + 1 reproduces the
460460
// nesting the previous, pre-gumbo parser produced (and walks each <ul> once, so
461461
// no entries are duplicated).
462+
// One suspended <ul> walk: `i` is the next child of `ul` to process at `level`.
463+
struct ChmUlFrame {
464+
const GumboNode* ul;
465+
int level;
466+
unsigned int i;
467+
};
468+
462469
static void WalkChmUl(EbookTocVisitor* visitor, const GumboNode* ulNode, bool isIndex, int level) {
463-
const GumboVector* lis = &ulNode->v.element.children;
464-
for (unsigned int j = 0; j < lis->length; j++) {
465-
const GumboNode* child = (const GumboNode*)lis->data[j];
470+
if (!ulNode) {
471+
return;
472+
}
473+
// Iterative version of the recursive <ul>/<li> walk: each stack frame holds
474+
// a <ul> and how far we've scanned its children, so a pathologically deep
475+
// ToC nesting can't overflow the call stack. Order and levels match the
476+
// recursive walk exactly (parent frame resumes after its child completes).
477+
Vec<ChmUlFrame> stack;
478+
stack.Append({ulNode, level, 0});
479+
while (stack.size() > 0) {
480+
ChmUlFrame& top = stack.Last();
481+
const GumboVector* lis = &top.ul->v.element.children;
482+
if (top.i >= lis->length) {
483+
stack.RemoveLast();
484+
continue;
485+
}
486+
const GumboNode* child = (const GumboNode*)lis->data[top.i];
487+
int lvl = top.level;
488+
top.i++;
489+
// any stack.Append() below may reallocate -> don't touch `top` after this
490+
466491
if (GumboTagNameIs(child, "ul")) {
467492
// a bare <ul> among the <li>s holds the children of the preceding <li>
468-
WalkChmUl(visitor, child, isIndex, level + 1);
493+
stack.Append({child, lvl + 1, 0});
469494
continue;
470495
}
471496
const GumboNode* li = child;
@@ -476,13 +501,13 @@ static void WalkChmUl(EbookTocVisitor* visitor, const GumboNode* ulNode, bool is
476501
if (!objNode) {
477502
continue;
478503
}
479-
bool valid = isIndex ? VisitChmIndexItem(visitor, objNode, level) : VisitChmTocItem(visitor, objNode, level);
504+
bool valid = isIndex ? VisitChmIndexItem(visitor, objNode, lvl) : VisitChmTocItem(visitor, objNode, lvl);
480505
if (!valid) {
481506
continue;
482507
}
483508
const GumboNode* nested = GumboFindChildByTag(li, "ul");
484509
if (nested) {
485-
WalkChmUl(visitor, nested, isIndex, level + 1);
510+
stack.Append({nested, lvl + 1, 0});
486511
}
487512
}
488513
}
@@ -508,26 +533,33 @@ static void WalkChmTocOrIndex(EbookTocVisitor* visitor, const GumboNode* firstUl
508533

509534
// Ignore any <ul><li> structure and visit every <object type="text/sitemap">
510535
// in document order. Used for ToCs where the list scaffolding is broken.
511-
static bool WalkBrokenChmTocOrIndex(EbookTocVisitor* visitor, const GumboNode* node, bool isIndex, bool* hadOneInOut) {
512-
if (!node) {
513-
return *hadOneInOut;
514-
}
515-
if (node->type == GUMBO_NODE_ELEMENT && GumboTagNameIs(node, "object")) {
516-
const GumboAttribute* type = gumbo_get_attribute(&node->v.element.attributes, "type");
517-
if (type && str::EqI(type->value, "text/sitemap")) {
518-
*hadOneInOut |= isIndex ? VisitChmIndexItem(visitor, node, 1) : VisitChmTocItem(visitor, node, 1);
519-
return *hadOneInOut; // don't recurse into the object's <param> children
536+
static bool WalkBrokenChmTocOrIndex(EbookTocVisitor* visitor, const GumboNode* root, bool isIndex, bool* hadOneInOut) {
537+
// iterative pre-order DFS so a deeply nested document can't overflow the stack
538+
Vec<const GumboNode*> toVisit;
539+
toVisit.Append(root);
540+
while (toVisit.size() > 0) {
541+
const GumboNode* node = toVisit.Pop();
542+
if (!node) {
543+
continue;
520544
}
521-
}
522-
const GumboVector* children = nullptr;
523-
if (node->type == GUMBO_NODE_ELEMENT) {
524-
children = &node->v.element.children;
525-
} else if (node->type == GUMBO_NODE_DOCUMENT) {
526-
children = &node->v.document.children;
527-
}
528-
if (children) {
529-
for (unsigned int i = 0; i < children->length; i++) {
530-
WalkBrokenChmTocOrIndex(visitor, (const GumboNode*)children->data[i], isIndex, hadOneInOut);
545+
if (node->type == GUMBO_NODE_ELEMENT && GumboTagNameIs(node, "object")) {
546+
const GumboAttribute* type = gumbo_get_attribute(&node->v.element.attributes, "type");
547+
if (type && str::EqI(type->value, "text/sitemap")) {
548+
*hadOneInOut |= isIndex ? VisitChmIndexItem(visitor, node, 1) : VisitChmTocItem(visitor, node, 1);
549+
continue; // don't recurse into the object's <param> children
550+
}
551+
}
552+
const GumboVector* children = nullptr;
553+
if (node->type == GUMBO_NODE_ELEMENT) {
554+
children = &node->v.element.children;
555+
} else if (node->type == GUMBO_NODE_DOCUMENT) {
556+
children = &node->v.document.children;
557+
}
558+
if (children) {
559+
// push in reverse so children are visited in document order
560+
for (unsigned int i = children->length; i > 0; i--) {
561+
toVisit.Append((const GumboNode*)children->data[i - 1]);
562+
}
531563
}
532564
}
533565
return *hadOneInOut;

src/EngineEbook.cpp

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,33 +1522,37 @@ static uint CharsetNameToCodepage(const char* charset) {
15221522
}
15231523

15241524
static uint FindHttpCharsetInNode(const GumboNode* node) {
1525-
if (!node) {
1526-
return 0;
1527-
}
1528-
if (node->type == GUMBO_NODE_ELEMENT && GumboTagNameIs(node, "meta")) {
1529-
const GumboAttribute* httpEquiv = gumbo_get_attribute(&node->v.element.attributes, "http-equiv");
1530-
if (httpEquiv && str::EqI(httpEquiv->value, "Content-Type")) {
1531-
const GumboAttribute* content = gumbo_get_attribute(&node->v.element.attributes, "content");
1532-
AutoFree mimetype, charset;
1533-
if (content && str::Parse(content->value, "%S;%_charset=%S", &mimetype, &charset)) {
1534-
uint cp = CharsetNameToCodepage(charset);
1535-
if (cp) {
1536-
return cp;
1525+
// iterative pre-order DFS so a deeply nested document can't overflow the stack
1526+
Vec<const GumboNode*> toVisit;
1527+
toVisit.Append(node);
1528+
while (toVisit.size() > 0) {
1529+
const GumboNode* n = toVisit.Pop();
1530+
if (!n) {
1531+
continue;
1532+
}
1533+
if (n->type == GUMBO_NODE_ELEMENT && GumboTagNameIs(n, "meta")) {
1534+
const GumboAttribute* httpEquiv = gumbo_get_attribute(&n->v.element.attributes, "http-equiv");
1535+
if (httpEquiv && str::EqI(httpEquiv->value, "Content-Type")) {
1536+
const GumboAttribute* content = gumbo_get_attribute(&n->v.element.attributes, "content");
1537+
AutoFree mimetype, charset;
1538+
if (content && str::Parse(content->value, "%S;%_charset=%S", &mimetype, &charset)) {
1539+
uint cp = CharsetNameToCodepage(charset);
1540+
if (cp) {
1541+
return cp;
1542+
}
15371543
}
15381544
}
15391545
}
1540-
}
1541-
const GumboVector* children = nullptr;
1542-
if (node->type == GUMBO_NODE_ELEMENT) {
1543-
children = &node->v.element.children;
1544-
} else if (node->type == GUMBO_NODE_DOCUMENT) {
1545-
children = &node->v.document.children;
1546-
}
1547-
if (children) {
1548-
for (unsigned int i = 0; i < children->length; i++) {
1549-
uint cp = FindHttpCharsetInNode((const GumboNode*)children->data[i]);
1550-
if (cp) {
1551-
return cp;
1546+
const GumboVector* children = nullptr;
1547+
if (n->type == GUMBO_NODE_ELEMENT) {
1548+
children = &n->v.element.children;
1549+
} else if (n->type == GUMBO_NODE_DOCUMENT) {
1550+
children = &n->v.document.children;
1551+
}
1552+
if (children) {
1553+
// push in reverse so children are visited in document order
1554+
for (unsigned int i = children->length; i > 0; i--) {
1555+
toVisit.Append((const GumboNode*)children->data[i - 1]);
15521556
}
15531557
}
15541558
}

src/EngineImages.cpp

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,53 +1919,60 @@ struct ComicInfoParser : json::ValueVisitor {
19191919
void Parse(const ByteSlice& xmlData);
19201920
};
19211921

1922-
static void ComicInfoVisitNode(ComicInfoParser* cip, const GumboNode* node) {
1923-
if (!node) {
1924-
return;
1925-
}
1926-
if (node->type == GUMBO_NODE_ELEMENT) {
1927-
if (GumboTagNameIs(node, "Title")) {
1928-
TempStr v = GumboTextContentTemp(node);
1929-
if (v) {
1930-
cip->Visit("/ComicBookInfo/1.0/title", v, json::Type::String);
1931-
}
1932-
} else if (GumboTagNameIs(node, "Year")) {
1933-
TempStr v = GumboTextContentTemp(node);
1934-
if (v) {
1935-
cip->Visit("/ComicBookInfo/1.0/publicationYear", v, json::Type::Number);
1936-
}
1937-
} else if (GumboTagNameIs(node, "Month")) {
1938-
TempStr v = GumboTextContentTemp(node);
1939-
if (v) {
1940-
cip->Visit("/ComicBookInfo/1.0/publicationMonth", v, json::Type::Number);
1941-
}
1942-
} else if (GumboTagNameIs(node, "Summary")) {
1943-
TempStr v = GumboTextContentTemp(node);
1944-
if (v) {
1945-
cip->Visit("/X-summary", v, json::Type::String);
1946-
}
1947-
} else if (GumboTagNameIs(node, "Writer")) {
1948-
TempStr v = GumboTextContentTemp(node);
1949-
if (v) {
1950-
cip->Visit("/ComicBookInfo/1.0/credits[0]/person", v, json::Type::String);
1951-
cip->Visit("/ComicBookInfo/1.0/credits[0]/primary", "true", json::Type::Bool);
1922+
static void ComicInfoVisitNode(ComicInfoParser* cip, const GumboNode* root) {
1923+
// iterative pre-order DFS so a deeply nested document can't overflow the stack
1924+
Vec<const GumboNode*> toVisit;
1925+
toVisit.Append(root);
1926+
while (toVisit.size() > 0) {
1927+
const GumboNode* node = toVisit.Pop();
1928+
if (!node) {
1929+
continue;
1930+
}
1931+
const GumboVector* children = nullptr;
1932+
if (node->type == GUMBO_NODE_ELEMENT) {
1933+
if (GumboTagNameIs(node, "Title")) {
1934+
TempStr v = GumboTextContentTemp(node);
1935+
if (v) {
1936+
cip->Visit("/ComicBookInfo/1.0/title", v, json::Type::String);
1937+
}
1938+
} else if (GumboTagNameIs(node, "Year")) {
1939+
TempStr v = GumboTextContentTemp(node);
1940+
if (v) {
1941+
cip->Visit("/ComicBookInfo/1.0/publicationYear", v, json::Type::Number);
1942+
}
1943+
} else if (GumboTagNameIs(node, "Month")) {
1944+
TempStr v = GumboTextContentTemp(node);
1945+
if (v) {
1946+
cip->Visit("/ComicBookInfo/1.0/publicationMonth", v, json::Type::Number);
1947+
}
1948+
} else if (GumboTagNameIs(node, "Summary")) {
1949+
TempStr v = GumboTextContentTemp(node);
1950+
if (v) {
1951+
cip->Visit("/X-summary", v, json::Type::String);
1952+
}
1953+
} else if (GumboTagNameIs(node, "Writer")) {
1954+
TempStr v = GumboTextContentTemp(node);
1955+
if (v) {
1956+
cip->Visit("/ComicBookInfo/1.0/credits[0]/person", v, json::Type::String);
1957+
cip->Visit("/ComicBookInfo/1.0/credits[0]/primary", "true", json::Type::Bool);
1958+
}
1959+
} else if (GumboTagNameIs(node, "Penciller")) {
1960+
TempStr v = GumboTextContentTemp(node);
1961+
if (v) {
1962+
cip->Visit("/ComicBookInfo/1.0/credits[1]/person", v, json::Type::String);
1963+
cip->Visit("/ComicBookInfo/1.0/credits[1]/primary", "true", json::Type::Bool);
1964+
}
19521965
}
1953-
} else if (GumboTagNameIs(node, "Penciller")) {
1954-
TempStr v = GumboTextContentTemp(node);
1955-
if (v) {
1956-
cip->Visit("/ComicBookInfo/1.0/credits[1]/person", v, json::Type::String);
1957-
cip->Visit("/ComicBookInfo/1.0/credits[1]/primary", "true", json::Type::Bool);
1966+
children = &node->v.element.children;
1967+
} else if (node->type == GUMBO_NODE_DOCUMENT) {
1968+
children = &node->v.document.children;
1969+
}
1970+
if (children) {
1971+
// push in reverse so children are visited in document order
1972+
for (unsigned int i = children->length; i > 0; i--) {
1973+
toVisit.Append((const GumboNode*)children->data[i - 1]);
19581974
}
19591975
}
1960-
const GumboVector* children = &node->v.element.children;
1961-
for (unsigned int i = 0; i < children->length; i++) {
1962-
ComicInfoVisitNode(cip, (const GumboNode*)children->data[i]);
1963-
}
1964-
} else if (node->type == GUMBO_NODE_DOCUMENT) {
1965-
const GumboVector* children = &node->v.document.children;
1966-
for (unsigned int i = 0; i < children->length; i++) {
1967-
ComicInfoVisitNode(cip, (const GumboNode*)children->data[i]);
1968-
}
19691976
}
19701977
}
19711978

src/GumboHelpers.cpp

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,28 @@ const GumboNode* GumboFindChildByTag(const GumboNode* node, const char* name) {
4646
}
4747

4848
const GumboNode* GumboFindDescendantByTag(const GumboNode* node, const char* name) {
49-
if (!node) {
50-
return nullptr;
51-
}
52-
const GumboVector* children = nullptr;
53-
if (node->type == GUMBO_NODE_ELEMENT) {
54-
if (GumboTagNameIs(node, name)) {
55-
return node;
49+
// iterative pre-order DFS so a deeply nested document can't overflow the
50+
// stack (gumbo builds the tree iteratively, but recursing over it doesn't)
51+
Vec<const GumboNode*> toVisit;
52+
toVisit.Append(node);
53+
while (toVisit.size() > 0) {
54+
const GumboNode* n = toVisit.Pop();
55+
if (!n) {
56+
continue;
5657
}
57-
children = &node->v.element.children;
58-
} else if (node->type == GUMBO_NODE_DOCUMENT) {
59-
children = &node->v.document.children;
60-
}
61-
if (children) {
62-
for (unsigned int i = 0; i < children->length; i++) {
63-
const GumboNode* found = GumboFindDescendantByTag((const GumboNode*)children->data[i], name);
64-
if (found) {
65-
return found;
58+
const GumboVector* children = nullptr;
59+
if (n->type == GUMBO_NODE_ELEMENT) {
60+
if (GumboTagNameIs(n, name)) {
61+
return n;
62+
}
63+
children = &n->v.element.children;
64+
} else if (n->type == GUMBO_NODE_DOCUMENT) {
65+
children = &n->v.document.children;
66+
}
67+
if (children) {
68+
// push in reverse so children are visited in document order
69+
for (unsigned int i = children->length; i > 0; i--) {
70+
toVisit.Append((const GumboNode*)children->data[i - 1]);
6671
}
6772
}
6873
}

0 commit comments

Comments
 (0)