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 { private var reader: Reader private let limits: Limits private var open: [PureXML.Model.QualifiedName] = [] private var pending: [Event] = [] private var sawRoot = false private var primed = false /// 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) { reader = Reader(pulling: pull) self.limits = limits } /// Creates a reader over a string (a convenience over the streaming init). init(_ string: String, limits: Limits = .default) { reader = Reader(string) self.limits = limits } /// Returns the next event, or nil at the end of the document. mutating func next() throws -> Event? { if !primed { primed = true if reader.peek() == "\u{FEFF}" { reader.advance() } } if !pending.isEmpty { return pending.removeFirst() } return open.isEmpty ? try nextAtTopLevel() : try nextInContent() } private mutating func nextAtTopLevel() throws -> Event? { while true { reader.skipSpace() guard let lead = reader.peek() else { return nil } if reader.matches("") { guard let character = reader.advance() else { throw ParseError.unterminatedComment(mark) } length += 1 try checkContent(length, mark) content.append(character) } reader.consume("-->") return .comment(content) } private mutating func scanCDATA() throws -> Event { let mark = reader.mark reader.consume("") { guard let character = reader.advance() else { throw ParseError.unterminatedCDATA(mark) } length += 1 try checkContent(length, mark) content.append(character) } reader.consume("]]>") return .cdata(content) } private mutating func scanProcessingInstruction() throws -> (target: String, data: String) { let mark = reader.mark reader.consume("") { guard let character = reader.advance() else { throw ParseError.unterminatedTag(mark) } length += 1 try checkContent(length, mark) data.append(character) } reader.consume("?>") return (target.description, data) } private mutating func scanAttributes() throws -> [PureXML.Model.Attribute] { var attributes: [PureXML.Model.Attribute] = [] var seen: Set = [] while true { reader.skipSpace() guard let next = reader.peek(), next != ">", next != "/" else { return attributes } let mark = reader.mark let name = try scanName() reader.skipSpace() guard reader.consume("=") else { throw ParseError.expectedEquals(reader.mark) } reader.skipSpace() let value = try scanAttributeValue() guard seen.insert(name.description).inserted else { throw ParseError.duplicateAttribute(name: name.description, mark) } attributes.append(PureXML.Model.Attribute(name: name, value: value)) } } private mutating func scanAttributeValue() throws -> String { let mark = reader.mark guard let quote = reader.peek(), quote == "\"" || quote == "'" else { throw ParseError.unquotedAttributeValue(reader.mark) } reader.advance() var raw = "" var length = 0 while let character = reader.peek(), character != quote { length += 1 try checkContent(length, mark) raw.append(character) reader.advance() } guard reader.consume(String(quote)) else { throw ParseError.unexpectedEndOfInput(reader.mark) } return try EntityDecoder.decode(raw, at: mark) } private mutating func scanName() throws -> PureXML.Model.QualifiedName { let mark = reader.mark guard let first = reader.peek(), first.isXMLNameStart else { throw ParseError.expectedName(reader.mark) } var raw = "" var length = 0 while let character = reader.peek(), character.isXMLNameContinuation { length += 1 guard length <= limits.maxNameLength else { throw ParseError.nameTooLong(limit: limits.maxNameLength, mark) } raw.append(character) reader.advance() } return PureXML.Model.QualifiedName(raw) } private func checkContent(_ length: Int, _ mark: Mark) throws { guard length <= limits.maxContentLength else { throw ParseError.contentTooLong(limit: limits.maxContentLength, mark) } } } }