PureYAML/Sources/Parsing/LibYAML.Parser.Directives.swift
Mihaela Mihaljevic 0588996506 feat(parsing): run the whole library on the ported engine
Routes the value and stream APIs through the ported scanner+parser (the
event API already was), so PureYAML now parses entirely on the libyaml
port. Adds the features needed to match and exceed the previous engine:
JSON UTF-16 surrogate-pair \u escapes, bare documents after an explicit
`...`, reserved-directive handling that still requires a following `---`,
and a 64-level nesting bound enforced on the event stream so deeply nested
input fails with nestingTooDeep instead of overflowing a recursive
consumer. Parser errors are translated to the diagnostic-aware ParseError,
recovering precise codes where identifiable and a readable malformed
fallback otherwise; error-reporting fixtures are rebased onto the port.

Canonical event conformance: 268 -> 271 of 308 (7Z25, HWV9, QT73).
393 tests pass.
2026-06-24 18:02:22 +02:00

71 lines
3.5 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 if case .reservedDirective = token.type {
// A reserved directive is ignored (YAML 1.2 §6.8.1) but still
// consumed here so the explicit-document path requires a `---`.
} 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
}
}