The gap-only manifest stopped guarding a case once it was fixed, so later work silently regressed previously-fixed cases (caught only by an external full-suite run, not the gated sweep or full corpus). Replace it with a full manifest of all 402 official cases (Tests/Fixtures/yaml-test-suite-manifest.yaml), each carrying its conformant expectation and a passing/gap status, with every case's files vendored. The gated sweep now asserts every passing case still passes (the regression guard) and every gap still reproduces, so any regression fails the build. The honest gate-enforced count is 329/402 passing (the earlier 348 came from a more lenient one-time oracle). Repairs three regressions the new gate revealed: the bucket-6 tab rule over-rejected tabs in block-scalar content and as separation before a scalar value (root cause: the batch scanner and the incremental TokenCursor had divergent line-start loops; unified onto a single scanStep, fixing 96NN/00, 96NN/01, R4YG, Y79Y/001, DK95/00, DK95/03, DK95/07, 6CA3, Q5MG, J3BT); an anchor on a prior line before an alias key wrongly rejected 26DV; and a tag-only flow key mis-parsed WZ62. Genuine tab-indentation errors (4EJS, Y79Y/000) still reject.
329 lines
14 KiB
Swift
329 lines
14 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))
|
|
var didParseDocument = false
|
|
while cursor.current?.kind.isStreamEnd != true {
|
|
try parseDocument()
|
|
didParseDocument = true
|
|
}
|
|
if !didParseDocument {
|
|
let mark = cursor.current?.mark ?? streamStart.endMark
|
|
try emit(.documentStart(mark: mark))
|
|
try appendEmptyDocument(at: mark)
|
|
try emit(.documentEnd(mark: mark))
|
|
}
|
|
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 {
|
|
// An explicit `...` end marker closes the document outright. Whatever
|
|
// follows -- a bare node, an explicit `---`, or the end of the stream
|
|
// -- starts a fresh document on the next loop iteration, so emit the
|
|
// end here and return rather than demanding a document boundary token.
|
|
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))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 emptyScalarOrThrow(properties: properties)
|
|
}
|
|
|
|
switch token.kind {
|
|
case let .alias(anchor):
|
|
return try parseAliasNode(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, .mappingValue:
|
|
return try parseBlockMappingNode(token: token, properties: properties)
|
|
case .indent:
|
|
return try parseIndentedNode(properties: properties)
|
|
case let .scalar(value, style):
|
|
return try parseScalarNode(token: token, value: value, style: style, properties: properties)
|
|
default:
|
|
throw PureYAML.Parsing.ParseError.expectedNode(line: token.mark.line, column: token.mark.column)
|
|
}
|
|
}
|
|
|
|
/// Parses an alias node. An alias immediately followed by a `:` value
|
|
/// indicator is the key of a block mapping (`*alias : value`), not a
|
|
/// standalone alias, so route into the mapping and let the key parser
|
|
/// consume it; otherwise emit the alias event directly.
|
|
mutating func parseAliasNode(
|
|
anchor: String,
|
|
token: PureYAML.Parsing.Token,
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
// An alias is a complete reference to an already-anchored node and cannot
|
|
// itself carry an anchor or tag. libyaml handles ALIAS as the first branch
|
|
// of yaml_parser_parse_node (parser.c:563-570), so the property branches
|
|
// are unreachable for an alias; mirror that by rejecting `&b *alias`. The
|
|
// rejection applies only when the property sits on the *same line* as the
|
|
// alias: a property scanned on the previous line (e.g. `key: &anchor` then an
|
|
// indented `*alias : value`) anchors the enclosing mapping, not the alias key.
|
|
let hasProperties = properties.anchor != nil || properties.tag != nil
|
|
if hasProperties, properties.mark?.line == token.mark.line {
|
|
let mark = properties.mark ?? token.mark
|
|
throw PureYAML.Parsing.ParseError.expectedNode(line: mark.line, column: mark.column)
|
|
}
|
|
// An alias immediately followed by a `:` is the first key of a block mapping;
|
|
// any property that reached here belongs to the mapping (it was on a prior
|
|
// line, per the check above), so route into the mapping carrying it.
|
|
if try cursor.peek()?.kind.isMappingValue == true {
|
|
return try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: false,
|
|
)
|
|
}
|
|
_ = try cursor.advance()
|
|
let mark = properties.mark ?? token.mark
|
|
try emit(.alias(anchor: anchor, mark: mark))
|
|
return PureYAML.Parsing.NodeResult(kind: .alias, endMark: token.endMark)
|
|
}
|
|
|
|
/// Parses a block mapping introduced by a `mappingKey`/`mappingValue`
|
|
/// indicator. Properties on the same line as the first key indicator belong
|
|
/// to that key node, not to the mapping: `&a a: b` anchors the key `a`,
|
|
/// whereas `&a` alone on a line before the mapping anchors the map.
|
|
mutating func parseBlockMappingNode(
|
|
token: PureYAML.Parsing.Token,
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
if properties != .none, let mark = properties.mark, mark.line == token.mark.line, token.kind.isMappingKey {
|
|
return try parseBlockMapping(
|
|
properties: .none,
|
|
includeIndentedContinuation: false,
|
|
firstKeyProperties: properties,
|
|
)
|
|
}
|
|
return try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: false,
|
|
)
|
|
}
|
|
|
|
mutating func parseScalarNode(
|
|
token: PureYAML.Parsing.Token,
|
|
value: String,
|
|
style: PureYAML.Parsing.ScalarStyle,
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
if try canParsePlainScalarContinuation() {
|
|
return try parsePlainScalarWithContinuation(properties: properties)
|
|
}
|
|
_ = 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,
|
|
))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: token.endMark)
|
|
}
|
|
}
|