Repository navigation
Release 1.3.0 - #43
Merged
Merged
Conversation
- Require AngleSharp 1.9.0 (csproj default and CI) - Mark the net8.0 and net10.0 targets IsAotCompatible, so the trim and AOT analyzers run on every build - Walk trees with an explicit stack in XmlElement.TextContent and the DTD, internal-subset and identity validation walks; deeply nested documents no longer overflow the call stack and end the process - Speed up TextContent (no allocation for a single text or CDATA child) and DTD validation (declarations looked up by name, content models parsed once per document, identity walk skipped without ID declarations) - Cap the characters produced by expanding declared entities with the new XmlParserOptions.MaxCharactersFromEntities (default 10,000,000, 0 for no limit); beyond it parsing throws EntityExpansionLimitExceeded, or keeps the references as text when errors are suppressed - Add tests for deep nesting, content models, identity, TextContent and entity expansion - Add a BenchmarkDotNet project with Compare-Builds.ps1 to compare builds - Update AGENTS.md, CHANGELOG.md and the API and DTD docs
ValidateElementsAgainstDtd only runs when DtdParser produced element declarations, and DtdParser currently fails on the first token of every subset, so the lookup by name never runs. The measured gains come from parsing content models once and skipping the identity walk.
Adopt AngleSharp 1.9, deep-nesting hardening and an entity expansion cap
… text The fallback expansion replaced <!\[CDATA\[(.*?)\]\]> in the whole text of a character token whenever the document declared any entity. That stripped the markers from escaped text such as <![CDATA[b]]> (giving 'b'), and took quadratic time on many unterminated starts: 240 KB of <![CDATA[ took 33 s. The CDATA sections of each entity's replacement text are now unwrapped once, in a single pass, when the entity is registered, and that text is used where an entity contributes to element content (tokenizer and nested expansion). Attribute values keep the replacement text as before. A 60,000-result differential fuzz against the previous code differs only for escaped CDATA-looking text (the fix), CDATA sections split across entities, recursive entities, and CDATA entities whose content references another CDATA entity, where the previous code left CDATA markers behind.
Unwrap CDATA sections in entity replacement text, not in the expanded text
XmlParserOptions.MaximumDepth mirrors core's HtmlParserOptions.MaximumDepth: the deepest level the parser nests elements at, counting the root as level 1, defaulting to 512; zero or a negative value restores the default and Int32.MaxValue lifts the limit. An element beyond it throws MaximumDepthExceeded (0x314), since nesting it elsewhere changes an XML document; with IsSuppressingErrors it is attached at the limit as a sibling of the deepest element, as the HTML parser does. The deep-nesting tests and the deep benchmark shapes go beyond 512 levels to exercise the explicit-stack walks, so they lift the limit.
Limit how deep the parser nests elements, as the HTML parser does
The external subset a DOCTYPE names was read with File.ReadAllText from any path the document chose, with default options, including UNC paths (\\server\share), which connect to the server while parsing. It is now fetched like the document's other resources: through the browsing context's IResourceLoader, which only exists when resource loading is enabled, and its requesters, which decide the protocols (file: through e.g. AngleSharp.Io's requester). Relative SYSTEM identifiers resolve against the document URL. Without a loader nothing is loaded. LoadExternalSubsetAsync is the one loading path: the asynchronous parse awaits it, the synchronous parse waits for it. DtdParser no longer reads external entities from disk; it fails on the first token of every subset today, so this path did not run, but it would have read any file a document named once it did. Tests use a mock requester registered as the only requester, so no request reaches the network. The benchmark's External rows load their DTD through a file: requester and check that it is applied.
The External rows name the plain path again, which older builds read directly; as a URL its drive letter is the protocol, so the benchmark's requester serves single-letter protocols from the disk, without Uri.
Write-Summary stripped everything but digits and dots from the Mean and Allocated cells and read them as ns and B. BenchmarkDotNet writes the unit into each cell and picks it per row (952.654 us, 1,280.41 KB), so such rows were off by 1000x or 1024x, and a ratio was wrong whenever two builds of a case landed in different units. Each cell is now parsed as a value and a unit (ns, us with the Greek mu, the micro sign or u, ms, s; B, KB, MB, GB) and converted; NA (a failed benchmark) drops the row, - counts as no allocation, and an unknown unit throws. Reports are only read from this run's round-* folders, as a report left in a baseline source tree would otherwise count as a build. On the reports of an earlier three-way run the summary is unchanged; with one build's cells rewritten into us/ms and KB/MB it matches up to the rounding of the rewritten values, where the previous parsing got 20 of 24 rows wrong.
Read the units BenchmarkDotNet writes into the CSV cells
Loading the external subset no longer interrupts the parse. The download starts at the DOCTYPE and the parse goes on; the tokenizer keeps every reference to a non-built-in entity as written and records its offset (XmlEntityReference), and the builder records where it lands (text node or attribute value). When the tree is built, CompleteAsync waits for the download (the synchronous parse blocks there), builds the DTD from the external and internal subsets, replaces the recorded references by their offsets, and validates. Both parse paths complete the same way. The replacement uses the recorded offsets, never a search of the text. That fixes escaped references: &e; read as &e; and was expanded as well when e was declared. An empty entity now resolves to nothing; strict parsing threw on it before. An unknown entity still throws the same error at the same position, only at the end of the parse. OnCreated sees attribute values with their references as written. A 60,000-result differential fuzz against the previous #41 code differs only for escaped references to declared entities and for empty entities.
Load the external DTD subset through the resource loader and requesters
Contributor
|
Before you merge this: I'm running the benchmarks for the 1.3.0 changes tonight (Danish time). Also, AngleSharp 1.9.1 is out with AngleSharp/AngleSharp#1401 ( |
Contributor
Author
|
Sure - let's bring those into 1.3.0 |
Contributor
|
Benchmarks for 1.3.0 are done: devel (68d437c) against 17202f0 (devel after #38), both on AngleSharp 1.9.1, 3 alternating rounds pinned to the performance cores.
From my side 1.3.0 is good to go. |
Contributor
Author
|
Awesome - let's ship it! |
This was referenced Nov 3, 2026
This was referenced Nov 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Types of Changes
Prerequisites
Please make sure you can check the following two boxes:
Contribution Type
What types of changes does your code introduce? Put an
xin all the boxes that apply:Description
This release introduces version 1.3.0 of AngleSharp.Xml, focusing on improved performance, security, and compatibility. The most significant updates include raising the minimum AngleSharp dependency to 1.9, adding a hard entity expansion limit to prevent resource exhaustion, implementing explicit-stack algorithms to avoid stack overflows, and introducing comprehensive benchmarks to track and enforce performance improvements. Additionally, the CI pipeline is updated for trusted NuGet publishing and AOT compatibility is enforced.