perf(xslt): make the template evaluator iterative (#358)
The XSLT engine was mutually recursive on template-application and result-tree nesting (instantiate, evaluate, applyTemplates), so a recursive named template whose depth comes from source data could overflow the native stack (SIGBUS) on attacker-controllable input, and an identity transform of a deep source overflowed on a small (Swift-concurrency Task) stack. The engine is now an explicit work-stack driver (XSLTDriver). Each instruction appends its produced items to a sink or pushes sub-work, with the rest of the body pushed first so result-tree document order is preserved; template application, the built-in rule's source descent, and result-element nesting all run on the heap. Only stylesheet-bounded helper evaluation (a variable's or parameter's result-tree fragment, an attribute set, an xsl:message body) re-enters through a nested driver call, whose native depth is bounded by the stylesheet's own nesting. The result-tree serialization walks the engine now exposes at depth were made iterative too: XSLTNamespaceFixup (an explicit pre-order visit that keeps the ns0/ns1 counter in document order), the text and string-value extractions (iterative accumulators), and the content-type-meta and CDATA-section rewrites (the shared Node.rebuildingBottomUp spine). Deep recursion now succeeds on any stack: a 5000-level recursive template builds and serializes on the test Task stack. maxTemplateDepth is retained as runaway protection (an unbounded result tree would exhaust memory), defaulting to 40000 and throwing XSLTError.recursionLimitExceeded only past that. Behavior is otherwise unchanged: the full Apache Xalan XSLT 1.0 conformance corpus (1086 cases) and the in-repo XSLT/XPath suites pass identically; one previously-failing Xalan case (mdocs07) now passes and was de-baselined. Full suite green. Closes #356.
This commit is contained in:
parent
7b7adef016
commit
dc6727538d
@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- An XSLT transform with unbounded template recursion no longer overflows the stack; it fails gracefully with `XSLTError.recursionLimitExceeded` instead (#356). A recursive named template whose depth is driven by source data (for example `<xsl:with-param name="n" select="count(//x)"/>`) could recurse without bound and crash the process (SIGBUS) on an attacker-controllable input, the way every XSLT processor guards against (libxslt's `xsltMaxDepth`). Template-instantiation nesting is now counted and capped at `maxTemplateDepth` (default 300, exposed as `PureXML.XSLT.defaultMaxTemplateDepth`), a new optional parameter on `PureXML.XSLT.transform`. The default clears the parser's maximum source nesting depth (256) so an identity transform of the deepest permitted source still runs, while staying within an 8 MB main-thread stack with margin; a stylesheet with deeper legitimate recursion can raise the limit (on a correspondingly larger stack). Measurement note: source *nesting* depth was never the exposure here, since the engine only ever sees a source parsed at the default `maxDepth` and an identity transform of a 254-deep source completes normally; the reachable crash was data-driven template recursion. The full XSLT/XPath conformance suite is unchanged (no corpus transform recurses past the default). Making the engine fully iterative (so deep recursion succeeds rather than failing gracefully, and is safe on a small Swift-concurrency Task stack) remains tracked in #356.
|
||||
- The XSLT evaluator is now iterative, so template recursion and result-tree nesting no longer consume the native stack (#356). A recursive named template whose depth is driven by source data (for example `<xsl:with-param name="n" select="count(//x)"/>`) could previously recurse without bound and crash the process (SIGBUS) on attacker-controllable input; and even an identity transform of a deeply-nested source overflowed the stack on a small (Swift-concurrency Task) stack. The mutually-recursive `instantiate`/`evaluate`/`applyTemplates` engine is replaced by an explicit work-stack driver (`XSLTDriver`): each instruction appends its produced items to a sink or pushes sub-work, with the rest of the body pushed first so document order is preserved; template application, the built-in rule's source descent, and result-element nesting all run on the heap. Only stylesheet-bounded helper evaluation (a variable's or parameter's result-tree fragment, an attribute set, an `xsl:message` body) re-enters through a nested driver call, whose native depth is bounded by the stylesheet's own nesting. The result-tree serialization walks the engine now exposes at depth (the namespace fixup, the `text` and string-value extractions, and the content-type-meta and CDATA-section rewrites) were made iterative as well. Deep recursion now *succeeds* on any stack (a 5000-level recursive template builds and serializes on the test Task stack); `maxTemplateDepth` is retained as runaway protection (an unbounded result tree would otherwise exhaust memory), now defaulting to 40000 and tripping `XSLTError.recursionLimitExceeded` only past that. Behavior is otherwise unchanged: the full Apache Xalan XSLT 1.0 conformance corpus and the in-repo XSLT/XPath suites pass identically (one previously-failing Xalan case, `mdocs07`, now passes and was de-baselined). `maxTemplateDepth` and `PureXML.XSLT.defaultMaxTemplateDepth` (added for the earlier stack-based guard) keep their public shape.
|
||||
|
||||
- Serializing a tree as HTML (`PureXML.HTML.serialize`, the html output method's serializer) no longer recurses on the tree's depth, so it no longer overflows the stack on a deeply-nested tree (as shallow as a couple of thousand deep on a Swift Task stack). The serializer now drives a deferred-close work stack (the same shape as the XML serializer and `Canonicalizer`): an element pushes a close step then its children reversed, and a raw-text element (`script`, `style`) flags its direct text children to emit verbatim. The output is byte-identical: the void-element, raw-text, comment, and escaping semantics are unchanged, with the HTML conformance corpus passing. A 50000-deep tree now serializes without crashing. (#350)
|
||||
|
||||
|
||||
@ -70,82 +70,6 @@ extension PureXML.XSLT.Transformer {
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func elementInstruction(_ instruction: PureXML.XSLT.Instruction, _ context: XSLTContext) -> [PureXML.XSLT.ResultItem] {
|
||||
guard case let .element(nameTemplate, namespaceTemplate, namespaces, useAttributeSets, body) = instruction else {
|
||||
return []
|
||||
}
|
||||
let raw = avt(nameTemplate, context)
|
||||
let parts = raw.split(separator: ":", omittingEmptySubsequences: false)
|
||||
let hasExplicitNamespace = (namespaceTemplate.map { !avt($0, context).isEmpty }) ?? false
|
||||
let prefix = parts.count == 2 ? String(parts[0]) : nil
|
||||
// A prefix not bound to a namespace (and no explicit namespace attribute
|
||||
// supplies one) makes the QName unusable, as does a non-NCName part.
|
||||
let undeclaredPrefix = !hasExplicitNamespace && (prefix.map { $0 != "xml" && namespaces[$0] == nil } ?? false)
|
||||
let unusableName = raw.isEmpty || parts.contains(where: \.isEmpty) || parts.count > 2
|
||||
|| !parts.allSatisfy { PureXML.Parsing.XMLCharacter.isValidName(String($0)) } || undeclaredPrefix
|
||||
if unusableName {
|
||||
// The recovery (7.1.2, element-name-not-QName) emits the content
|
||||
// without the wrapper element.
|
||||
return instantiate(body, context).filter { if case .attribute = $0 { false } else { true } }
|
||||
}
|
||||
let name = createdName(nameTemplate, namespaceTemplate, namespaces, context, isAttribute: false)
|
||||
return [buildElement(name: name, literalAttributes: [], useAttributeSets: useAttributeSets, body: body, context)]
|
||||
}
|
||||
|
||||
func attributeInstruction(
|
||||
_ nameTemplate: PureXML.XSLT.ValueTemplate,
|
||||
_ namespaceTemplate: PureXML.XSLT.ValueTemplate?,
|
||||
_ namespaces: [String: String],
|
||||
_ body: [PureXML.XSLT.Instruction],
|
||||
_ context: XSLTContext,
|
||||
) -> [PureXML.XSLT.ResultItem] {
|
||||
let name = createdName(nameTemplate, namespaceTemplate, namespaces, context, isAttribute: true)
|
||||
// xsl:attribute content that creates a non-text node ignores it with its
|
||||
// content (XSLT 1.0 errata E27), like xsl:comment/processing-instruction.
|
||||
return [.attribute(.init(name: name, value: escapedTextValue(of: instantiate(body, context))))]
|
||||
}
|
||||
|
||||
func copyInstruction(_ useAttributeSets: [String], _ body: [PureXML.XSLT.Instruction], _ context: XSLTContext) -> [PureXML.XSLT.ResultItem] {
|
||||
// A non-tree current node copies itself: an attribute node yields an
|
||||
// attribute result, a namespace node its declaration.
|
||||
if let current = context.current {
|
||||
switch current {
|
||||
case let .attribute(_, attribute):
|
||||
return [.attribute(attribute)]
|
||||
case let .namespace(_, prefix, uri):
|
||||
return [.attribute(.init(prefix.isEmpty ? "xmlns" : "xmlns:" + prefix, uri))]
|
||||
case .tree:
|
||||
break
|
||||
}
|
||||
}
|
||||
switch context.node.kind {
|
||||
case .element:
|
||||
let copied = buildElement(
|
||||
name: context.node.name ?? .init(""),
|
||||
literalAttributes: Self.namespaceDeclarations(inScopeAt: context.node),
|
||||
useAttributeSets: useAttributeSets,
|
||||
body: body,
|
||||
context,
|
||||
)
|
||||
return [copied]
|
||||
case .text, .cdata:
|
||||
return [.node(.text(context.node.value))]
|
||||
case .comment:
|
||||
return [.node(.comment(context.node.value))]
|
||||
case .processingInstruction:
|
||||
return [.node(.processingInstruction(target: context.node.name?.description ?? "", data: context.node.value))]
|
||||
case .document:
|
||||
// Copying the root node produces no element of its own, but xsl:copy
|
||||
// may carry use-attribute-sets (7.5); those attributes have no copied
|
||||
// element to attach to, so they join the enclosing result element,
|
||||
// ahead of the copied content.
|
||||
let setAttributes = attributeSetAttributes(useAttributeSets, context, visiting: []).map(PureXML.XSLT.ResultItem.attribute)
|
||||
return setAttributes + instantiate(body, context)
|
||||
default:
|
||||
return instantiate(body, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.XSLT.Transformer {
|
||||
@ -256,30 +180,3 @@ extension PureXML.XSLT.Transformer {
|
||||
return XSLTNumbering.format(numbers, format, grouping)
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.XSLT.Transformer {
|
||||
/// A literal result element: aliased name and attributes, the copied
|
||||
/// 7.1.1 namespace declarations, then the shared element builder.
|
||||
func literalResult(_ instruction: PureXML.XSLT.Instruction, _ context: PureXML.XSLT.XSLTContext) -> PureXML.XSLT.ResultItem {
|
||||
guard case let .literalElement(name, attributes, namespaces, useAttributeSets, body) = instruction else {
|
||||
return .node(.text(""))
|
||||
}
|
||||
let aliasedAttributes = attributes.map { PureXML.XSLT.LiteralAttribute(name: aliased($0.name), value: $0.value) }
|
||||
// The copied namespace nodes (7.1.1) travel as xmlns attributes; the
|
||||
// fixup pass reuses them and drops the ones already in scope. An
|
||||
// aliased stylesheet namespace declares its result namespace instead.
|
||||
var declarations: [PureXML.XSLT.LiteralAttribute] = []
|
||||
for (prefix, uri) in namespaces.sorted(by: { $0.key < $1.key }) {
|
||||
let alias = stylesheet.namespaceAliases[uri]
|
||||
// Keep the literal prefix; an alias only remaps its namespace URI.
|
||||
let resolvedPrefix = prefix.isEmpty ? nil : prefix
|
||||
let resolvedURI = alias?.uri ?? uri
|
||||
let attributeName = resolvedPrefix.map { "xmlns:" + $0 } ?? "xmlns"
|
||||
declarations.append(PureXML.XSLT.LiteralAttribute(
|
||||
name: PureXML.Model.QualifiedName(attributeName),
|
||||
value: [.literal(resolvedURI)],
|
||||
))
|
||||
}
|
||||
return buildElement(name: aliased(name), literalAttributes: declarations + aliasedAttributes, useAttributeSets: useAttributeSets, body: body, context)
|
||||
}
|
||||
}
|
||||
|
||||
130
Sources/XSLT/XSLTInstantiation.swift
Normal file
130
Sources/XSLT/XSLTInstantiation.swift
Normal file
@ -0,0 +1,130 @@
|
||||
// The XSLT sequence-constructor evaluator, driven by an explicit work stack so
|
||||
// template recursion and result-tree nesting do not consume the native stack.
|
||||
// A recursive named template, an identity transform of a deeply-nested source,
|
||||
// or any deep result tree is evaluated iteratively here; only stylesheet-bounded
|
||||
// helper evaluation (a variable's or parameter's result-tree fragment, an
|
||||
// attribute set, an xsl:message body) re-enters through a nested driver call,
|
||||
// whose depth is bounded by the stylesheet's own nesting.
|
||||
//
|
||||
// A work item's `depth` is the logical template-application nesting (one per
|
||||
// apply-templates / call-template / built-in-rule descent), not a native frame
|
||||
// count. `maxTemplateDepth` caps it as runaway protection (an unbounded result
|
||||
// tree would otherwise exhaust memory), failing gracefully via `recursionGuard`.
|
||||
|
||||
extension PureXML.XSLT.Transformer {
|
||||
/// Instantiates a sequence constructor, iteratively.
|
||||
func instantiate(_ body: [PureXML.XSLT.Instruction], _ context: XSLTContext) -> [ResultItem] {
|
||||
XSLTDriver(self).run(body, context)
|
||||
}
|
||||
|
||||
/// Applies templates to `nodes` (already selected and sorted), iteratively.
|
||||
func applyTemplates(
|
||||
to nodes: [PureXML.XPath.Node],
|
||||
mode: String?,
|
||||
parameters: [PureXML.XSLT.Binding],
|
||||
_ context: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
XSLTDriver(self).applyTemplates(nodes, mode: mode, parameters: parameters, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// The iterative evaluator. One per top-level `instantiate`/`applyTemplates`
|
||||
/// call; nested helper evaluation makes its own. Holds the work stack so the step
|
||||
/// methods need not thread it as a parameter.
|
||||
final class XSLTDriver {
|
||||
typealias Host = PureXML.XSLT.Transformer
|
||||
typealias Instruction = PureXML.XSLT.Instruction
|
||||
typealias Binding = PureXML.XSLT.Binding
|
||||
|
||||
/// A mutable, ordered bag of produced items a branch's work appends into; a
|
||||
/// deferred `finish` step assembles the parent node from it.
|
||||
final class Accumulator {
|
||||
var items: [ResultItem] = []
|
||||
}
|
||||
|
||||
/// Where produced items go and at what logical recursion depth.
|
||||
struct Sink {
|
||||
let depth: Int
|
||||
let into: Accumulator
|
||||
}
|
||||
|
||||
/// The shared context of one template application across its node-set: the
|
||||
/// mode, the parameters passed, the caller's context, and the sink.
|
||||
struct Application {
|
||||
let mode: String?
|
||||
let parameters: [Binding]
|
||||
let caller: XSLTContext
|
||||
let sink: Sink
|
||||
}
|
||||
|
||||
/// How a completed child accumulator is turned into a node appended to a parent.
|
||||
enum Finish {
|
||||
/// Partition the child items into attributes (before the first node) and
|
||||
/// children, then wrap them in an element with the precomputed name and
|
||||
/// base attributes (attribute sets plus literal attributes).
|
||||
case element(PureXML.Model.QualifiedName, [PureXML.Model.Attribute])
|
||||
/// The `xsl:element` recovery for an unusable name: emit the content with
|
||||
/// no wrapper, dropping attribute items (XSLT 1.0 7.1.2).
|
||||
case filteredElement
|
||||
case comment
|
||||
case processingInstruction(String)
|
||||
case attribute(PureXML.Model.QualifiedName)
|
||||
}
|
||||
|
||||
/// One unit of evaluation work.
|
||||
enum Work {
|
||||
/// Run `body` from `cursor` in `context`, appending to the sink.
|
||||
case run([Instruction], Int, XSLTContext, Sink)
|
||||
/// Apply templates to already-selected, already-sorted `nodes`.
|
||||
case applyTemplates([PureXML.XPath.Node], Application)
|
||||
/// Apply templates to one node (its position and size fixed). Each node is
|
||||
/// its own deferred work item so a leaf node's text and a nested node's
|
||||
/// template output land in the sink in document order.
|
||||
case applyOne(PureXML.XPath.Node, Int, Int, Application)
|
||||
/// Assemble a node from a finished child accumulator into a parent.
|
||||
case finish(Finish, Accumulator, Accumulator)
|
||||
}
|
||||
|
||||
let transformer: Host
|
||||
var stack: [Work] = []
|
||||
|
||||
init(_ transformer: Host) {
|
||||
self.transformer = transformer
|
||||
}
|
||||
|
||||
func run(_ body: [Instruction], _ context: XSLTContext) -> [ResultItem] {
|
||||
let root = Accumulator()
|
||||
stack = [.run(body, 0, context, Sink(depth: 0, into: root))]
|
||||
drive()
|
||||
return root.items
|
||||
}
|
||||
|
||||
func applyTemplates(
|
||||
_ nodes: [PureXML.XPath.Node],
|
||||
mode: String?,
|
||||
parameters: [Binding],
|
||||
_ context: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
let root = Accumulator()
|
||||
let application = Application(mode: mode, parameters: parameters, caller: context, sink: Sink(depth: 0, into: root))
|
||||
stack = [.applyTemplates(nodes, application)]
|
||||
drive()
|
||||
return root.items
|
||||
}
|
||||
|
||||
private func drive() {
|
||||
while let work = stack.popLast() {
|
||||
if transformer.termination.message != nil || transformer.recursionGuard.exceeded { continue }
|
||||
switch work {
|
||||
case let .run(body, cursor, context, sink):
|
||||
stepRun(body, cursor, context, sink)
|
||||
case let .applyTemplates(nodes, application):
|
||||
stepApplyTemplates(nodes, application)
|
||||
case let .applyOne(xnode, position, size, application):
|
||||
stepApplyOne(xnode, position, size, application)
|
||||
case let .finish(kind, child, into):
|
||||
stepFinish(kind, child, into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
221
Sources/XSLT/XSLTInstantiationElements.swift
Normal file
221
Sources/XSLT/XSLTInstantiationElements.swift
Normal file
@ -0,0 +1,221 @@
|
||||
// Element construction, template application, and node assembly for the iterative
|
||||
// XSLT evaluator (see XSLTInstantiation.swift).
|
||||
|
||||
extension XSLTDriver {
|
||||
// MARK: Element construction
|
||||
|
||||
/// The attributes an element carries before its body content: attribute sets
|
||||
/// (lowest precedence) then literal attributes.
|
||||
private func elementBaseAttributes(
|
||||
_ literalAttributes: [PureXML.XSLT.LiteralAttribute],
|
||||
_ useAttributeSets: [String],
|
||||
_ context: XSLTContext,
|
||||
) -> [PureXML.Model.Attribute] {
|
||||
var attributes = transformer.attributeSetAttributes(useAttributeSets, context, visiting: [])
|
||||
attributes += literalAttributes.map { PureXML.Model.Attribute(name: $0.name, value: transformer.avt($0.value, context)) }
|
||||
return attributes
|
||||
}
|
||||
|
||||
/// Pushes a finished element and its body, so the body fills a child
|
||||
/// accumulator that the finish step then wraps.
|
||||
private func pushElementBody(_ finish: Finish, _ body: [Instruction], _ context: XSLTContext, _ sink: Sink) {
|
||||
let child = Accumulator()
|
||||
stack.append(.finish(finish, child, sink.into))
|
||||
stack.append(.run(body, 0, context, Sink(depth: sink.depth, into: child)))
|
||||
}
|
||||
|
||||
func pushLiteralElement(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) {
|
||||
guard case let .literalElement(name, attributes, namespaces, useAttributeSets, body) = instruction else { return }
|
||||
let aliasedAttributes = attributes.map { PureXML.XSLT.LiteralAttribute(name: transformer.aliased($0.name), value: $0.value) }
|
||||
// The copied namespace nodes (7.1.1) travel as xmlns attributes; an
|
||||
// aliased stylesheet namespace declares its result namespace instead.
|
||||
var declarations: [PureXML.XSLT.LiteralAttribute] = []
|
||||
for (prefix, uri) in namespaces.sorted(by: { $0.key < $1.key }) {
|
||||
let alias = transformer.stylesheet.namespaceAliases[uri]
|
||||
let resolvedPrefix = prefix.isEmpty ? nil : prefix
|
||||
let resolvedURI = alias?.uri ?? uri
|
||||
let attributeName = resolvedPrefix.map { "xmlns:" + $0 } ?? "xmlns"
|
||||
declarations.append(PureXML.XSLT.LiteralAttribute(name: PureXML.Model.QualifiedName(attributeName), value: [.literal(resolvedURI)]))
|
||||
}
|
||||
let base = elementBaseAttributes(declarations + aliasedAttributes, useAttributeSets, context)
|
||||
pushElementBody(.element(transformer.aliased(name), base), body, context, sink)
|
||||
}
|
||||
|
||||
func pushElement(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) {
|
||||
guard case let .element(nameTemplate, namespaceTemplate, namespaces, useAttributeSets, body) = instruction else { return }
|
||||
let raw = transformer.avt(nameTemplate, context)
|
||||
let parts = raw.split(separator: ":", omittingEmptySubsequences: false)
|
||||
let hasExplicitNamespace = (namespaceTemplate.map { !transformer.avt($0, context).isEmpty }) ?? false
|
||||
let prefix = parts.count == 2 ? String(parts[0]) : nil
|
||||
let undeclaredPrefix = !hasExplicitNamespace && (prefix.map { $0 != "xml" && namespaces[$0] == nil } ?? false)
|
||||
let unusableName = raw.isEmpty || parts.contains(where: \.isEmpty) || parts.count > 2
|
||||
|| !parts.allSatisfy { PureXML.Parsing.XMLCharacter.isValidName(String($0)) } || undeclaredPrefix
|
||||
if unusableName {
|
||||
// The recovery emits the content without the wrapper element.
|
||||
pushElementBody(.filteredElement, body, context, sink)
|
||||
return
|
||||
}
|
||||
let name = transformer.createdName(nameTemplate, namespaceTemplate, namespaces, context, isAttribute: false)
|
||||
let base = elementBaseAttributes([], useAttributeSets, context)
|
||||
pushElementBody(.element(name, base), body, context, sink)
|
||||
}
|
||||
|
||||
func pushCopy(_ useAttributeSets: [String], _ body: [Instruction], _ context: XSLTContext, _ sink: Sink) {
|
||||
// A non-tree current node copies itself.
|
||||
if let current = context.current {
|
||||
switch current {
|
||||
case let .attribute(_, attribute):
|
||||
sink.into.items.append(.attribute(attribute))
|
||||
return
|
||||
case let .namespace(_, prefix, uri):
|
||||
sink.into.items.append(.attribute(.init(prefix.isEmpty ? "xmlns" : "xmlns:" + prefix, uri)))
|
||||
return
|
||||
case .tree:
|
||||
break
|
||||
}
|
||||
}
|
||||
switch context.node.kind {
|
||||
case .element:
|
||||
let base = elementBaseAttributes(Host.namespaceDeclarations(inScopeAt: context.node), useAttributeSets, context)
|
||||
pushElementBody(.element(context.node.name ?? .init(""), base), body, context, sink)
|
||||
case .text, .cdata:
|
||||
sink.into.items.append(.node(.text(context.node.value)))
|
||||
case .comment:
|
||||
sink.into.items.append(.node(.comment(context.node.value)))
|
||||
case .processingInstruction:
|
||||
sink.into.items.append(.node(.processingInstruction(target: context.node.name?.description ?? "", data: context.node.value)))
|
||||
case .document:
|
||||
// The root copies to no element; use-attribute-sets join the enclosing
|
||||
// result element ahead of the copied content (7.5).
|
||||
let setAttributes = transformer.attributeSetAttributes(useAttributeSets, context, visiting: []).map(ResultItem.attribute)
|
||||
sink.into.items.append(contentsOf: setAttributes)
|
||||
stack.append(.run(body, 0, context, sink))
|
||||
default:
|
||||
stack.append(.run(body, 0, context, sink))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Template application
|
||||
|
||||
func stepApplyTemplates(_ nodes: [PureXML.XPath.Node], _ application: Application) {
|
||||
// Reversed so the per-node work pops in document order; each node's own
|
||||
// sub-work then completes before the next node's pops.
|
||||
let size = nodes.count
|
||||
for (offset, xnode) in nodes.enumerated().reversed() {
|
||||
stack.append(.applyOne(xnode, offset + 1, size, application))
|
||||
}
|
||||
}
|
||||
|
||||
func stepApplyOne(_ xnode: PureXML.XPath.Node, _ position: Int, _ size: Int, _ application: Application) {
|
||||
guard let owner = Host.ownerNode(xnode) else { return }
|
||||
let nodeContext = XSLTContext(
|
||||
node: owner,
|
||||
current: xnode.treeNode == nil ? xnode : nil,
|
||||
position: position,
|
||||
size: size,
|
||||
variables: application.caller.variables,
|
||||
mode: application.mode,
|
||||
)
|
||||
if let template = transformer.bestTemplate(for: xnode, mode: application.mode) {
|
||||
pushTemplateBody(template, nodeContext, application.parameters, application.caller, application.sink)
|
||||
} else {
|
||||
pushBuiltInRule(xnode, application.mode, nodeContext, application.sink)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a template's parameters and schedules its body (one deeper).
|
||||
func pushTemplateBody(
|
||||
_ template: PureXML.XSLT.Template,
|
||||
_ context: XSLTContext,
|
||||
_ parameters: [Binding],
|
||||
_ caller: XSLTContext,
|
||||
_ sink: Sink,
|
||||
) {
|
||||
let depth = sink.depth + 1
|
||||
guard depth <= transformer.maxTemplateDepth else {
|
||||
transformer.recursionGuard.exceeded = true
|
||||
return
|
||||
}
|
||||
var context = context
|
||||
context.importPrecedence = template.importPrecedence
|
||||
context.importRangeLow = template.importRangeLow
|
||||
context.namespaces = template.namespaces
|
||||
context.baseURI = template.baseURI
|
||||
for parameter in template.parameters {
|
||||
if let passed = parameters.first(where: { $0.name == parameter.name }) {
|
||||
context.variables[parameter.name] = transformer.variableValue(passed.select, passed.body, caller)
|
||||
} else {
|
||||
context.variables[parameter.name] = transformer.variableValue(parameter.select, parameter.body, context)
|
||||
}
|
||||
}
|
||||
stack.append(.run(template.body, 0, context, Sink(depth: depth, into: sink.into)))
|
||||
}
|
||||
|
||||
func pushBuiltInRule(_ xnode: PureXML.XPath.Node, _ mode: String?, _ context: XSLTContext, _ sink: Sink) {
|
||||
let depth = sink.depth + 1
|
||||
guard depth <= transformer.maxTemplateDepth else {
|
||||
transformer.recursionGuard.exceeded = true
|
||||
return
|
||||
}
|
||||
switch xnode {
|
||||
case let .tree(node):
|
||||
switch node.kind {
|
||||
case .element, .document:
|
||||
let application = Application(mode: mode, parameters: [], caller: context, sink: Sink(depth: depth, into: sink.into))
|
||||
stack.append(.applyTemplates(node.children.map { .tree($0) }, application))
|
||||
case .text, .cdata:
|
||||
sink.into.items.append(.node(.text(node.value)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .attribute(_, attribute):
|
||||
sink.into.items.append(.node(.text(attribute.value)))
|
||||
case let .namespace(_, _, uri):
|
||||
sink.into.items.append(.node(.text(uri)))
|
||||
}
|
||||
}
|
||||
|
||||
func pushCallTemplate(_ name: String, _ parameters: [Binding], _ context: XSLTContext, _ sink: Sink) {
|
||||
// XSLT 1.0 section 6: the highest import-precedence definition wins.
|
||||
guard let template = transformer.stylesheet.templates.filter({ $0.name == name })
|
||||
.max(by: { $0.importPrecedence < $1.importPrecedence }) else { return }
|
||||
pushTemplateBody(template, context, parameters, context, sink)
|
||||
}
|
||||
|
||||
func pushApplyImports(_ context: XSLTContext, _ sink: Sink) {
|
||||
if let template = transformer.bestTemplate(for: context.focus, mode: context.mode, below: context.importPrecedence, atLeast: context.importRangeLow) {
|
||||
pushTemplateBody(template, context, [], context, sink)
|
||||
} else {
|
||||
pushBuiltInRule(context.focus, context.mode, context, sink)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Finishing a node
|
||||
|
||||
func stepFinish(_ kind: Finish, _ child: Accumulator, _ into: Accumulator) {
|
||||
switch kind {
|
||||
case let .element(name, baseAttributes):
|
||||
var attributes = baseAttributes
|
||||
var children: [PureXML.Model.Node] = []
|
||||
for item in child.items {
|
||||
switch item {
|
||||
case let .attribute(attribute):
|
||||
// Ignored once content has been added (the recovery for an
|
||||
// attribute created after children).
|
||||
if children.isEmpty { attributes.append(attribute) }
|
||||
case let .node(node):
|
||||
children.append(node)
|
||||
}
|
||||
}
|
||||
into.items.append(.node(.element(.init(name: name, attributes: Host.deduplicated(attributes), children: children))))
|
||||
case .filteredElement:
|
||||
into.items.append(contentsOf: child.items.filter { if case .attribute = $0 { false } else { true } })
|
||||
case .comment:
|
||||
into.items.append(.node(.comment(transformer.escapedTextValue(of: child.items))))
|
||||
case let .processingInstruction(target):
|
||||
into.items.append(.node(.processingInstruction(target: target, data: transformer.escapedTextValue(of: child.items))))
|
||||
case let .attribute(name):
|
||||
into.items.append(.attribute(.init(name: name, value: transformer.escapedTextValue(of: child.items))))
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Sources/XSLT/XSLTInstantiationSteps.swift
Normal file
131
Sources/XSLT/XSLTInstantiationSteps.swift
Normal file
@ -0,0 +1,131 @@
|
||||
// The step methods of the iterative XSLT evaluator (see XSLTInstantiation.swift).
|
||||
// Each instruction either appends its produced items to the sink synchronously or
|
||||
// pushes sub-work; the rest of the body is always pushed first so it pops last,
|
||||
// keeping result-tree document order.
|
||||
|
||||
extension XSLTDriver {
|
||||
func stepRun(_ body: [Instruction], _ cursor: Int, _ context: XSLTContext, _ sink: Sink) {
|
||||
guard cursor < body.count else { return }
|
||||
let instruction = body[cursor]
|
||||
switch instruction {
|
||||
case let .variable(name, select, variableBody):
|
||||
// A variable binds for the rest of the body; thread the updated context.
|
||||
var context = context
|
||||
context.variables[name] = transformer.variableValue(select, variableBody, context)
|
||||
stack.append(.run(body, cursor + 1, context, sink))
|
||||
return
|
||||
case let .fallback(fallbackBody):
|
||||
stack.append(.run(body, cursor + 1, context, sink))
|
||||
stack.append(.run(fallbackBody, 0, context, sink))
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
// The remainder of the body runs after this instruction's items.
|
||||
stack.append(.run(body, cursor + 1, context, sink))
|
||||
if runSimple(instruction, context, sink) { return }
|
||||
if runControl(instruction, context, sink) { return }
|
||||
if runTemplate(instruction, context, sink) { return }
|
||||
runConstruct(instruction, context, sink)
|
||||
}
|
||||
|
||||
private func runSimple(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
|
||||
switch instruction {
|
||||
case let .literalText(text):
|
||||
sink.into.items.append(.node(.text(text)))
|
||||
case let .valueOf(select, raw):
|
||||
let value = transformer.string(select, context)
|
||||
sink.into.items.append(.node(.text(raw ? PureXML.XSLT.RawText.marked(value) : value)))
|
||||
case .number:
|
||||
sink.into.items.append(.node(.text(transformer.numberInstruction(instruction, context))))
|
||||
case let .copyOf(select):
|
||||
sink.into.items.append(contentsOf: transformer.copyOf(select, context))
|
||||
case let .message(terminate, messageBody):
|
||||
if terminate, transformer.termination.message == nil {
|
||||
transformer.termination.message = Host.text(of: transformer.instantiate(messageBody, context))
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func runControl(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
|
||||
switch instruction {
|
||||
case let .ifInstruction(test, thenBody):
|
||||
if transformer.boolean(test, context) { stack.append(.run(thenBody, 0, context, sink)) }
|
||||
case let .choose(whens, otherwise):
|
||||
let branch = whens.first { transformer.boolean($0.test, context) }?.body ?? otherwise
|
||||
stack.append(.run(branch, 0, context, sink))
|
||||
case let .forEach(select, sorts, eachBody):
|
||||
pushForEach(select, sorts, eachBody, context, sink)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func runTemplate(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
|
||||
switch instruction {
|
||||
case let .applyTemplates(select, mode, sorts, parameters):
|
||||
let nodes = transformer.sorted(transformer.selectXPathNodes(select ?? "node()", context), sorts, context)
|
||||
stack.append(.applyTemplates(nodes, Application(mode: mode, parameters: parameters, caller: context, sink: sink)))
|
||||
case let .callTemplate(name, parameters):
|
||||
pushCallTemplate(name, parameters, context, sink)
|
||||
case .applyImports:
|
||||
pushApplyImports(context, sink)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func runConstruct(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) {
|
||||
switch instruction {
|
||||
case .literalElement:
|
||||
pushLiteralElement(instruction, context, sink)
|
||||
case .element:
|
||||
pushElement(instruction, context, sink)
|
||||
case let .attribute(nameTemplate, namespaceTemplate, namespaces, attributeBody):
|
||||
let name = transformer.createdName(nameTemplate, namespaceTemplate, namespaces, context, isAttribute: true)
|
||||
pushTextNode(.attribute(name), attributeBody, context, sink)
|
||||
case let .copy(useAttributeSets, copyBody):
|
||||
pushCopy(useAttributeSets, copyBody, context, sink)
|
||||
case let .comment(commentBody):
|
||||
pushTextNode(.comment, commentBody, context, sink)
|
||||
case let .processingInstruction(nameTemplate, piBody):
|
||||
pushTextNode(.processingInstruction(transformer.avt(nameTemplate, context)), piBody, context, sink)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func pushForEach(
|
||||
_ select: String,
|
||||
_ sorts: [PureXML.XSLT.Sort],
|
||||
_ body: [Instruction],
|
||||
_ context: XSLTContext,
|
||||
_ sink: Sink,
|
||||
) {
|
||||
let nodes = transformer.sorted(transformer.selectXPathNodes(select, context), sorts, context)
|
||||
// Reversed so the nodes' bodies pop in document order into the sink.
|
||||
for (offset, xnode) in nodes.enumerated().reversed() {
|
||||
guard let owner = Host.ownerNode(xnode) else { continue }
|
||||
var itemContext = XSLTContext(
|
||||
node: owner,
|
||||
current: xnode.treeNode == nil ? xnode : nil,
|
||||
position: offset + 1,
|
||||
size: nodes.count,
|
||||
variables: context.variables,
|
||||
)
|
||||
itemContext.baseURI = context.baseURI
|
||||
stack.append(.run(body, 0, itemContext, sink))
|
||||
}
|
||||
}
|
||||
|
||||
private func pushTextNode(_ kind: Finish, _ body: [Instruction], _ context: XSLTContext, _ sink: Sink) {
|
||||
let child = Accumulator()
|
||||
stack.append(.finish(kind, child, sink.into))
|
||||
stack.append(.run(body, 0, context, Sink(depth: sink.depth, into: child)))
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,61 @@
|
||||
/// A mutable bag of fixed-up nodes a branch's children gather into, so the
|
||||
/// deferred build step assembles the parent once they are done.
|
||||
private final class FixupAccumulator {
|
||||
var nodes: [PureXML.Model.Node] = []
|
||||
}
|
||||
|
||||
/// One unit of namespace-fixup work: visit a source node (with the namespace
|
||||
/// bindings in scope, appending its fixed form to `into`), or, after a branch's
|
||||
/// children are gathered, build the element or document.
|
||||
private enum FixupWork {
|
||||
case visit(PureXML.Model.Node, inScope: [String: String], into: FixupAccumulator)
|
||||
case buildElement(PureXML.Model.QualifiedName, [PureXML.Model.Attribute], FixupAccumulator, FixupAccumulator)
|
||||
case buildDocument(FixupAccumulator, FixupAccumulator)
|
||||
}
|
||||
|
||||
/// Namespace fixup over an XSLT result tree (the serialization step of
|
||||
/// XSLT 1.0 section 7.1): every element and attribute name that carries a
|
||||
/// namespace URI gets an in-scope declaration, with `ns0`, `ns1`, ... prefixes
|
||||
/// generated when the carried prefix is absent or bound to a different URI.
|
||||
enum XSLTNamespaceFixup {
|
||||
private static let xmlNamespace = "http://www.w3.org/XML/1998/namespace"
|
||||
|
||||
/// Rebuilds the tree with namespace declarations fixed, without recursing on
|
||||
/// its depth: an explicit work stack drives a pre-order visit (so the `ns0`,
|
||||
/// `ns1`, ... counter advances in document order, exactly as the recursive form
|
||||
/// did) that gathers each branch's fixed children into an accumulator a
|
||||
/// deferred build step assembles.
|
||||
static func apply(_ node: PureXML.Model.Node) -> PureXML.Model.Node {
|
||||
var counter = 0
|
||||
return fix(node, inScope: ["xml": "http://www.w3.org/XML/1998/namespace"], counter: &counter)
|
||||
let root = FixupAccumulator()
|
||||
var stack: [FixupWork] = [.visit(node, inScope: ["xml": xmlNamespace], into: root)]
|
||||
while let work = stack.popLast() {
|
||||
switch work {
|
||||
case let .visit(node, inScope, into):
|
||||
switch node {
|
||||
case let .document(children):
|
||||
let gathered = FixupAccumulator()
|
||||
stack.append(.buildDocument(gathered, into))
|
||||
for child in children.reversed() {
|
||||
stack.append(.visit(child, inScope: inScope, into: gathered))
|
||||
}
|
||||
case let .element(element):
|
||||
let (attributes, childScope) = fixElement(element, inScope: inScope, counter: &counter)
|
||||
let gathered = FixupAccumulator()
|
||||
stack.append(.buildElement(element.name, attributes, gathered, into))
|
||||
for child in element.children.reversed() {
|
||||
stack.append(.visit(child, inScope: childScope, into: gathered))
|
||||
}
|
||||
default:
|
||||
into.nodes.append(node)
|
||||
}
|
||||
case let .buildElement(name, attributes, gathered, into):
|
||||
into.nodes.append(.element(.init(name: name, attributes: attributes, children: gathered.nodes)))
|
||||
case let .buildDocument(gathered, into):
|
||||
into.nodes.append(.document(gathered.nodes))
|
||||
}
|
||||
}
|
||||
return root.nodes.first ?? node
|
||||
}
|
||||
|
||||
/// Declarations the element already carries enter scope first.
|
||||
@ -34,17 +84,6 @@ enum XSLTNamespaceFixup {
|
||||
}
|
||||
}
|
||||
|
||||
private static func fix(_ node: PureXML.Model.Node, inScope: [String: String], counter: inout Int) -> PureXML.Model.Node {
|
||||
switch node {
|
||||
case let .document(children):
|
||||
.document(children.map { fix($0, inScope: inScope, counter: &counter) })
|
||||
case let .element(element):
|
||||
.element(fix(element, inScope: inScope, counter: &counter))
|
||||
default:
|
||||
node
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops declarations that repeat an inherited binding: copied 7.1.1
|
||||
/// namespace nodes travel on every literal element and would otherwise
|
||||
/// redeclare on each descendant.
|
||||
@ -60,7 +99,14 @@ enum XSLTNamespaceFixup {
|
||||
}
|
||||
}
|
||||
|
||||
private static func fix(_ element: PureXML.Model.Element, inScope: [String: String], counter: inout Int) -> PureXML.Model.Element {
|
||||
/// Fixes an element's own attributes and computes the namespace scope its
|
||||
/// children inherit (the per-element work of the fixup, without recursing into
|
||||
/// the children). The `ns0`, `ns1`, ... counter advances here, in visit order.
|
||||
private static func fixElement(
|
||||
_ element: PureXML.Model.Element,
|
||||
inScope: [String: String],
|
||||
counter: inout Int,
|
||||
) -> (attributes: [PureXML.Model.Attribute], scope: [String: String]) {
|
||||
var scope = inScope
|
||||
var attributes = withoutRedundantDeclarations(element.attributes, inScope: inScope)
|
||||
enterDeclarations(attributes, into: &scope)
|
||||
@ -79,9 +125,8 @@ enum XSLTNamespaceFixup {
|
||||
continue // Already bound correctly.
|
||||
}
|
||||
if let candidate = prefix, !localPrefixes.contains(candidate), element.name.prefix != candidate {
|
||||
// Redeclare the carried prefix locally, shadowing any
|
||||
// inherited binding (the source document's own shape). The
|
||||
// element's own prefix is never shadowed.
|
||||
// Redeclare the carried prefix locally, shadowing any inherited
|
||||
// binding. The element's own prefix is never shadowed.
|
||||
localPrefixes.insert(candidate)
|
||||
} else if prefix == nil || scope[prefix ?? ""] != nil {
|
||||
// Generate a fresh prefix when absent or taken locally.
|
||||
@ -105,7 +150,6 @@ enum XSLTNamespaceFixup {
|
||||
value: attributes[index].value,
|
||||
)
|
||||
}
|
||||
let children = element.children.map { fix($0, inScope: scope, counter: &counter) }
|
||||
return .init(name: element.name, attributes: attributes, children: children)
|
||||
return (attributes, scope)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
extension PureXML.XSLT {
|
||||
/// The default ceiling on template-instantiation nesting (see
|
||||
/// ``RecursionGuard``). Chosen to clear the parser's max source depth (256),
|
||||
/// so an identity transform of the deepest permitted source still runs, while
|
||||
/// staying within an 8 MB stack with margin; unbounded template recursion
|
||||
/// (which can overflow the stack) trips it well before that. Configurable per
|
||||
/// transform for stylesheets with deeper legitimate recursion.
|
||||
public static let defaultMaxTemplateDepth = 300
|
||||
/// The default ceiling on logical template-application nesting (see
|
||||
/// ``RecursionGuard``). The evaluator is iterative, so this is no longer a
|
||||
/// stack limit but a runaway guard: legitimate deep recursion (well into the
|
||||
/// thousands) succeeds, while an infinite or unbounded recursion, whose result
|
||||
/// tree would grow without bound, fails gracefully once it passes this depth.
|
||||
/// Configurable per transform.
|
||||
public static let defaultMaxTemplateDepth = 40000
|
||||
|
||||
/// One produced item: a node for the result tree, or an attribute to attach to
|
||||
/// the enclosing element.
|
||||
@ -73,14 +73,15 @@ extension PureXML.XSLT {
|
||||
var message: String?
|
||||
}
|
||||
|
||||
/// Shared, mutable guard against unbounded template-instantiation recursion
|
||||
/// (a recursive template whose depth is driven by source data would otherwise
|
||||
/// overflow the native stack). `depth` tracks the current nesting of template
|
||||
/// applications; `exceeded` latches once it passes the configured limit, so the
|
||||
/// instantiation loops unwind and the transform fails gracefully. A reference
|
||||
/// type so the value-semantics transformer can update it.
|
||||
/// Shared, mutable latch for runaway template recursion. The evaluator is
|
||||
/// iterative, so deep recursion no longer overflows the stack; this instead
|
||||
/// caps the *logical* template-application depth (carried on each work item)
|
||||
/// so an unbounded or infinite recursion, whose result tree would grow without
|
||||
/// bound, fails gracefully rather than exhausting memory. `exceeded` latches
|
||||
/// once the depth passes `maxTemplateDepth`, so the work stack drains and the
|
||||
/// transform throws. A reference type so the value-semantics transformer can
|
||||
/// set it.
|
||||
final class RecursionGuard {
|
||||
var depth = 0
|
||||
var exceeded = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,64 +87,6 @@ extension PureXML.XSLT.Transformer {
|
||||
text.enumerated().compactMap { $1 == " " ? $0 : nil }
|
||||
}
|
||||
|
||||
func forEach(
|
||||
_ select: String,
|
||||
_ sorts: [PureXML.XSLT.Sort],
|
||||
_ body: [PureXML.XSLT.Instruction],
|
||||
_ context: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
let nodes = sorted(selectXPathNodes(select, context), sorts, context)
|
||||
var items: [ResultItem] = []
|
||||
for (offset, xnode) in nodes.enumerated() {
|
||||
guard let owner = Self.ownerNode(xnode) else { continue }
|
||||
var itemContext = XSLTContext(
|
||||
node: owner,
|
||||
current: xnode.treeNode == nil ? xnode : nil,
|
||||
position: offset + 1,
|
||||
size: nodes.count,
|
||||
variables: context.variables,
|
||||
)
|
||||
itemContext.baseURI = context.baseURI
|
||||
items += instantiate(body, itemContext)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func chooseInstruction(
|
||||
_ whens: [PureXML.XSLT.Branch],
|
||||
_ otherwise: [PureXML.XSLT.Instruction],
|
||||
_ context: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
for branch in whens where boolean(branch.test, context) {
|
||||
return instantiate(branch.body, context)
|
||||
}
|
||||
return instantiate(otherwise, context)
|
||||
}
|
||||
|
||||
func builtInRule(_ xnode: PureXML.XPath.Node, mode: String?, _ context: XSLTContext) -> [ResultItem] {
|
||||
recursionGuard.depth += 1
|
||||
defer { recursionGuard.depth -= 1 }
|
||||
guard recursionGuard.depth <= maxTemplateDepth else {
|
||||
recursionGuard.exceeded = true
|
||||
return []
|
||||
}
|
||||
return switch xnode {
|
||||
case let .tree(node):
|
||||
switch node.kind {
|
||||
case .element, .document:
|
||||
applyTemplates(to: node.children.map { .tree($0) }, mode: mode, parameters: [], context)
|
||||
case .text, .cdata:
|
||||
[.node(.text(node.value))]
|
||||
default:
|
||||
[]
|
||||
}
|
||||
case let .attribute(_, attribute):
|
||||
[.node(.text(attribute.value))]
|
||||
case let .namespace(_, _, uri):
|
||||
[.node(.text(uri))]
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the nodes selected by `select` into the result tree (`xsl:copy-of`);
|
||||
/// a non-node-set result copies as its string value. Whole subtrees are taken
|
||||
/// as-is, so this does not recurse on their depth.
|
||||
@ -162,40 +104,4 @@ extension PureXML.XSLT.Transformer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func message(_ terminate: Bool, _ body: [PureXML.XSLT.Instruction], _ context: XSLTContext) -> [ResultItem] {
|
||||
if terminate, termination.message == nil {
|
||||
termination.message = Self.text(of: instantiate(body, context))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Binds the template's parameters (a passed `with-param` wins over the
|
||||
/// declared default) and instantiates its body.
|
||||
func instantiateTemplate(
|
||||
_ template: PureXML.XSLT.Template,
|
||||
_ context: XSLTContext,
|
||||
passing parameters: [PureXML.XSLT.Binding],
|
||||
from caller: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
recursionGuard.depth += 1
|
||||
defer { recursionGuard.depth -= 1 }
|
||||
guard recursionGuard.depth <= maxTemplateDepth else {
|
||||
recursionGuard.exceeded = true
|
||||
return []
|
||||
}
|
||||
var context = context
|
||||
context.importPrecedence = template.importPrecedence
|
||||
context.importRangeLow = template.importRangeLow
|
||||
context.namespaces = template.namespaces
|
||||
context.baseURI = template.baseURI
|
||||
for parameter in template.parameters {
|
||||
if let passed = parameters.first(where: { $0.name == parameter.name }) {
|
||||
context.variables[parameter.name] = variableValue(passed.select, passed.body, caller)
|
||||
} else {
|
||||
context.variables[parameter.name] = variableValue(parameter.select, parameter.body, context)
|
||||
}
|
||||
}
|
||||
return instantiate(template.body, context)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,27 +86,31 @@ public extension PureXML.XSLT {
|
||||
/// prepended to the children of the first `head` element (16.2: the html
|
||||
/// output method should add a meta element giving the encoding).
|
||||
private static func withContentTypeMeta(_ node: PureXML.Model.Node, encoding: String) -> PureXML.Model.Node {
|
||||
switch node {
|
||||
case let .document(children):
|
||||
return .document(children.map { withContentTypeMeta($0, encoding: encoding) })
|
||||
case let .element(element):
|
||||
var children = element.children.map { withContentTypeMeta($0, encoding: encoding) }
|
||||
let needsMeta = element.name.localName.lowercased() == "head"
|
||||
&& !children.contains(where: { isContentTypeMeta($0) })
|
||||
if needsMeta {
|
||||
let meta = PureXML.Model.Element(
|
||||
name: .init("META"),
|
||||
attributes: [
|
||||
.init("http-equiv", "Content-Type"),
|
||||
.init("content", "text/html; charset=\(encoding)"),
|
||||
],
|
||||
children: [],
|
||||
)
|
||||
children.insert(.element(meta), at: 0)
|
||||
// Rebuilt bottom-up through the shared iterative spine so a deeply-nested
|
||||
// result does not overflow the stack.
|
||||
node.rebuildingBottomUp { node, rebuiltChildren in
|
||||
switch node {
|
||||
case .document:
|
||||
return .document(rebuiltChildren)
|
||||
case let .element(element):
|
||||
var children = rebuiltChildren
|
||||
let needsMeta = element.name.localName.lowercased() == "head"
|
||||
&& !children.contains(where: { isContentTypeMeta($0) })
|
||||
if needsMeta {
|
||||
let meta = PureXML.Model.Element(
|
||||
name: .init("META"),
|
||||
attributes: [
|
||||
.init("http-equiv", "Content-Type"),
|
||||
.init("content", "text/html; charset=\(encoding)"),
|
||||
],
|
||||
children: [],
|
||||
)
|
||||
children.insert(.element(meta), at: 0)
|
||||
}
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
return node
|
||||
}
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,18 +123,24 @@ public extension PureXML.XSLT {
|
||||
/// replaced by CDATA nodes, so the serializer emits them as CDATA sections.
|
||||
private static func withCDATASections(_ node: PureXML.Model.Node, _ names: Set<String>) -> PureXML.Model.Node {
|
||||
guard !names.isEmpty else { return node }
|
||||
switch node {
|
||||
case let .document(children):
|
||||
return .document(children.map { withCDATASections($0, names) })
|
||||
case let .element(element):
|
||||
let wrap = names.contains(element.name.localName) || names.contains(element.name.description)
|
||||
let children = element.children.flatMap { child -> [PureXML.Model.Node] in
|
||||
if wrap, case let .text(value) = child { return cdataNodes(value) }
|
||||
return [withCDATASections(child, names)]
|
||||
// Rebuilt bottom-up through the shared iterative spine; an element named in
|
||||
// `names` has its own text children re-wrapped as CDATA.
|
||||
return node.rebuildingBottomUp { node, rebuiltChildren in
|
||||
switch node {
|
||||
case .document:
|
||||
return .document(rebuiltChildren)
|
||||
case let .element(element):
|
||||
let wrap = names.contains(element.name.localName) || names.contains(element.name.description)
|
||||
let children = wrap
|
||||
? rebuiltChildren.flatMap { child -> [PureXML.Model.Node] in
|
||||
if case let .text(value) = child { return cdataNodes(value) }
|
||||
return [child]
|
||||
}
|
||||
: rebuiltChildren
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
return node
|
||||
}
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,11 +254,23 @@ public extension PureXML.XSLT {
|
||||
/// The concatenated text content of a result tree, for the `text` output
|
||||
/// method: character data only, with markup, comments, and PIs dropped.
|
||||
private static func textValue(of node: PureXML.Model.Node) -> String {
|
||||
switch node {
|
||||
case let .text(value), let .cdata(value): value
|
||||
case let .document(children): children.map(textValue).joined()
|
||||
case let .element(element): element.children.map(textValue).joined()
|
||||
case .comment, .processingInstruction: ""
|
||||
// Iterative pre-order walk so a deeply-nested result tree does not overflow
|
||||
// the stack; children push reversed to keep document order. Only character
|
||||
// data contributes; markup, comments, and PIs drop out.
|
||||
var result = ""
|
||||
var stack: [PureXML.Model.Node] = [node]
|
||||
while let current = stack.popLast() {
|
||||
switch current {
|
||||
case let .text(value), let .cdata(value):
|
||||
result += value
|
||||
case let .document(children):
|
||||
stack.append(contentsOf: children.reversed())
|
||||
case let .element(element):
|
||||
stack.append(contentsOf: element.children.reversed())
|
||||
case .comment, .processingInstruction:
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ extension PureXML.XSLT {
|
||||
return .document(applyTemplates(to: [.tree(root)], mode: nil, parameters: [], context).compactMap(Self.nodeOf))
|
||||
}
|
||||
|
||||
fileprivate func bestTemplate(for node: PureXML.XPath.Node, mode: String?, below ceiling: Int = .max, atLeast floor: Int = .min) -> Template? {
|
||||
func bestTemplate(for node: PureXML.XPath.Node, mode: String?, below ceiling: Int = .max, atLeast floor: Int = .min) -> Template? {
|
||||
stylesheet.templates.enumerated()
|
||||
.filter { entry in
|
||||
entry.element.mode == mode && entry.element.importPrecedence < ceiling
|
||||
@ -144,50 +144,6 @@ extension PureXML.XSLT {
|
||||
patternTable
|
||||
}
|
||||
|
||||
func applyTemplates(
|
||||
to nodes: [PureXML.XPath.Node],
|
||||
mode: String?,
|
||||
parameters: [Binding],
|
||||
_ context: XSLTContext,
|
||||
) -> [ResultItem] {
|
||||
var items: [ResultItem] = []
|
||||
for (offset, xnode) in nodes.enumerated() {
|
||||
if termination.message != nil || recursionGuard.exceeded { break }
|
||||
guard let owner = Self.ownerNode(xnode) else { continue }
|
||||
let nodeContext = XSLTContext(
|
||||
node: owner,
|
||||
current: xnode.treeNode == nil ? xnode : nil,
|
||||
position: offset + 1,
|
||||
size: nodes.count,
|
||||
variables: context.variables,
|
||||
mode: mode,
|
||||
)
|
||||
if let template = bestTemplate(for: xnode, mode: mode) {
|
||||
items += instantiateTemplate(template, nodeContext, passing: parameters, from: context)
|
||||
} else {
|
||||
items += builtInRule(xnode, mode: mode, nodeContext)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func instantiate(_ body: [Instruction], _ context: XSLTContext) -> [ResultItem] {
|
||||
var items: [ResultItem] = []
|
||||
var context = context
|
||||
for instruction in body {
|
||||
if termination.message != nil || recursionGuard.exceeded { break }
|
||||
switch instruction {
|
||||
case let .variable(name, select, varBody):
|
||||
context.variables[name] = variableValue(select, varBody, context)
|
||||
case let .fallback(fallbackBody):
|
||||
items += instantiate(fallbackBody, context)
|
||||
default:
|
||||
items += evaluate(instruction, context)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func variableValue(_ select: String?, _ body: [Instruction], _ context: XSLTContext) -> PureXML.XPath.Value {
|
||||
if let select { return value(select, context) ?? .string("") }
|
||||
// XSLT 1.0 11.2: a variable with no select and empty content is the
|
||||
@ -258,119 +214,28 @@ extension PureXML.XSLT {
|
||||
}
|
||||
|
||||
private static func stringValue(_ node: PureXML.Model.Node) -> String {
|
||||
switch node {
|
||||
case let .text(value), let .cdata(value): value
|
||||
case let .element(element): element.children.reduce(into: "") { $0 += stringValue($1) }
|
||||
case let .document(children): children.reduce(into: "") { $0 += stringValue($1) }
|
||||
default: ""
|
||||
// Iterative pre-order walk so a deeply-nested result tree does not
|
||||
// overflow the stack; only text and CDATA contribute.
|
||||
var result = ""
|
||||
var stack: [PureXML.Model.Node] = [node]
|
||||
while let current = stack.popLast() {
|
||||
switch current {
|
||||
case let .text(value), let .cdata(value):
|
||||
result += value
|
||||
case let .element(element):
|
||||
stack.append(contentsOf: element.children.reversed())
|
||||
case let .document(children):
|
||||
stack.append(contentsOf: children.reversed())
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.XSLT.Transformer {
|
||||
fileprivate func evaluate(_ instruction: PureXML.XSLT.Instruction, _ context: XSLTContext) -> [ResultItem] {
|
||||
simpleEvaluate(instruction, context) ?? structuralEvaluate(instruction, context)
|
||||
}
|
||||
|
||||
private func simpleEvaluate(_ instruction: PureXML.XSLT.Instruction, _ context: XSLTContext) -> [ResultItem]? {
|
||||
switch instruction {
|
||||
case let .literalText(text): [.node(.text(text))]
|
||||
case let .valueOf(select, raw):
|
||||
[.node(.text(raw ? PureXML.XSLT.RawText.marked(string(select, context)) : string(select, context)))]
|
||||
case let .applyTemplates(select, mode, sorts, parameters):
|
||||
applyTemplates(
|
||||
to: sorted(selectXPathNodes(select ?? "node()", context), sorts, context),
|
||||
mode: mode,
|
||||
parameters: parameters,
|
||||
context,
|
||||
)
|
||||
case let .forEach(select, sorts, body): forEach(select, sorts, body, context)
|
||||
case let .ifInstruction(test, body): boolean(test, context) ? instantiate(body, context) : []
|
||||
case let .choose(whens, otherwise): chooseInstruction(whens, otherwise, context)
|
||||
case let .copyOf(select): copyOf(select, context)
|
||||
case let .callTemplate(name, parameters): callTemplate(name, parameters, context)
|
||||
case .variable: []
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiates an `xsl:message` body as its text; `terminate` records the
|
||||
// signal so the transform aborts with it. Produces no result-tree output.
|
||||
|
||||
// Builds a literal result element, rewriting its name and attribute names
|
||||
// through any `xsl:namespace-alias` in effect.
|
||||
|
||||
private func structuralEvaluate(_ instruction: PureXML.XSLT.Instruction, _ context: XSLTContext) -> [ResultItem] {
|
||||
switch instruction {
|
||||
case .literalElement:
|
||||
[literalResult(instruction, context)]
|
||||
case .element:
|
||||
elementInstruction(instruction, context)
|
||||
case let .attribute(nameTemplate, namespaceTemplate, namespaces, body):
|
||||
attributeInstruction(nameTemplate, namespaceTemplate, namespaces, body, context)
|
||||
case let .copy(useAttributeSets, body):
|
||||
copyInstruction(useAttributeSets, body, context)
|
||||
case .number:
|
||||
[.node(.text(numberInstruction(instruction, context)))]
|
||||
case let .comment(body):
|
||||
[.node(.comment(escapedTextValue(of: instantiate(body, context))))]
|
||||
case let .processingInstruction(name, body):
|
||||
[.node(.processingInstruction(target: avt(name, context), data: escapedTextValue(of: instantiate(body, context))))]
|
||||
case let .message(terminate, body):
|
||||
message(terminate, body, context)
|
||||
case .applyImports:
|
||||
applyImports(context)
|
||||
default:
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-applies templates to the current node in the current mode, considering
|
||||
/// only those below the current template's import precedence, falling back to
|
||||
/// the built-in rule when none match.
|
||||
private func applyImports(_ context: XSLTContext) -> [ResultItem] {
|
||||
if let template = bestTemplate(for: context.focus, mode: context.mode, below: context.importPrecedence, atLeast: context.importRangeLow) {
|
||||
return instantiateTemplate(template, context, passing: [], from: context)
|
||||
}
|
||||
return builtInRule(context.focus, mode: context.mode, context)
|
||||
}
|
||||
|
||||
private func callTemplate(_ name: String, _ parameters: [PureXML.XSLT.Binding], _ context: XSLTContext) -> [ResultItem] {
|
||||
// XSLT 1.0 section 6: pick the highest import-precedence definition, not the
|
||||
// first by position, so an included template outranks a same-name import.
|
||||
guard let template = stylesheet.templates.filter({ $0.name == name })
|
||||
.max(by: { $0.importPrecedence < $1.importPrecedence }) else { return [] }
|
||||
return instantiateTemplate(template, context, passing: parameters, from: context)
|
||||
}
|
||||
|
||||
// MARK: Building elements
|
||||
|
||||
func buildElement(
|
||||
name: PureXML.Model.QualifiedName,
|
||||
literalAttributes: [PureXML.XSLT.LiteralAttribute],
|
||||
useAttributeSets: [String],
|
||||
body: [PureXML.XSLT.Instruction],
|
||||
_ context: XSLTContext,
|
||||
) -> ResultItem {
|
||||
// Attribute sets are lowest precedence, then the element's literal
|
||||
// attributes, then its xsl:attribute body; a later same-named attribute
|
||||
// replaces an earlier one.
|
||||
var attributes = attributeSetAttributes(useAttributeSets, context, visiting: [])
|
||||
attributes += literalAttributes.map { PureXML.Model.Attribute(name: $0.name, value: avt($0.value, context)) }
|
||||
var children: [PureXML.Model.Node] = []
|
||||
for item in instantiate(body, context) {
|
||||
switch item {
|
||||
case let .attribute(attribute):
|
||||
// Ignored once content has been added (the XSLT recovery for
|
||||
// an attribute created after children).
|
||||
if children.isEmpty { attributes.append(attribute) }
|
||||
case let .node(node): children.append(node)
|
||||
}
|
||||
}
|
||||
return .node(.element(.init(name: name, attributes: Self.deduplicated(attributes), children: children)))
|
||||
}
|
||||
|
||||
/// The attributes contributed by `names` and the attribute sets they include,
|
||||
/// lower precedence first, with a `visiting` guard against recursive includes.
|
||||
func attributeSetAttributes(_ names: [String], _ context: XSLTContext, visiting: Set<String>) -> [PureXML.Model.Attribute] {
|
||||
|
||||
@ -9,7 +9,6 @@ extend/extend04
|
||||
idkey/idkey31
|
||||
impincl/impincl08
|
||||
impincl/impincl27
|
||||
mdocs/mdocs07
|
||||
mdocs/mdocs19
|
||||
output/output22
|
||||
output/output26
|
||||
|
||||
@ -1,96 +1,59 @@
|
||||
// The recursion guard must be exercised on a large stack (the limit is above
|
||||
// what a small stack holds), which needs Foundation's Thread. WASI is
|
||||
// single-threaded and has no Thread/Dispatch, so these tests are excluded
|
||||
// there; the guard itself is platform-independent pure Swift, covered by the
|
||||
// macOS and Linux runs.
|
||||
#if !os(WASI)
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// XSLT template recursion is bounded so an unbounded recursive template (whose
|
||||
/// depth can be driven by source data) fails gracefully instead of overflowing
|
||||
/// the stack (#356). Source *nesting* depth is already bounded by the parser's
|
||||
/// `maxDepth`, so it is not the exposure; a recursive named template is.
|
||||
///
|
||||
/// These run on a dedicated large-stack thread: the recursion limit (300) is
|
||||
/// above what swift-testing's small Task stack holds, so the guard must be given
|
||||
/// a production-sized stack to fire on rather than the test harness overflowing
|
||||
/// first. That is exactly the gap the limit closes on a normal 8 MB main thread.
|
||||
@Suite("XSLT template recursion limit")
|
||||
struct XSLTRecursionLimitTests {
|
||||
private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
|
||||
/// The XSLT evaluator is iterative, so template recursion is bounded by heap, not
|
||||
/// the native stack (#356). Deep recursion that would once have overflowed the
|
||||
/// stack now succeeds, even here where swift-testing runs on a small Task stack;
|
||||
/// only a runaway/infinite recursion (whose result tree would grow without bound)
|
||||
/// fails gracefully at the configurable `maxTemplateDepth`.
|
||||
@Suite("XSLT template recursion")
|
||||
struct XSLTRecursionTests {
|
||||
private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
|
||||
|
||||
/// Runs `body` on a thread with a large stack and returns its result, so the
|
||||
/// recursion guard is exercised on a production-sized stack.
|
||||
private func onLargeStack<T: Sendable>(_ body: @escaping @Sendable () -> T) -> T {
|
||||
let box = Box<T>()
|
||||
let done = DispatchSemaphore(value: 0)
|
||||
let thread = Thread {
|
||||
box.value = body()
|
||||
done.signal()
|
||||
}
|
||||
thread.stackSize = 64 * 1024 * 1024
|
||||
thread.start()
|
||||
done.wait()
|
||||
guard let value = box.value else { preconditionFailure("the worker thread produced no result") }
|
||||
return value
|
||||
}
|
||||
/// A stylesheet whose named template recurses `count` times, driving the
|
||||
/// recursion from the passed parameter (a stand-in for `count(//x)`).
|
||||
private func recursiveStylesheet(_ count: Int) -> String {
|
||||
"<xsl:stylesheet version=\"1.0\" \(xsl)>"
|
||||
+ "<xsl:template match=\"/\"><xsl:call-template name=\"d\">"
|
||||
+ "<xsl:with-param name=\"n\" select=\"\(count)\"/></xsl:call-template></xsl:template>"
|
||||
+ "<xsl:template name=\"d\"><xsl:param name=\"n\"/>"
|
||||
+ "<xsl:if test=\"$n > 0\"><x><xsl:call-template name=\"d\">"
|
||||
+ "<xsl:with-param name=\"n\" select=\"$n - 1\"/></xsl:call-template></x></xsl:if></xsl:template>"
|
||||
+ "</xsl:stylesheet>"
|
||||
}
|
||||
|
||||
private final class Box<T>: @unchecked Sendable { var value: T? }
|
||||
@Test("deep recursion succeeds on a small stack instead of overflowing")
|
||||
func test_deepRecursionSucceeds() throws {
|
||||
// 5000 levels far exceeds what the recursive engine survived on a Task
|
||||
// stack (a few hundred); the iterative engine builds it on the heap.
|
||||
let out = try PureXML.XSLT.transform(stylesheet: recursiveStylesheet(5000), source: "<r/>")
|
||||
#expect(out.components(separatedBy: "<x>").count - 1 == 4999)
|
||||
}
|
||||
|
||||
/// A stylesheet whose named template recurses `count` times, driving the
|
||||
/// recursion from the passed parameter (a stand-in for `count(//x)`).
|
||||
private func recursiveStylesheet(_ count: Int) -> String {
|
||||
"<xsl:stylesheet version=\"1.0\" \(xsl)>"
|
||||
+ "<xsl:template match=\"/\"><xsl:call-template name=\"d\">"
|
||||
+ "<xsl:with-param name=\"n\" select=\"\(count)\"/></xsl:call-template></xsl:template>"
|
||||
+ "<xsl:template name=\"d\"><xsl:param name=\"n\"/>"
|
||||
+ "<xsl:if test=\"$n > 0\"><x><xsl:call-template name=\"d\">"
|
||||
+ "<xsl:with-param name=\"n\" select=\"$n - 1\"/></xsl:call-template></x></xsl:if></xsl:template>"
|
||||
+ "</xsl:stylesheet>"
|
||||
}
|
||||
|
||||
@Test("recursion past the default limit throws rather than crashing")
|
||||
func test_deepRecursionFailsGracefully() {
|
||||
let error: (any Error)? = onLargeStack {
|
||||
do {
|
||||
_ = try PureXML.XSLT.transform(stylesheet: recursiveStylesheet(5000), source: "<r/>")
|
||||
return nil
|
||||
} catch {
|
||||
return error
|
||||
}
|
||||
}
|
||||
#expect(error as? PureXML.XSLT.XSLTError == .recursionLimitExceeded(PureXML.XSLT.defaultMaxTemplateDepth))
|
||||
}
|
||||
|
||||
@Test("recursion within the limit transforms normally")
|
||||
func test_shallowRecursionSucceeds() {
|
||||
let out = onLargeStack { (try? PureXML.XSLT.transform(stylesheet: recursiveStylesheet(100), source: "<r/>")) ?? "" }
|
||||
#expect(out.contains("<x>"))
|
||||
}
|
||||
|
||||
@Test("a raised maxTemplateDepth allows deeper legitimate recursion")
|
||||
func test_configurableLimit() {
|
||||
let out = onLargeStack {
|
||||
(try? PureXML.XSLT.transform(stylesheet: recursiveStylesheet(1000), source: "<r/>", maxTemplateDepth: 4000)) ?? ""
|
||||
}
|
||||
// Recursion went far past the default limit of 300 (it would otherwise have
|
||||
// thrown), producing ~1000 nested <x> elements.
|
||||
#expect(out.components(separatedBy: "<x>").count - 1 > 900)
|
||||
}
|
||||
|
||||
@Test("an identity transform of the deepest permitted source still runs")
|
||||
func test_identityOfMaxDepthSource() {
|
||||
let depth = 254 // the parser permits up to maxDepth (256) nesting.
|
||||
let src = String(repeating: "<a>", count: depth) + "leaf" + String(repeating: "</a>", count: depth)
|
||||
let sheet = "<xsl:stylesheet version=\"1.0\" \(xsl)>"
|
||||
+ "<xsl:template match=\"@*|node()\"><xsl:copy><xsl:apply-templates select=\"@*|node()\"/></xsl:copy></xsl:template>"
|
||||
+ "</xsl:stylesheet>"
|
||||
let out = onLargeStack { (try? PureXML.XSLT.transform(stylesheet: sheet, source: src)) ?? "" }
|
||||
#expect(out.contains("leaf"))
|
||||
#expect(out.contains(String(repeating: "<a>", count: depth)))
|
||||
@Test("a runaway recursion fails gracefully at the configured limit")
|
||||
func test_runawayFailsGracefully() {
|
||||
// A low limit stands in for an unbounded recursion: it trips the guard
|
||||
// cheaply and throws rather than building without bound.
|
||||
#expect(throws: PureXML.XSLT.XSLTError.recursionLimitExceeded(100)) {
|
||||
_ = try PureXML.XSLT.transform(stylesheet: recursiveStylesheet(5000), source: "<r/>", maxTemplateDepth: 100)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@Test("recursion within the limit transforms normally")
|
||||
func test_withinLimitSucceeds() throws {
|
||||
let out = try PureXML.XSLT.transform(stylesheet: recursiveStylesheet(100), source: "<r/>")
|
||||
#expect(out.components(separatedBy: "<x>").count - 1 == 99)
|
||||
}
|
||||
|
||||
@Test("an identity transform of the deepest permitted source runs on a small stack")
|
||||
func test_identityOfMaxDepthSource() throws {
|
||||
let depth = 254 // the parser permits up to maxDepth (256) nesting.
|
||||
let src = String(repeating: "<a>", count: depth) + "leaf" + String(repeating: "</a>", count: depth)
|
||||
let sheet = "<xsl:stylesheet version=\"1.0\" \(xsl)>"
|
||||
+ "<xsl:template match=\"@*|node()\"><xsl:copy><xsl:apply-templates select=\"@*|node()\"/></xsl:copy></xsl:template>"
|
||||
+ "</xsl:stylesheet>"
|
||||
let out = try PureXML.XSLT.transform(stylesheet: sheet, source: src)
|
||||
#expect(out.contains("leaf"))
|
||||
#expect(out.contains(String(repeating: "<a>", count: depth)))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user