`PureXML.HTML.serialize` walked an element's children recursively, so a deeply-nested tree overflowed the stack (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.emit`: 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, so nested elements still serialize by their own name. The output is byte-identical. Void-element, raw-text, comment, and escaping semantics are unchanged (a new micro-test pins them) and the HTML conformance corpus passes. A 50000-deep tree now serializes without crashing, guarded by `HTMLSerializeDepthSafetyTests`. Part of #350.
43 lines
2.0 KiB
Swift
43 lines
2.0 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// HTML serialization must not recurse on a tree's depth (#350). The serializer
|
|
/// walked an element's children recursively, so it overflowed the stack on a
|
|
/// deeply-nested tree (as shallow as a couple of thousand deep on the Swift Task
|
|
/// stack these tests run on). It now drives a deferred-close work stack; these
|
|
/// tests reach depths the recursive form could not survive, and pin the
|
|
/// raw-text, void, and escaping semantics the refactor must preserve.
|
|
@Suite("HTML serialize depth safety")
|
|
struct HTMLSerializeDepthSafetyTests {
|
|
private func deepTree(_ depth: Int, leaf: String) -> PureXML.Model.Node {
|
|
var node: PureXML.Model.Node = .text(leaf)
|
|
for _ in 0 ..< depth {
|
|
node = .element(.init("div", children: [node]))
|
|
}
|
|
return node
|
|
}
|
|
|
|
@Test("serialize on a 50k-deep tree emits balanced tags without overflowing")
|
|
func test_deepSerializeIsBounded() {
|
|
let depth = 50000
|
|
let html = PureXML.HTML.serialize(deepTree(depth, leaf: "leaf"))
|
|
#expect(html.hasPrefix(String(repeating: "<div>", count: depth) + "leaf"))
|
|
#expect(html.hasSuffix(String(repeating: "</div>", count: depth)))
|
|
}
|
|
|
|
@Test("raw-text, void, comment, and escaping semantics are unchanged")
|
|
func test_semanticsPreserved() {
|
|
// A raw-text element writes its own text verbatim; a nested element still
|
|
// serializes by its own name; text outside is escaped; a void element has
|
|
// no end tag and drops nested content.
|
|
let tree: PureXML.Model.Node = .element(.init("body", children: [
|
|
.element(.init("script", children: [.text("if (a < b) x()")])),
|
|
.text("a < b & c"),
|
|
.element(.init("br", children: [.text("dropped")])),
|
|
.comment(" note "),
|
|
]))
|
|
let html = PureXML.HTML.serialize(tree)
|
|
#expect(html == "<body><script>if (a < b) x()</script>a < b & c<br><!-- note --></body>")
|
|
}
|
|
}
|