@@ -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+
462469static 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;
0 commit comments