Repository navigation
Load the external DTD subset through the resource loader and requesters - #41
Conversation
cb5e80c to
5121c7c
Compare
|
#38 is merged now and this is rebased onto devel, so it only shows its own changes: 2 commits, 14 files (+281/-24). Most of it is docs and tests; happy to split the docs into a separate PR if that helps. |
FlorianRappl
left a comment
There was a problem hiding this comment.
Does not look like a good solution to me.
A good solution uses the AngleSharp ecosystem and it's requesters. So local files can only be read is there is a requested for file: protocols (eg as provided by AngleSharp.Io). Same is true for URLs etc
This way one can be really flexible up to the point of mocking input or determining the resolution on the fly.
|
Makes sense, I'll rework it that way:
One question: the requesters are async. Should external DTDs only load in Also worth noting: an app that configures |
|
Well from the inside we don't know/don't care of we have been called async - so I would not differentiate. Regarding the loading: true I'd only call this if resource loading is enabled. Makes most sense to me. |
|
Thanks. Loading only when resource loading is enabled means going through the browsing context's On sync vs. async: the requesters only offer Would you accept blocking there, or rather that the synchronous parse skips external DTDs (or only uses them when the parse runs through the async path, e.g. browsing-context loading)? I'll implement whichever you prefer. |
|
Someone who uses the sync path (not the recommendation but it's fine) should not use requesters - and if should not put them into the same synchronization context. From my perspective this is on the user. I'd not distinguish here and I'd use the same logic for both ways (there should not be a duplicate). |
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.
5121c7c to
4e8f59c
Compare
|
Reworked as discussed: the external subset now loads through |
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.
|
Thanks. The benchmark's On interrupting the parse: it waits at the DOCTYPE because entities declared in the external subset (
Which do you prefer? And should this still go into 1.3.0 (#43), or after it? |
|
Good point regarding the entity validation. Can we aggregate the entities and defer their validation / resolution to the end of the parse? This way, we can keep on parsing and only deal with the validation as whole at the end. |
|
Thanks! Yes, that's doable:
The replacement uses the recorded positions rather than searching the text afterwards, which would also hit escaped text such as Since this PR is approved as it is, I'd suggest merging it and doing the deferral as a follow-up PR. Or would you rather have it in this one? |
|
I'd see it in this one. Also note for (2): To not overcomplicate things I'd always do the entity resolution at the end. This way, it's consistent and we don't need to check if a download is active or finished. We always do the same. |
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 AngleSharp#41 code differs only for escaped references to declared entities and for empty entities.
|
Done in 07d4409: entity references are now always resolved at the end of the parse.
Tests: 635 pass, 8 new. One test holds the DTD back and checks that the whole body is parsed before it arrives. A 60,000-result differential fuzz against the previous commit differs only for escaped references and empty entities. Still not benchmarked. |
Reworked as discussed: the external DTD subset now loads through the AngleSharp ecosystem instead of the file system.
Problem
Parsing read the file a document's
<!DOCTYPE r SYSTEM "...">named withFile.ReadAllText, from any path, with default options. That includes UNC paths:\\server\share\x.dtdmakes Windows connect toserverand authenticate as the current user while the document is parsed.Change
IResourceLoader, which only exists when resource loading is enabled, and its requesters, which decide the protocols.file:requester, such as AngleSharp.Io's.new XmlParser()andWithXml()alone.ResourceRequestneeds an element for its origin and referrer. The DOCTYPE comes before any element, so a detached element of the document stands in.XmlDomBuilder.LoadExternalSubsetAsync. The async parse awaits it (ConfigureAwait(false)), and the sync parse waits for it. The docs point callers on a synchronization context to the async parse.TryLoadExternalSubsetandTryResolveSystemPathin the builder, andDtdParser'sGetText/TryResolvePath.DtdParsercurrently fails on the first token of every subset (swallowed by the builder), so that path didn't run, but it would have read any file a document named onceDtdParserworked.Tests
Mocks/MockRequesterservesmock:andhttp://*.testresources. Registered as the only requester, soWithDefaultLoaderadds no HTTP requester and no test reaches the network.XmlExternalDtdSupportcovers:file:requester;XmlDeepNesting,XmlDtdIdentityValidation) serve their DTD through the mock instead of a temp file. Their documents and assertions are unchanged.Externalrows load their DTD through a smallfile:requester, andGlobalSetupchecks that the DTD is applied.🤖 Generated with Claude Code