PureYAML/Sources/Parsing/LibYAML.Scanner.Directive.swift
Mihaela Mihaljevic 6ce1f906f9 feat(parsing): surpass libyaml on reserved directives and YAML versions
YAML 1.2 §6.8.1 requires a reserved directive (anything other than %YAML
or %TAG) to be ignored with a warning, and a document whose minor version
is newer than 1.2 to be parsed with a warning. libyaml rejects both. The
scanner now drops a reserved directive's whole line and emits no token,
and the parser accepts any major-version-1 document.

Canonical event conformance: 253 -> 258 of 308 (2LFX, 6LVF, MUS6/05,
MUS6/06, BEC7). 333 tests pass.
2026-06-24 17:37:32 +02:00

147 lines
6.3 KiB
Swift

extension PureYAML.Parsing.LibYAML.Scanner {
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
private typealias Token = PureYAML.Parsing.LibYAML.Token
private typealias Mark = PureYAML.Parsing.Mark
/// The longest version number libyaml accepts (`MAX_NUMBER_LENGTH`).
private var maxVersionNumberLength: Int {
9
}
/// Port of `yaml_parser_fetch_directive`: reset the indentation and simple keys,
/// then scan a `%YAML` or `%TAG` directive token.
mutating func fetchDirective() throws {
indentEngine.unrollIndent(column: -1, mark: reader.mark)
try removeSimpleKey()
simpleKeyAllowed = false
if let token = try scanDirective() {
indentEngine.enqueue(token)
}
}
/// Port of `yaml_parser_scan_directive`, extended to YAML 1.2 §6.8.1: a `%YAML`
/// or `%TAG` directive produces its token; any other (reserved) directive is
/// ignored with a warning, producing no token, rather than libyaml's hard
/// rejection. Returns nil for the ignored case.
private mutating func scanDirective() throws -> Token? {
let startMark = reader.mark
reader.skip()
let name = try scanDirectiveName(startMark: startMark)
if name == "YAML" {
let version = try scanVersionDirectiveValue(startMark: startMark)
let token = Token(type: .versionDirective(major: version.major, minor: version.minor), startMark: startMark, endMark: reader.mark)
try consumeDirectiveTail(startMark: startMark)
return token
}
if name == "TAG" {
let value = try scanTagDirectiveValue(startMark: startMark)
let token = Token(type: .tagDirective(handle: value.handle, prefix: value.prefix), startMark: startMark, endMark: reader.mark)
try consumeDirectiveTail(startMark: startMark)
return token
}
skipReservedDirectiveLine()
return nil
}
/// Consume a reserved directive's parameters through the end of the line; the
/// whole line is ignored per YAML 1.2 §6.8.1.
private mutating func skipReservedDirectiveLine() {
while !Classify.isBreakZ(reader.peek()) {
reader.skip()
}
if Classify.isBreak(reader.peek()) {
reader.skipLine()
}
}
/// Eat trailing blanks and an optional comment, require a line break, eat it.
private mutating func consumeDirectiveTail(startMark: Mark) throws {
while Classify.isBlank(reader.peek()) {
reader.skip()
}
if reader.peek() == "#" {
while !Classify.isBreakZ(reader.peek()) {
reader.skip()
}
}
guard Classify.isBreakZ(reader.peek()) else {
throw directiveError("while scanning a directive", "did not find expected comment or line break", startMark)
}
if Classify.isBreak(reader.peek()) {
reader.skipLine()
}
}
/// Port of `yaml_parser_scan_directive_name`: the alphanumeric name after `%`,
/// which must be non-empty and followed by a blank or line break.
private mutating func scanDirectiveName(startMark: Mark) throws -> String {
var name = ""
while let character = reader.peek(), Classify.isAlpha(character) {
name.unicodeScalars.append(character)
reader.skip()
}
guard !name.isEmpty else {
throw directiveError("while scanning a directive", "could not find expected directive name", startMark)
}
guard Classify.isBlankZ(reader.peek()) else {
throw directiveError("while scanning a directive", "found unexpected non-alphabetical character", startMark)
}
return name
}
/// Port of `yaml_parser_scan_version_directive_value`: `major.minor`.
private mutating func scanVersionDirectiveValue(startMark: Mark) throws -> (major: Int, minor: Int) {
while Classify.isBlank(reader.peek()) {
reader.skip()
}
let major = try scanVersionDirectiveNumber(startMark: startMark)
guard reader.peek() == "." else {
throw directiveError("while scanning a %YAML directive", "did not find expected digit or '.' character", startMark)
}
reader.skip()
let minor = try scanVersionDirectiveNumber(startMark: startMark)
return (major, minor)
}
/// Port of `yaml_parser_scan_version_directive_number`: a bounded run of digits.
private mutating func scanVersionDirectiveNumber(startMark: Mark) throws -> Int {
var value = 0
var length = 0
while let character = reader.peek(), Classify.isDigit(character) {
length += 1
guard length <= maxVersionNumberLength else {
throw directiveError("while scanning a %YAML directive", "found extremely long version number", startMark)
}
value = value * 10 + Int(character.value - Unicode.Scalar("0").value)
reader.skip()
}
guard length > 0 else {
throw directiveError("while scanning a %YAML directive", "did not find expected version number", startMark)
}
return value
}
/// Port of `yaml_parser_scan_tag_directive_value`: `!handle! prefix`.
private mutating func scanTagDirectiveValue(startMark: Mark) throws -> (handle: String, prefix: String) {
while Classify.isBlank(reader.peek()) {
reader.skip()
}
let handle = try scanTagHandle(directive: true, startMark: startMark)
guard Classify.isBlank(reader.peek()) else {
throw directiveError("while scanning a %TAG directive", "did not find expected whitespace", startMark)
}
while Classify.isBlank(reader.peek()) {
reader.skip()
}
let prefix = try scanTagURI(uriChar: true, directive: true, head: "", startMark: startMark)
guard Classify.isBlankZ(reader.peek()) else {
throw directiveError("while scanning a %TAG directive", "did not find expected whitespace or line break", startMark)
}
return (handle, prefix)
}
private func directiveError(_ context: String, _ problem: String, _ mark: Mark) -> PureYAML.Parsing.LibYAML.ScannerError {
PureYAML.Parsing.LibYAML.ScannerError(context: context, problem: problem, mark: mark)
}
}