Directives apply to the document that follows, so the previous document
must be closed with an explicit '...' end marker before them. After an
implicit end, a '%YAML'/'%TAG'/reserved directive is now rejected instead
of being silently accepted. Also reject a '%YAML' version trailed directly
by '#' with no separating space ('%YAML 1.1#...'), since a comment needs
leading separation; libyaml accepts both.
This restores reject-side conformance the suite manifest recorded as
passing but the libyaml port had dropped (9HCY, EB22, MUS6/00, MUS6/01,
RHX7), and updates the two PureYAML gap-error fixtures whose rejection now
fires earlier and more precisely at the misplaced directive.
Refresh yaml-test-suite-manifest.yaml to version 3: whole-suite
conformance is 384/402 (up from the recorded 331); 53 cases still marked
as gaps now pass, none regress.
154 lines
6.9 KiB
Swift
154 lines
6.9 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
|
|
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 a `reservedDirective` token whose content
|
|
/// the parser drops, rather than libyaml's hard rejection. The reserved token
|
|
/// still marks the document as a directives document, so it must be followed by
|
|
/// a `---`.
|
|
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 Token(type: .reservedDirective, startMark: startMark, endMark: reader.mark)
|
|
}
|
|
|
|
/// 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)
|
|
// The version must be followed by separation; a comment needs a space
|
|
// before its `#`, so `%YAML 1.1#...` (no space) is malformed. libyaml
|
|
// accepts it, treating the `#` as an adjacent comment.
|
|
guard Classify.isBlankZ(reader.peek()) else {
|
|
throw directiveError("while scanning a %YAML directive", "did not find expected digit or '.' character", 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)
|
|
}
|
|
}
|