PureXML/Sources/Canonical/CanonicalNodeSubset.swift
Mihaela Mihaljevic 00c55e642f
perf: make tree-to-tree transforms depth-safe (DTD defaults, canonicalization) (#352)
* 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.
2026-06-26 19:27:37 +02:00

71 lines
3.4 KiB
Swift

extension PureXML.Canonical.Canonicalizer {
/// The apex element with its inherited `xml:*` attributes added, and (for
/// inclusive mode) the namespaces in scope from omitted ancestors merged in
/// as declarations so the apex renders its full namespace context.
static func augmentedApex(
_ apex: PureXML.Model.Element,
inheritedXML: [PureXML.Model.Attribute],
mergingNamespaces ancestorNamespaces: [String: String]?,
stripApexBase: Bool,
) -> PureXML.Model.Element {
// Under 1.1, the apex's own xml:base is folded into the merged base in
// inheritedXML, so drop the original to avoid emitting it twice.
let own = stripApexBase
? apex.attributes.filter { !($0.name.prefix == "xml" && $0.name.localName == "base") }
: apex.attributes
var attributes = own + inheritedXML
if let ancestorNamespaces {
let declared = Set(apex.attributes.map(\.name.description))
for (prefix, uri) in ancestorNamespaces {
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
if !declared.contains(name) { attributes.append(PureXML.Model.Attribute(name, uri)) }
}
}
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
}
/// The namespace declarations in scope from a node's omitted ancestors,
/// nearest ancestor winning (so an inner redeclaration is preserved).
static func inScopeNamespaces(above node: PureXML.Model.TreeNode) -> [String: String] {
var result: [String: String] = [:]
var current = node.parent
while let ancestor = current {
for attribute in ancestor.attributes {
guard let prefix = declaredPrefix(of: attribute.name), result[prefix] == nil else { continue }
result[prefix] = attribute.value
}
current = ancestor.parent
}
return result
}
/// The `xml:*` attributes in scope from a node's omitted ancestors that the
/// apex does not already set, nearest ancestor winning.
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
let present = Set(apex.attributes.map(\.name.description))
if mergeBase { return inherited11XMLAttributes(above: node, apex: apex, present: present) }
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
// apex does not already set.
var seen: Set<String> = []
var result: [PureXML.Model.Attribute] = []
var current = node.parent
while let ancestor = current {
for attribute in ancestor.attributes where attribute.name.prefix == "xml" {
let key = attribute.name.description
guard !present.contains(key), seen.insert(key).inserted else { continue }
result.append(attribute)
}
current = ancestor.parent
}
return result
}
/// The prefix a namespace declaration binds (empty for the default
/// namespace), or nil when the attribute is not a namespace declaration.
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
if name.prefix == nil, name.localName == "xmlns" { return "" }
if name.prefix == "xmlns" { return name.localName }
return nil
}
}