PureXML/Tests/CanonicalDepthSafetyTests.swift
Mihaela Mihaljevic 33160ef775
perf(canonical): make the prefix rewrite and node-subset emit depth-safe (#355)
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.
2026-06-27 17:08:37 +02:00

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)
}
}