PureYAML/Sources/Parsing/LibYAML.Parser.Directives.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

68 lines
3.3 KiB
Swift

extension PureYAML.Parsing.LibYAML.Parser {
/// The implied tag handles, mirroring libyaml's `default_tag_directives`.
private var defaultTagDirectives: [(handle: String, prefix: String)] {
[("!", "!"), ("!!", "tag:yaml.org,2002:")]
}
/// Port of `yaml_parser_process_directives`: consume the `%YAML` and `%TAG`
/// directive tokens at the head of a document, validating the version and
/// recording the tag handles, then install the default tag handles. The
/// version is checked against a local so a duplicate `%YAML` is rejected.
mutating func processDirectives() throws {
var version: (major: Int, minor: Int)?
var token = peekToken()
while true {
if case let .versionDirective(major, minor) = token.type {
try recordVersion(major: major, minor: minor, into: &version, mark: token.startMark)
} else if case let .tagDirective(handle, prefix) = token.type {
try appendTagDirective(handle: handle, prefix: prefix, allowDuplicates: false, mark: token.startMark)
} else {
break
}
skipToken()
token = peekToken()
}
for directive in defaultTagDirectives {
try appendTagDirective(handle: directive.handle, prefix: directive.prefix, allowDuplicates: true, mark: token.startMark)
}
}
private mutating func recordVersion(major: Int, minor: Int, into version: inout (major: Int, minor: Int)?, mark: Mark) throws {
guard version == nil else {
throw ParserError(context: nil, problem: "found duplicate %YAML directive", mark: mark)
}
// YAML 1.2 §6.8.1: a document whose major version is 1 is parsed even when
// the minor version is newer than 1.2 (with a warning), rather than
// libyaml's hard rejection of any minor other than 1 or 2.
guard major == 1 else {
throw ParserError(context: nil, problem: "found incompatible YAML document", mark: mark)
}
version = (major, minor)
}
/// Port of `yaml_parser_append_tag_directive`: append a handle/prefix mapping,
/// rejecting a redefinition unless duplicates are allowed (the defaults are).
mutating func appendTagDirective(handle: String, prefix: String, allowDuplicates: Bool, mark: Mark) throws {
if tagDirectives.contains(where: { $0.handle == handle }) {
if allowDuplicates {
return
}
throw ParserError(context: nil, problem: "found duplicate %TAG directive", mark: mark)
}
tagDirectives.append((handle, prefix))
}
/// Port of the tag-resolution block in `yaml_parser_parse_node`: an empty
/// handle means the suffix is a verbatim tag; otherwise substitute the handle's
/// registered prefix, failing on an undefined handle.
func resolveTag(handle: String, suffix: String, startMark _: Mark, tagMark: Mark) throws -> String {
guard !handle.isEmpty else {
return suffix
}
guard let directive = tagDirectives.first(where: { $0.handle == handle }) else {
throw ParserError(context: "while parsing a node", problem: "found undefined tag handle", mark: tagMark)
}
return directive.prefix + suffix
}
}