PureYAML/Sources/Parsing/ParseError.swift
Mihaela Mihaljevic f9fe2325f7 fix(parsing): reject raw control characters in parse(), matching libyaml
PureYAML.parse() silently accepted raw C0 control characters anywhere in
the input; Yams's underlying libyaml rejects them unconditionally at the
reader level. Found by comparing parse() directly against Yams on a
real-world OpenAPI corpus. The printable-character predicate this needed
already existed (CharacterClass.isPrintable, matching libyaml's
c-printable set) but was only ever wired into anchor-name validation, and
the separate PreflightScanner control-character check is opt-in via
validate(), not part of what parse() itself runs.

Reader now records the index of the first non-printable scalar as it
buffers input (covering both batch and streaming), and fetchNextToken()
throws once scanning has actually reached that position, not merely once
one exists, so an earlier real syntax error is not hidden behind a later
control character.

Verified with 5 new tests and a full re-run of the official yaml-test-suite
conformance (333/333 parse-level, 208/208 value-level, unchanged) and the
whole-suite streaming differential (still clean), confirming no regression.
2026-07-10 11:03:27 +02:00

192 lines
8.5 KiB
Swift

public extension PureYAML.Parsing {
/// Parser error with source line information when available.
enum ParseError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
case emptyDocument
case tabIndentation(line: Int)
case invalidControlCharacter(line: Int, column: Int)
case unexpectedIndentation(line: Int)
case mixedCollectionStyles(line: Int)
case expectedMappingKey(line: Int)
case expectedAnchorName(line: Int)
case expectedAliasName(line: Int)
case expectedNode(line: Int, column: Int)
case expectedScalarKey(line: Int, column: Int)
case incompatibleYAMLDirective(line: Int)
case directiveWithoutDocument(line: Int)
case invalidTaggedScalar(tag: String, value: String, line: Int, column: Int)
case invalidMergeValue(line: Int, column: Int)
case unterminatedTag(line: Int)
case unterminatedQuotedString(line: Int)
case unsupportedDirective(name: String, line: Int)
case unsupportedMultiDocumentStream(line: Int)
case undefinedAlias(anchor: String, line: Int, column: Int)
case unexpectedEvent(expected: String, actual: String, line: Int, column: Int)
case unexpectedToken(expected: String, actual: String, line: Int, column: Int)
case nestingTooDeep(line: Int, column: Int)
case malformed(reason: String, line: Int, column: Int)
public var description: String {
switch self {
case .emptyDocument:
"document is empty"
case let .tabIndentation(line):
"tabs are not allowed for indentation at line \(line)"
case let .invalidControlCharacter(line, column):
"invalid control character at line \(line), column \(column)"
case let .unexpectedIndentation(line):
"unexpected indentation at line \(line)"
case let .mixedCollectionStyles(line):
"mapping and sequence entries are mixed at line \(line)"
case let .expectedMappingKey(line):
"expected a mapping key at line \(line)"
case let .expectedAnchorName(line):
"expected an anchor name at line \(line)"
case let .expectedAliasName(line):
"expected an alias name at line \(line)"
case let .expectedNode(line, column):
"expected a YAML node at line \(line), column \(column)"
case let .expectedScalarKey(line, column):
"expected a scalar mapping key at line \(line), column \(column)"
case let .incompatibleYAMLDirective(line):
"incompatible YAML directive at line \(line)"
case let .directiveWithoutDocument(line):
"directive is not followed by a document start at line \(line)"
case let .invalidTaggedScalar(tag, value, line, column):
"invalid scalar '\(value)' for tag '\(tag)' at line \(line), column \(column)"
case let .invalidMergeValue(line, column):
"expected merge value to be a mapping or sequence of mappings at line \(line), column \(column)"
case let .unterminatedTag(line):
"unterminated tag at line \(line)"
case let .unterminatedQuotedString(line):
"unterminated quoted string at line \(line)"
case let .unsupportedDirective(name, line):
"unsupported directive '\(name)' at line \(line)"
case let .unsupportedMultiDocumentStream(line):
"multi-document streams are not supported at line \(line)"
case let .undefinedAlias(anchor, line, column):
"undefined alias '\(anchor)' at line \(line), column \(column)"
case let .unexpectedEvent(expected, actual, line, column):
"expected \(expected) at line \(line), column \(column), found \(actual)"
case let .unexpectedToken(expected, actual, line, column):
"expected \(expected) at line \(line), column \(column), found \(actual)"
case let .nestingTooDeep(line, column):
"nesting is too deep at line \(line), column \(column)"
case let .malformed(reason, line, column):
"\(reason) at line \(line), column \(column)"
}
}
public var diagnosticCode: String {
switch self {
case .emptyDocument:
"emptyDocument"
case .tabIndentation:
"tabIndentation"
case .invalidControlCharacter:
"invalidControlCharacter"
case .unexpectedIndentation:
"unexpectedIndentation"
case .mixedCollectionStyles:
"mixedCollectionStyles"
case .expectedMappingKey:
"expectedMappingKey"
case .expectedAnchorName:
"expectedAnchorName"
case .expectedAliasName:
"expectedAliasName"
case .expectedNode:
"expectedNode"
case .expectedScalarKey:
"expectedScalarKey"
case .incompatibleYAMLDirective:
"incompatibleYAMLDirective"
case .directiveWithoutDocument:
"directiveWithoutDocument"
case .invalidTaggedScalar:
"invalidTaggedScalar"
case .invalidMergeValue:
"invalidMergeValue"
case .unterminatedTag:
"unterminatedTag"
case .unterminatedQuotedString:
"unterminatedQuotedString"
case .unsupportedDirective:
"unsupportedDirective"
case .unsupportedMultiDocumentStream:
"unsupportedMultiDocumentStream"
case .undefinedAlias:
"undefinedAlias"
case .unexpectedEvent:
"unexpectedEvent"
case .unexpectedToken:
"unexpectedToken"
case .nestingTooDeep:
"nestingTooDeep"
case .malformed:
"malformed"
}
}
public var sourceLine: Int? {
switch self {
case .emptyDocument:
nil
case let .tabIndentation(line),
let .unexpectedIndentation(line),
let .mixedCollectionStyles(line),
let .expectedMappingKey(line),
let .expectedAnchorName(line),
let .expectedAliasName(line),
let .incompatibleYAMLDirective(line),
let .directiveWithoutDocument(line),
let .unterminatedTag(line),
let .unterminatedQuotedString(line),
let .unsupportedDirective(_, line),
let .unsupportedMultiDocumentStream(line):
line
case let .expectedNode(line, _),
let .expectedScalarKey(line, _),
let .invalidTaggedScalar(_, _, line, _),
let .invalidMergeValue(line, _),
let .undefinedAlias(_, line, _),
let .unexpectedEvent(_, _, line, _),
let .unexpectedToken(_, _, line, _),
let .nestingTooDeep(line, _),
let .invalidControlCharacter(line, _),
let .malformed(_, line, _):
line
}
}
public var sourceColumn: Int? {
switch self {
case .emptyDocument,
.tabIndentation,
.unexpectedIndentation,
.mixedCollectionStyles,
.expectedMappingKey,
.expectedAnchorName,
.expectedAliasName,
.incompatibleYAMLDirective,
.directiveWithoutDocument,
.unterminatedTag,
.unterminatedQuotedString,
.unsupportedDirective,
.unsupportedMultiDocumentStream:
nil
case let .expectedNode(_, column),
let .expectedScalarKey(_, column),
let .invalidTaggedScalar(_, _, _, column),
let .invalidMergeValue(_, column),
let .undefinedAlias(_, _, column),
let .unexpectedEvent(_, _, _, column),
let .unexpectedToken(_, _, _, column),
let .nestingTooDeep(_, column),
let .invalidControlCharacter(_, column),
let .malformed(_, _, column):
column
}
}
}
}