public extension PureXML.Parsing { /// A pull-based, streaming XML event reader. /// /// It consumes its input incrementally through a character-source closure and /// emits one ``Event`` at a time from ``next()``, holding only bounded state: /// a stack of open element names and a small lookahead buffer. It never /// requires the whole document in memory, so it can drive arbitrarily large /// or chunked input (a file, a socket, a generator). The tree-building /// ``Parser`` is a thin adapter over this core. /// /// Safe by default: a `` declaration is rejected rather than /// processed, removing the DTD-based threat classes (XXE, entity-expansion /// DoS) at the door. Bounded ``Limits`` cap depth, name, and content size. struct EventReader { var reader: Reader private let limits: Limits private let resolver: EntityResolver var open: [PureXML.Model.QualifiedName] = [] var namespaces = NamespaceContext() /// The DTD information extracted so far (entities, element models, /// attribute lists, parameter entities, and external identifiers). var documentType = DocumentType() /// The parsed XML declaration ``, when the document opens with /// one; nil otherwise. var xmlDeclaration: XMLDeclaration? var entityBudget: Int var pending: [Event] = [] private var sawRoot = false private var primed = false /// When true, ``next()`` repairs malformed input in place instead of /// throwing: it records a ``Diagnostic`` and emits best-effort events. let recovering: Bool var reachedEnd = false /// The problems found so far, when reading in recovering mode. var diagnostics: [Diagnostic] = [] /// The source position where the most recently produced event's node began, /// so a pull cursor can report each node's line and column. private(set) var eventStart: Mark = .start /// Creates a reader over a character-pulling source. The closure returns /// the next character or nil at end of input. init( pulling pull: @escaping () -> Character?, limits: Limits = .default, resolver: EntityResolver = .refusing, recovering: Bool = false, ) { reader = Reader(pulling: pull) self.limits = limits self.resolver = resolver self.recovering = recovering entityBudget = limits.maxEntityExpansion } /// Creates a reader over a string (a convenience over the streaming init). init(_ string: String, limits: Limits = .default, resolver: EntityResolver = .refusing, recovering: Bool = false) { reader = Reader(string) self.limits = limits self.resolver = resolver self.recovering = recovering entityBudget = limits.maxEntityExpansion } /// Returns the next event, or nil at the end of the document. In recovering /// mode it never throws: each malformed construct is recorded as a /// diagnostic and reading continues, popping to a matching open element on /// a mismatched end tag and stopping (so the caller closes what is open) on /// truncation. mutating func next() throws -> Event? { if !primed { primed = true if reader.peek() == "\u{FEFF}" { reader.advance() } } while true { if !pending.isEmpty { return pending.removeFirst() } guard recovering else { return open.isEmpty ? try nextAtTopLevel() : try nextInContent() } if reachedEnd { return nil } let start = offset do { return open.isEmpty ? try nextAtTopLevel() : try nextInContent() } catch let error as ParseError { if let event = recoveredEvent(from: error, startingAt: start) { return event } } } } private mutating func nextAtTopLevel() throws -> Event? { while true { reader.skipSpace() guard let lead = reader.peek() else { return nil } eventStart = reader.mark if reader.matches("