See #50. Unsupported markup could end the process instead of being reported, some malformed HTML blocks could not be opened, and two parts of the renderer did work that grew with the square of the document.
Change
HtmlNesting (Core): a count of open tags kept while tokens arrive (stops at 4,096), and an exact, non-recursive measurement of the parsed tree (limit 256 levels). The count is an upper bound on depth: an end tag closes only the tag on top.
MarkdownRenderer parses once with the sanitizer library's own settings (scripting on, CSS-aware context), checks the bound, then sanitizes that tree with SanitizeDom and serializes it with the library's formatter. Output is unchanged.
Text ranges are clamped to their block; overlapping ranges are left as authored; replacements are applied in one forward pass.
Heading anchors continue from the last suffix used for a repeated title.
DocumentOutline takes a CancellationToken, checked between the render, the parse and each heading. Wiring it to window close and newer loads is a later change in the window code.
Threat model: nesting is bounded; parse time is not.
User-visible: a document nested deeper than 256 levels is reported as one that cannot be opened.
Verification (run)
Local gate script (.NET gates): all pass. Core tests 721 passed, line coverage 100%; fuzz at 5,000 iterations per seed and target passes.
Equivalence of the new sanitize path with the library's Sanitize(string): 40,000 seeded mutated inputs, byte-identical output (throwaway test, not committed).
New tests render on a thread with a quarter of the default stack, so a recursive walk the bound failed to stop would end the test run.
Mutation check: 28 mutants across the bound, the rewrite, the clamp and the anchor loop; 25 killed. Survivors: the start-of-range clamp (no input found that needs it; kept as a guard) and two that only change how many dictionary lookups the anchor loop does.
Measured before the change: 100,000 nested elements took 58 s to parse; now refused after 4,097 tokens.
Not run: the FlaUI desktop suite. This change does not touch the WinUI project.
AI assistance
Written with Claude Fable 5.2. An independent read-only review of this diff is recorded below before merge.
Closes #50.
## Problem
See #50. Unsupported markup could end the process instead of being reported, some malformed HTML blocks could not be opened, and two parts of the renderer did work that grew with the square of the document.
## Change
- `HtmlNesting` (Core): a count of open tags kept while tokens arrive (stops at 4,096), and an exact, non-recursive measurement of the parsed tree (limit 256 levels). The count is an upper bound on depth: an end tag closes only the tag on top.
- `MarkdownRenderer` parses once with the sanitizer library's own settings (scripting on, CSS-aware context), checks the bound, then sanitizes that tree with `SanitizeDom` and serializes it with the library's formatter. Output is unchanged.
- Text ranges are clamped to their block; overlapping ranges are left as authored; replacements are applied in one forward pass.
- Heading anchors continue from the last suffix used for a repeated title.
- `DocumentOutline` takes a `CancellationToken`, checked between the render, the parse and each heading. Wiring it to window close and newer loads is a later change in the window code.
- Threat model: nesting is bounded; parse time is not.
User-visible: a document nested deeper than 256 levels is reported as one that cannot be opened.
## Verification (run)
- Local gate script (.NET gates): all pass. Core tests 721 passed, line coverage 100%; fuzz at 5,000 iterations per seed and target passes.
- Equivalence of the new sanitize path with the library's `Sanitize(string)`: 40,000 seeded mutated inputs, byte-identical output (throwaway test, not committed).
- New tests render on a thread with a quarter of the default stack, so a recursive walk the bound failed to stop would end the test run.
- Mutation check: 28 mutants across the bound, the rewrite, the clamp and the anchor loop; 25 killed. Survivors: the start-of-range clamp (no input found that needs it; kept as a guard) and two that only change how many dictionary lookups the anchor loop does.
- Measured before the change: 100,000 nested elements took 58 s to parse; now refused after 4,097 tokens.
Not run: the FlaUI desktop suite. This change does not touch the WinUI project.
## AI assistance
Written with Claude Fable 5.2. An independent read-only review of this diff is recorded below before merge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Element nesting is bounded before any library walks the parsed tree: a
count kept while tokens arrive stops runaway input early, and an exact
measurement of the parsed tree applies the limit of 256 levels. The
renderer parses once with the sanitizer's own settings and sanitizes that
tree, so the bound is enforced between the parse and the sanitizer's
passes. Deeper markup is an ArgumentException, which the reader already
reports as an unsupported document.
An unterminated script or comment can report a text run past the end of
its block, and malformed markup can report one that starts inside
another; both are handled instead of failing the open. Raw-text
formatting is applied in one forward pass, and repeated heading titles
continue from the last anchor suffix used, so neither grows with the
square of the document. DocumentOutline accepts a cancellation token that
is honoured between its phases.
The threat model states that nesting is bounded and parse time is not.
Closes#50
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #50.
Problem
See #50. Unsupported markup could end the process instead of being reported, some malformed HTML blocks could not be opened, and two parts of the renderer did work that grew with the square of the document.
Change
HtmlNesting(Core): a count of open tags kept while tokens arrive (stops at 4,096), and an exact, non-recursive measurement of the parsed tree (limit 256 levels). The count is an upper bound on depth: an end tag closes only the tag on top.MarkdownRendererparses once with the sanitizer library's own settings (scripting on, CSS-aware context), checks the bound, then sanitizes that tree withSanitizeDomand serializes it with the library's formatter. Output is unchanged.DocumentOutlinetakes aCancellationToken, checked between the render, the parse and each heading. Wiring it to window close and newer loads is a later change in the window code.User-visible: a document nested deeper than 256 levels is reported as one that cannot be opened.
Verification (run)
Sanitize(string): 40,000 seeded mutated inputs, byte-identical output (throwaway test, not committed).Not run: the FlaUI desktop suite. This change does not touch the WinUI project.
AI assistance
Written with Claude Fable 5.2. An independent read-only review of this diff is recorded below before merge.
🤖 Generated with Claude Code
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.