Add PureXML.Parsing.Limits: configurable caps on nesting depth (default 256, matching libxml2), name length, and content length, enforced during scanning in the event reader. This protects against pathological input (deeply nested or oversized documents) and keeps the recursive Model.Node value safe to build and tear down (extreme depth otherwise overflows on ARC release). Threaded through Parser.parse and the top-level parse/events API. Reimplemented from libxml2's bounded-by-default posture; no upstream code copied.
84 lines
3.4 KiB
Swift
84 lines
3.4 KiB
Swift
/// A partially-built element while its children stream in. File-scope and
|
|
/// private: an internal detail of the tree builder, not part of the namespace.
|
|
private struct ElementFrame {
|
|
let name: PureXML.Model.QualifiedName
|
|
let attributes: [PureXML.Model.Attribute]
|
|
var children: [PureXML.Model.Node] = []
|
|
}
|
|
|
|
public extension PureXML.Parsing {
|
|
/// Builds a ``PureXML/Model/Node`` tree by draining the streaming
|
|
/// ``EventReader`` with an explicit element stack. It is iterative, not
|
|
/// recursive, and the streaming core means it never requires the whole input
|
|
/// in memory at once: characters are pulled on demand. Callers that do not
|
|
/// need a full tree should consume ``EventReader`` directly.
|
|
struct Parser: Sendable {
|
|
public init() {}
|
|
|
|
/// Parses a single XML document from a string into a document node.
|
|
public func parse(_ xml: String, limits: Limits = .default) throws -> PureXML.Model.Node {
|
|
try build(EventReader(xml, limits: limits))
|
|
}
|
|
|
|
/// Parses a single XML document from an incremental character source. The
|
|
/// closure returns the next character or nil at end of input, so the
|
|
/// document can arrive in chunks and is never held whole.
|
|
public func parse(
|
|
pulling pull: @escaping () -> Character?,
|
|
limits: Limits = .default,
|
|
) throws -> PureXML.Model.Node {
|
|
try build(EventReader(pulling: pull, limits: limits))
|
|
}
|
|
|
|
private func build(_ source: EventReader) throws -> PureXML.Model.Node {
|
|
var reader = source
|
|
var roots: [PureXML.Model.Node] = []
|
|
var stack: [ElementFrame] = []
|
|
var produced = false
|
|
|
|
while let event = try reader.next() {
|
|
produced = true
|
|
switch event {
|
|
case let .startElement(name, attributes):
|
|
stack.append(ElementFrame(name: name, attributes: attributes))
|
|
case .endElement:
|
|
guard let frame = stack.popLast() else {
|
|
throw ParseError.unexpectedEndOfInput(.start)
|
|
}
|
|
let element = PureXML.Model.Element(
|
|
name: frame.name,
|
|
attributes: frame.attributes,
|
|
children: frame.children,
|
|
)
|
|
attach(.element(element), to: &stack, roots: &roots)
|
|
case let .characters(text):
|
|
attach(.text(text), to: &stack, roots: &roots)
|
|
case let .cdata(text):
|
|
attach(.cdata(text), to: &stack, roots: &roots)
|
|
case let .comment(text):
|
|
attach(.comment(text), to: &stack, roots: &roots)
|
|
case let .processingInstruction(target, data):
|
|
attach(.processingInstruction(target: target, data: data), to: &stack, roots: &roots)
|
|
}
|
|
}
|
|
|
|
guard produced else {
|
|
throw ParseError.emptyDocument
|
|
}
|
|
return .document(roots)
|
|
}
|
|
|
|
private func attach(
|
|
_ node: PureXML.Model.Node,
|
|
to stack: inout [ElementFrame],
|
|
roots: inout [PureXML.Model.Node],
|
|
) {
|
|
if stack.isEmpty {
|
|
roots.append(node)
|
|
} else {
|
|
stack[stack.count - 1].children.append(node)
|
|
}
|
|
}
|
|
}
|
|
}
|