PureYAML/Sources/Parsing/TokenEventParser.Flow.swift
Mihaela M 97584dc57c fix(parsing): reject empty entries in flow collections
In a flow sequence or mapping a comma separates entries, so one must
precede it. The parser advanced past a comma without checking that an
entry came before, accepting a leading comma ([ , a ]) or a repeated comma
([ a, , b ]) as empty entries. Each flow loop now tracks whether an entry
precedes the comma and rejects it otherwise. A single trailing comma
([ a, b, ]) and empty collections ([], {}) remain valid, since the closing
bracket is handled before that check.

Measured against the official YAML test-suite, parse-level conformance
rises from 249/333 (74.8%) to 251/333 (75.4%) with no regression.
2026-06-27 16:26:46 +02:00

123 lines
4.8 KiB
Swift

extension PureYAML.Parsing.TokenEventParser {
mutating func parseFlowSequence(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow sequence start") { kind in
if case .flowSequenceStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.sequenceStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowSequenceEnd = cursor.current?.kind {
let end = try expect("flow sequence end") { kind in
if case .flowSequenceEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.sequenceEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// A comma separates entries, so one must precede it: a leading or repeated comma
// is an empty entry, which is not allowed. A single trailing comma before the
// closing bracket is fine, since the close is handled above regardless of `hasEntry`.
guard hasEntry else {
throw unexpectedToken(expected: "flow sequence entry or flow sequence end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseNode().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
}
}
throw unexpectedToken(expected: "flow sequence end")
}
mutating func parseFlowMapping(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow mapping start") { kind in
if case .flowMappingStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.mappingStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowMappingEnd = cursor.current?.kind {
let end = try expect("flow mapping end") { kind in
if case .flowMappingEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// As in a flow sequence, a comma separates entries and so requires one before it;
// a leading or repeated comma is an empty entry. A trailing comma is fine.
guard hasEntry else {
throw unexpectedToken(expected: "flow mapping entry or flow mapping end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseFlowMappingPair().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow mapping end")
}
}
throw unexpectedToken(expected: "flow mapping end")
}
mutating func parseFlowMappingPair() throws -> PureYAML.Parsing.NodeResult {
if cursor.current?.kind.isMappingKey == true {
return try parseMappingPair()
}
_ = try parseNode()
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
guard properties != .none || cursor.current?.kind.isFlowTerminator == false else {
let mark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
return try parseNode(properties: properties)
}
}