diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d607a4..5bc276e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Released on Wednesday, November 11 2026. - Improved the performance of nested tables and inline-blocks, which were laid out twice as often with every level (#54) @michaelthyregod - Fixed adjacent text nodes (e.g. inserted by a script or a framework like Blazor) each starting a line of their own in a block, table cell or flex item: they are laid out as one text, like the single node the parser makes of the same text - Fixed the intrinsic width of a row flex container counting each of adjacent text nodes as an item of its own (with a gap between them) +- Improved deeply nested pages: what is nested too deep for the stack is left out instead of ending the process with a stack overflow (#55) @michaelthyregod # 0.11.1 diff --git a/src/AngleSharp.Renderer.Tests/DeepNestingTests.cs b/src/AngleSharp.Renderer.Tests/DeepNestingTests.cs new file mode 100644 index 0000000..93cc321 --- /dev/null +++ b/src/AngleSharp.Renderer.Tests/DeepNestingTests.cs @@ -0,0 +1,70 @@ +namespace AngleSharp.Renderer.Tests; + +using System.Text; +using System.Threading; +using AngleSharp; +using AngleSharp.Css; + +/// +/// Deeply nested documents: layout recurses per nesting level, so a deep enough tree must leave +/// its innermost content out rather than overflow the stack (which ends the whole process). +/// +public sealed class DeepNestingTests +{ + private static readonly DefaultRenderDevice Device = new() { ViewPortWidth = 300, ViewPortHeight = 200, FontSize = 16 }; + + // The HTML parser keeps at most 511 levels below the body, so this is the deepest markup can get. + private const int ParserDepthLimit = 511; + + // A thread-pool thread or a desktop app's thread commonly has 1 MB of stack. + private const int StackSize = 1024 * 1024; + + [Theory] + [InlineData("
", "
")] + [InlineData("x", "")] + [InlineData("
", "
")] + [InlineData("
", "
")] + [InlineData("
", "
")] + [InlineData("
", "
")] + [InlineData("
", "
")] + public async Task DeepestParsableNesting_RendersWithoutOverflowingTheStack(string open, string close) + { + var markup = new StringBuilder(""); + + for (var i = 0; i < ParserDepthLimit; i++) + { + markup.Append(open); + } + + markup.Append("leaf"); + + for (var i = 0; i < ParserDepthLimit; i++) + { + markup.Append(close); + } + + markup.Append(""); + var context = BrowsingContext.New(Configuration.Default.WithCss().WithRenderDevice(Device)); + var document = await context.OpenAsync(request => request.Content(markup.ToString())); + byte[]? png = null; + Exception? error = null; + var thread = new Thread(() => + { + try + { + png = new HtmlRenderer().RenderToPng(document, Device).Data; + } + catch (Exception ex) + { + error = ex; + } + }, StackSize); + + thread.Start(); + thread.Join(); + + Assert.Null(error); + Assert.NotNull(png); + Assert.NotEmpty(png); + } +} diff --git a/src/AngleSharp.Renderer/HtmlRenderer.Inline.cs b/src/AngleSharp.Renderer/HtmlRenderer.Inline.cs index 99afdf3..ab62ea4 100644 --- a/src/AngleSharp.Renderer/HtmlRenderer.Inline.cs +++ b/src/AngleSharp.Renderer/HtmlRenderer.Inline.cs @@ -545,7 +545,8 @@ public void Collect(IRenderNode node, RenderTextStyle style, int styleId, Inline private void CollectElement(ElementRenderNode element, RenderTextStyle parentStyle, InlineBox? parentBox) { - if (!GeneratesBoxes(element)) + // a deep subtree is left out once the stack runs low (see LayoutNode) + if (!GeneratesBoxes(element) || !RuntimeHelpers.TryEnsureSufficientExecutionStack()) { return; } @@ -3096,6 +3097,12 @@ private static IntrinsicWidths ComputeElementContribution(ElementRenderNode elem /// private static IntrinsicWidths ComputeContentIntrinsicWidths(ElementRenderNode element, RenderTextStyle textStyle, float percentageBase, LayoutContext context) { + // a deep subtree is left out once the stack runs low (see LayoutNode) + if (!RuntimeHelpers.TryEnsureSufficientExecutionStack()) + { + return new IntrinsicWidths(0f, 0f); + } + var widths = ComputeElementContentWidths(element, textStyle, percentageBase, context); if (s_layoutPass.Value is { } pass) diff --git a/src/AngleSharp.Renderer/HtmlRenderer.cs b/src/AngleSharp.Renderer/HtmlRenderer.cs index 6820567..b68aa35 100644 --- a/src/AngleSharp.Renderer/HtmlRenderer.cs +++ b/src/AngleSharp.Renderer/HtmlRenderer.cs @@ -1241,6 +1241,13 @@ private static void LayoutNode( LayoutTextNode(textNode.Ref, containingX, containingWidth, ref cursorY, ref previousBlockMarginBottom, ref suppressNextBlockTopMargin, floats, ref textIndentConsumed, textStyle, context, displayList, maxY); return; case ElementRenderNode element: + // Every nesting level takes a large stack frame; when the stack runs low the rest of + // the subtree is left out instead of ending the process with a stack overflow. + if (!RuntimeHelpers.TryEnsureSufficientExecutionStack()) + { + return; + } + LayoutStickyAwareElement(element, containingX, containingY, containingWidth, ref cursorY, ref previousBlockMarginBottom, ref suppressNextBlockTopMargin, floats, ref textIndentConsumed, textStyle, context, displayList, maxY, isFlexItem, isRowDirection, flexMainSize, flexCrossSize, containingHeight, positionedContainingX, positionedContainingY, positionedContainingWidth, positionedContainingHeight, forcedBorderBoxWidth, isAtomicInline, forcedBorderBoxHeight, contentOffsetY, mainAxisMarginsPlaced, crossAxisMarginsPlaced); return; default: @@ -8308,6 +8315,13 @@ private static List GetAuthoredRules(IDocument document, string pr private static bool TryGetFirstCollapsibleChildTopMargin(ElementRenderNode node, float containingWidth, LayoutContext context, out float marginTop) { + // a deep subtree is left out once the stack runs low (see LayoutNode) + if (!RuntimeHelpers.TryEnsureSufficientExecutionStack()) + { + marginTop = 0f; + return false; + } + var throughPositive = 0f; var throughNegative = 0f; var hasThrough = false; @@ -8390,6 +8404,12 @@ private static bool TryGetFirstCollapsibleChildTopMargin(ElementRenderNode node, /// private static bool IsSelfCollapsing(ElementRenderNode element, LayoutContext context) { + // a deep subtree is left out once the stack runs low (see LayoutNode) + if (!RuntimeHelpers.TryEnsureSufficientExecutionStack()) + { + return false; + } + var styleMap = GetOrCreateStyleMap(context, element.Ref, element.ComputedStyle); if (!IsFlowBlockDisplay(styleMap) || IsReplacedElement(element.Ref) || ResolveFormControlKind(element) != FormControlKind.None)