* perf(model): rebuild DTD-default application iteratively, not recursively applyingDefaults rebuilt the tree with a per-node recursive walk, so applying DTD attribute defaults to a deeply-nested document overflowed the stack (around 2000 deep on a Swift Task stack). It now rebuilds bottom-up through a new shared iterative spine, Node.rebuildingBottomUp, which transforms each node once its children are rebuilt via an explicit work stack. The same primitive will carry the other tree-to-tree transforms (canonical prefix rewrite, XSLT namespace fixup) in follow-ups. Defaults are applied identically: 1788 tests pass. Part of #350. * perf(canonical): serialize C14N iteratively, not recursively Canonicalizer.emit recursed on element children, so canonicalizing a deeply-nested document overflowed the stack (crashed around 5000 deep). The element serializer now drives a deferred-close work stack (the same shape as the main serializer): each step carries the in-scope and already-rendered namespace context, children push reversed to emit in document order, and a close step emits the end tag. The shallow document case and all namespace-rendering logic are unchanged. The canonical bytes are byte-identical: the C14N 1.0, 1.1, and exclusive conformance corpora pass, full suite 1789. A 50000-deep document now canonicalizes without crashing. The node-subset helpers moved to their own file to keep within the file-length limit. Part of #350.
39 lines
1.9 KiB
Swift
39 lines
1.9 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// Tree-to-tree transforms must not recurse on a document's depth (#350). They
|
|
/// rebuilt the tree with a per-node recursive walk, so transforming a deeply
|
|
/// nested document overflowed the stack (as shallow as a couple of thousand deep
|
|
/// on a Swift Task stack, which these tests run on). The walks are now iterative.
|
|
@Suite("Transform depth safety")
|
|
struct TransformDepthSafetyTests {
|
|
private func deepXML(_ depth: Int, doctype: String = "") -> String {
|
|
doctype + String(repeating: "<a>", count: depth) + "x" + String(repeating: "</a>", count: depth)
|
|
}
|
|
|
|
@Test("applying DTD attribute defaults to a deep document does not overflow")
|
|
func test_deepDTDDefaults() throws {
|
|
let depth = 50000
|
|
let xml = deepXML(depth, doctype: "<!DOCTYPE a [<!ATTLIST a kept CDATA \"on\">]>")
|
|
let node = try PureXML.parseApplyingInternalDTDDefaults(xml, limits: .init(maxDepth: depth + 1, allowDoctype: true))
|
|
// Every `a` element gained the defaulted `kept="on"` attribute; check the
|
|
// outermost one without recursing.
|
|
guard case let .document(roots) = node, let root = roots.first(where: { $0.element != nil })?.element else {
|
|
Issue.record("expected a document with a root element")
|
|
return
|
|
}
|
|
#expect(root.attributes.contains { $0.name.localName == "kept" && $0.value == "on" })
|
|
}
|
|
|
|
@Test("canonicalizing a deep document does not overflow")
|
|
func test_deepCanonicalize() throws {
|
|
let depth = 50000
|
|
let document = try PureXML.parse(deepXML(depth), limits: .init(maxDepth: depth + 1))
|
|
let canonical = PureXML.Canonical.Canonicalizer(options: .inclusive).canonicalize(document)
|
|
// Each level contributes "<a>"; the leaf text and the closes round it out.
|
|
#expect(canonical.hasPrefix("<a>"))
|
|
#expect(canonical.hasSuffix("</a>"))
|
|
#expect(canonical.contains("x"))
|
|
}
|
|
}
|