Two Canonical XML transforms still recursed one native frame per element level, so a deeply-nested document overflowed the stack. The C14N 2.0 sequential prefix rewrite walks the tree twice. Its assign pass (which records one canonical prefix per namespace in document order) is now an iterative pre-order walk; its rebuild pass is now a child-gathering accumulator driven by an explicit work stack, threading the in-scope declarations and the root flag down per element. The document-order prefix assignment and the root-hoisted declarations are unchanged. The node-subset serializer (`canonicalize(_:including:)`) now drives the same deferred-close work stack as the main serializer, threading each element's in-scope and rendered namespace context; an omitted element still passes its declarations to descendants without rendering or a close tag. The canonical bytes are unchanged: the C14N exclusive, 2.0, and node-subset corpora pass (1794 tests). A 50000-deep document now rewrites and subset-canonicalizes without crashing, guarded by `CanonicalDepthSafetyTests`. Part of #350.
46 lines
2.3 KiB
Swift
46 lines
2.3 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// The Canonical XML transforms that were still recursive must not overflow on a
|
|
/// deeply-nested tree (#350): the C14N 2.0 sequential prefix rewrite (its
|
|
/// document-order assignment pass and its rebuild pass) and the node-subset
|
|
/// serializer. Both now drive explicit work stacks; these reach depths the
|
|
/// recursive forms could not survive. The main inclusive serializer is covered by
|
|
/// `TransformDepthSafetyTests`.
|
|
@Suite("Canonical depth safety")
|
|
struct CanonicalDepthSafetyTests {
|
|
private let namespace = "urn:t"
|
|
|
|
/// A `depth`-deep chain of namespaced `x:a` elements, the outermost declaring
|
|
/// the namespace, with a text leaf at the bottom.
|
|
private func deepNamespacedDocument(_ depth: Int) -> PureXML.Model.Node {
|
|
let name = PureXML.Model.QualifiedName(prefix: "x", localName: "a", namespaceURI: namespace)
|
|
var node: PureXML.Model.Node = .element(.init(name: name, attributes: [], children: [.text("leaf")]))
|
|
for index in 1 ..< depth {
|
|
let attributes: [PureXML.Model.Attribute] = index == depth - 1 ? [.init("xmlns:x", namespace)] : []
|
|
node = .element(.init(name: name, attributes: attributes, children: [node]))
|
|
}
|
|
return .document([node])
|
|
}
|
|
|
|
@Test("the C14N 2.0 sequential prefix rewrite is depth-safe")
|
|
func test_deepPrefixRewrite() {
|
|
let depth = 50000
|
|
let options = PureXML.Canonical.Options(prefixRewrite: .sequential)
|
|
let canonical = PureXML.Canonical.canonicalize(deepNamespacedDocument(depth), options: options)
|
|
// Every element is renamed to the single canonical prefix n0, declared once.
|
|
#expect(canonical.contains("leaf"))
|
|
#expect(canonical.contains("xmlns:n0=\"\(namespace)\""))
|
|
#expect(canonical.components(separatedBy: "</n0:a>").count - 1 == depth)
|
|
}
|
|
|
|
@Test("the node-subset canonicalizer is depth-safe")
|
|
func test_deepNodeSubset() {
|
|
let depth = 50000
|
|
let document = PureXML.Model.TreeNode(deepNamespacedDocument(depth))
|
|
let canonical = PureXML.Canonical.Canonicalizer(options: .inclusive).canonicalize(document) { _ in true }
|
|
#expect(canonical.contains("leaf"))
|
|
#expect(canonical.components(separatedBy: "</x:a>").count - 1 == depth)
|
|
}
|
|
}
|