Opt-in located parse (parseLocated / parseStreamLocated) on the hand-rolled engine, with exact scalar end positions. Resolved against main's extracted parseScalarNode by threading the token end mark through it.
306 lines
13 KiB
Swift
306 lines
13 KiB
Swift
extension PureYAML.Parsing {
|
|
struct TokenEventParser {
|
|
var cursor: TokenCursor
|
|
let scalarParser: Parser
|
|
var events: [Event]?
|
|
var eventSink: ((Event) throws -> Void)?
|
|
|
|
/// Current node recursion depth, tracked so deeply nested input fails
|
|
/// with a diagnostic instead of overflowing the stack.
|
|
var depth = 0
|
|
|
|
/// Maximum node nesting the parser will follow before failing with
|
|
/// ``ParseError/nestingTooDeep(line:column:)``.
|
|
///
|
|
/// This bound is the single safety invariant for deeply nested input,
|
|
/// and it is *necessary*, not merely convenient. A ``Model/Value`` is a
|
|
/// recursive tree, so composing it, emitting it, comparing it, hashing
|
|
/// it, and -- crucially -- deallocating it all recurse with the nesting
|
|
/// depth. Swift tears down a recursive value type recursively, and that
|
|
/// teardown cannot be made iterative cheaply, so the only way to keep
|
|
/// every operation within the call stack is to bound the depth of the
|
|
/// tree where it is built: here, while parsing.
|
|
///
|
|
/// Because every parsing entry point routes through ``parseNode``, this
|
|
/// one bound caps the depth of any parsed tree, which transitively keeps
|
|
/// the parser, the event composers, the emitter, equality, hashing, and
|
|
/// deallocation all well inside their limits. Measured on the main
|
|
/// thread's 8 MB stack: the parser overflows near depth 225 and the
|
|
/// tree operations near depth 50,000, while real-world specs nest below
|
|
/// ~40. So 64 accepts every realistic document yet rejects pathological
|
|
/// input -- for example a 100 KB `[[[...]]]` -- cleanly and early.
|
|
///
|
|
/// The bound cannot simply be raised: above ~225 the recursive parser
|
|
/// itself overflows before the guard fires (lifting it would require an
|
|
/// iterative parser), and a tree past ~5,000 deep crashes on `deinit`
|
|
/// regardless, so a higher limit would trade a parse-time failure for an
|
|
/// un-fixable teardown crash. 64 is the largest value that is both safe
|
|
/// here and sufficient for real input.
|
|
///
|
|
/// Caveat: a tree built *without* the parser -- for example by encoding
|
|
/// a pathologically deep `Codable` value -- bypasses this bound, and
|
|
/// operations on such a tree are O(depth) in stack like any recursive
|
|
/// value type. Keeping those shallow is the caller's responsibility, the
|
|
/// same way the standard library cannot prevent unbounded recursion in
|
|
/// user code.
|
|
static let maxNestingDepth = 64
|
|
|
|
init(
|
|
tokens: [Token],
|
|
scalarParser: Parser,
|
|
) {
|
|
cursor = TokenCursor(tokens: tokens)
|
|
self.scalarParser = scalarParser
|
|
events = []
|
|
eventSink = nil
|
|
}
|
|
|
|
mutating func parse() throws -> [Event] {
|
|
try parseEvents()
|
|
return events ?? []
|
|
}
|
|
|
|
init(
|
|
tokens: [Token],
|
|
scalarParser: Parser,
|
|
eventSink: @escaping (Event) throws -> Void,
|
|
) {
|
|
cursor = TokenCursor(tokens: tokens)
|
|
self.scalarParser = scalarParser
|
|
events = nil
|
|
self.eventSink = eventSink
|
|
}
|
|
|
|
init(
|
|
yaml: String,
|
|
scalarParser: Parser,
|
|
eventSink: @escaping (Event) throws -> Void,
|
|
) {
|
|
cursor = TokenCursor(yaml: yaml)
|
|
self.scalarParser = scalarParser
|
|
events = nil
|
|
self.eventSink = eventSink
|
|
}
|
|
|
|
mutating func parseStreaming() throws {
|
|
try parseEvents()
|
|
}
|
|
|
|
mutating func parseEvents() throws {
|
|
let streamStart = try expect("stream start") { kind in
|
|
if case .streamStart = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
try emit(.streamStart(mark: streamStart.mark))
|
|
// A stream whose only content is comments or whitespace has zero documents
|
|
// (YAML 1.2, matching libyaml and the test suite), so no document events are
|
|
// emitted. The value-level API maps an empty document stream to a single null
|
|
// value separately.
|
|
while cursor.current?.kind.isStreamEnd != true {
|
|
try parseDocument()
|
|
}
|
|
let streamEnd = try expect("stream end") { kind in
|
|
if case .streamEnd = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
try emit(.streamEnd(mark: streamEnd.mark))
|
|
}
|
|
|
|
mutating func emit(_ event: Event) throws {
|
|
if let eventSink {
|
|
try eventSink(event)
|
|
} else {
|
|
events?.append(event)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureYAML.Parsing.TokenEventParser {
|
|
mutating func parseDocument() throws {
|
|
let startMark = try parseDocumentStartMark()
|
|
try emit(.documentStart(mark: startMark))
|
|
|
|
if cursor.current?.kind.isDocumentEnd == true {
|
|
let end = try expect("document end") { $0.isDocumentEnd }
|
|
try appendEmptyDocument(at: end.mark)
|
|
try emit(.documentEnd(mark: end.endMark))
|
|
return
|
|
}
|
|
|
|
if cursor.current?.kind.isDocumentStart == true || cursor.current?.kind.isStreamEnd == true {
|
|
try appendEmptyDocument(at: cursor.current?.mark ?? startMark)
|
|
try emit(.documentEnd(mark: cursor.current?.mark ?? startMark))
|
|
return
|
|
}
|
|
|
|
var endMark = try parseDocumentRootNode().endMark
|
|
while cursor.current?.kind.isDedent == true {
|
|
let dedent = try expect("dedent") { $0.isDedent }
|
|
endMark = dedent.endMark
|
|
}
|
|
if cursor.current?.kind.isDocumentEnd == true {
|
|
// A `...` ends this document. Content after it begins a new bare document, parsed by the
|
|
// outer stream loop, so emit the end and return rather than expecting a boundary here.
|
|
let end = try expect("document end") { $0.isDocumentEnd }
|
|
try emit(.documentEnd(mark: end.endMark))
|
|
return
|
|
}
|
|
if let current = cursor.current, !canEndDocument(at: current) {
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: "document boundary or stream end",
|
|
actual: current.kind.description,
|
|
line: current.mark.line,
|
|
column: current.mark.column,
|
|
)
|
|
}
|
|
try emit(.documentEnd(mark: endMark))
|
|
}
|
|
|
|
mutating func parseDocumentRootNode() throws -> PureYAML.Parsing.NodeResult {
|
|
guard cursor.current?.kind.isIndent == true else {
|
|
return try parseNode()
|
|
}
|
|
_ = try expect("indent") { $0.isIndent }
|
|
let result = try parseNode()
|
|
if cursor.current?.kind.isDedent == true {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
return result
|
|
}
|
|
|
|
mutating func parseDocumentStartMark() throws -> PureYAML.Parsing.Mark {
|
|
guard cursor.current?.kind.isDocumentStart == true else {
|
|
return cursor.current?.mark ?? cursor.previous?.endMark ?? .start
|
|
}
|
|
let start = try cursor.advance()
|
|
return start?.mark ?? .start
|
|
}
|
|
|
|
mutating func appendEmptyDocument(at mark: PureYAML.Parsing.Mark) throws {
|
|
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark, endMark: mark))
|
|
}
|
|
|
|
func canEndDocument(at token: PureYAML.Parsing.Token) -> Bool {
|
|
token.kind.isDocumentStart || token.kind.isStreamEnd
|
|
}
|
|
|
|
mutating func parseNode() throws -> PureYAML.Parsing.NodeResult {
|
|
let properties = try consumeProperties()
|
|
return try parseNode(properties: properties)
|
|
}
|
|
|
|
/// Reject documents nested past ``maxNestingDepth`` with a diagnostic so a
|
|
/// pathologically nested input fails cleanly instead of overflowing the
|
|
/// recursive-descent stack.
|
|
func enforceNestingLimit(properties: PureYAML.Parsing.NodeProperties) throws {
|
|
guard depth > Self.maxNestingDepth else {
|
|
return
|
|
}
|
|
let mark = properties.mark ?? cursor.current?.mark ?? cursor.previous?.endMark ?? .start
|
|
throw PureYAML.Parsing.ParseError.nestingTooDeep(line: mark.line, column: mark.column)
|
|
}
|
|
|
|
/// Handles a node position with no node token. An anchor or tag with nothing after it
|
|
/// (`- !!str`, `&a` alone) applies to an empty node, so emit a null scalar carrying the
|
|
/// properties; with no properties, a missing node is an error.
|
|
mutating func emitEmptyNodeOrThrow(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let mark = properties.mark ?? cursor.current?.mark ?? cursor.previous?.endMark ?? .start
|
|
guard properties.anchor != nil || properties.tag != nil else {
|
|
throw PureYAML.Parsing.ParseError.expectedNode(line: mark.line, column: mark.column)
|
|
}
|
|
try emit(.scalar(value: "", anchor: properties.anchor, tag: properties.tag, style: .plain, mark: mark, endMark: mark))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
|
}
|
|
|
|
/// An alias is a bare reference, so it cannot carry an anchor or tag of its own (`&b *a`).
|
|
mutating func parseAlias(
|
|
anchor: String,
|
|
token: PureYAML.Parsing.Token,
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
guard properties.anchor == nil, properties.tag == nil else {
|
|
let mark = properties.mark ?? token.mark
|
|
throw PureYAML.Parsing.ParseError.aliasWithProperties(line: mark.line, column: mark.column)
|
|
}
|
|
_ = try cursor.advance()
|
|
try emit(.alias(anchor: anchor, mark: token.mark))
|
|
return PureYAML.Parsing.NodeResult(kind: .alias, endMark: token.endMark)
|
|
}
|
|
|
|
mutating func parseNode(
|
|
properties initialProperties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
depth += 1
|
|
defer { depth -= 1 }
|
|
try enforceNestingLimit(properties: initialProperties)
|
|
var properties = initialProperties
|
|
try properties.mergeUnset(from: consumeProperties())
|
|
|
|
guard let token = cursor.current, !token.kind.isTerminator else {
|
|
return try emitEmptyNodeOrThrow(properties: properties)
|
|
}
|
|
|
|
switch token.kind {
|
|
case let .alias(anchor):
|
|
return try parseAlias(anchor: anchor, token: token, properties: properties)
|
|
case .blockEntry:
|
|
return try parseBlockSequence(properties: properties)
|
|
case .blockScalarHeader:
|
|
return try parseBlockScalar(properties: properties)
|
|
case .flowMappingStart:
|
|
return try parseFlowMapping(properties: properties)
|
|
case .flowSequenceStart:
|
|
return try parseFlowSequence(properties: properties)
|
|
case .mappingKey:
|
|
return try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: false,
|
|
)
|
|
case let .scalar(value, style):
|
|
return try parseScalarNode(value: value, style: style, token: token, properties: properties)
|
|
case .mappingValue:
|
|
// An explicit key that is only properties before its `:` (`? &e` then `: v`) is an empty
|
|
// key carrying those properties. Emit the empty scalar without consuming the `:`, so the
|
|
// mapping machinery reads it as the value indicator. With no properties this is a misplaced
|
|
// `:`, which `emitEmptyNodeOrThrow` keeps as an error. (The implicit form `&e : v` never
|
|
// reaches here: the scanner synthesizes its empty key as an explicit scalar token.)
|
|
return try emitEmptyNodeOrThrow(properties: properties)
|
|
default:
|
|
throw PureYAML.Parsing.ParseError.expectedNode(line: token.mark.line, column: token.mark.column)
|
|
}
|
|
}
|
|
|
|
/// Emits a scalar node from a scalar token, applying any node properties. A plain scalar that the
|
|
/// scanner split around a comment (the comment-interrupted shape) is rejected rather than emitted.
|
|
mutating func parseScalarNode(
|
|
value: String,
|
|
style: PureYAML.Parsing.ScalarStyle,
|
|
token: PureYAML.Parsing.Token,
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
if try isCommentInterruptedScalar() {
|
|
return try rejectCommentInterruptedScalar()
|
|
}
|
|
_ = try cursor.advance()
|
|
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
|
|
let mark = properties.mark ?? token.mark
|
|
try emit(.scalar(
|
|
value: decoded,
|
|
anchor: properties.anchor,
|
|
tag: properties.tag,
|
|
style: style,
|
|
mark: mark,
|
|
endMark: token.endMark,
|
|
))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: token.endMark)
|
|
}
|
|
}
|