Tighten error detection per the YAML 1.2 spec across four rules: (1) a tab may never supply block-structure indentation (line-start indentation, block-scalar indentation, and separation before a nested block node are rejected; a tab separating an indicator from a scalar value stays valid); (2) a ---/... document marker inside an open quoted scalar or flow collection is rejected instead of silently ending the document; (3) a %YAML/%TAG line that opens a second explicit document with no intervening ... is rejected; (4) an anchor cannot be attached to an alias node. This repairs 4 of the 5 error-detection regressions introduced by buckets 3-5 (MUS6/01, N782, SU74, Y79Y/009); 9MQT/01 is deferred (its invalidity is in same-line content after the --- marker, which needs document-content scanning that risks the baseline). Rejects 13 accepted-invalid suite cases and fixes 2 more by value (6BCT, Y79Y/010); 305/402 passing. BEHAVIOR CHANGE: tab-indented block structure (previously parsed permissively) is now rejected per spec; no real-world corpus seed relied on this. Updates the manifest, fixtures, counts, two affected tests (with spec justification), and the roadmap (bucket 6 marked PARTIAL).
322 lines
13 KiB
Swift
322 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))
|
|
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`.
|
|
if properties.anchor != nil || properties.tag != nil {
|
|
let mark = properties.mark ?? token.mark
|
|
throw PureYAML.Parsing.ParseError.expectedNode(line: mark.line, column: mark.column)
|
|
}
|
|
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)
|
|
}
|
|
}
|