PureXML/Sources/Parsing/Parser.swift
Mihaela Mihaljevic c75c97aa9b feat(dtd): external and parameter entities, secure by default
Add internal parameter entities (parsed, stored, and expanded within the
DTD, including bare %name; references that inject markup declarations,
bounded by depth and the expansion budget). Record external general
entities and the external subset identifier, and load them only through an
injected EntityResolver (a struct of closures). The default refusing
resolver fetches nothing, so XXE stays closed: an external entity fails as
undefined and the external subset is left unread. Internal declarations win
over the external subset.

Closes #29
2026-06-08 03:08:20 +02:00

154 lines
6.5 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. Supply
/// a ``EntityResolver`` to opt into external entities; the default refuses
/// them, keeping XXE closed.
public func parse(
_ xml: String,
limits: Limits = .default,
resolver: EntityResolver = .refusing,
) throws -> PureXML.Model.Node {
try build(EventReader(xml, limits: limits, resolver: resolver)).node
}
/// Parses a string and surfaces the parsed DTD (entities, element content
/// models, parameter entities, external identifiers) alongside the tree,
/// for schema validation.
public func parseWithDocumentType(
_ xml: String,
limits: Limits = .default,
resolver: EntityResolver = .refusing,
) throws -> (node: PureXML.Model.Node, documentType: DocumentType) {
try build(EventReader(xml, limits: limits, resolver: resolver))
}
/// Parses a single XML document from raw bytes, detecting the encoding
/// (UTF-8 or UTF-16, with or without a byte-order mark) before parsing.
public func parse(bytes: [UInt8], limits: Limits = .default) throws -> PureXML.Model.Node {
try parse(ByteDecoder.decode(bytes), 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)).node
}
/// Parses a single XML document from an incremental BYTE source, decoding
/// the encoding (UTF-8 or UTF-16) on the fly so the bytes are never fully
/// buffered. The closure returns the next byte or nil at end of input.
public func parse(
pullingBytes pull: @escaping () -> UInt8?,
limits: Limits = .default,
) throws -> PureXML.Model.Node {
var decoder = StreamingDecoder(pullingBytes: pull)
return try build(EventReader(pulling: { decoder.next() }, limits: limits)).node
}
/// Parses a document, delivering SAX-style callbacks instead of building a
/// tree. The handler's callbacks fire as the parse streams.
public func parse(
_ xml: String,
sax handler: SAXHandler,
limits: Limits = .default,
resolver: EntityResolver = .refusing,
) throws {
var reader = EventReader(xml, limits: limits, resolver: resolver)
handler.startDocument?()
var produced = false
while let event = try reader.next() {
produced = true
deliver(event, to: handler)
}
guard produced else { throw ParseError.emptyDocument }
handler.endDocument?()
}
private func deliver(_ event: Event, to handler: SAXHandler) {
switch event {
case let .startElement(name, attributes):
handler.startElement?(name, attributes)
case let .endElement(name):
handler.endElement?(name)
case let .characters(text):
handler.characters?(text)
case let .cdata(text):
handler.cdata?(text)
case let .comment(text):
handler.comment?(text)
case let .processingInstruction(target, data):
handler.processingInstruction?(target, data)
}
}
private func build(_ source: EventReader) throws -> (node: PureXML.Model.Node, documentType: DocumentType) {
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), reader.documentType)
}
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)
}
}
}
}