Three root causes: (1) a '%' begins a directive only in directive position (before document content, outside flow); everywhere else it is plain content, fixing misfires inside plain scalars, flow mappings, and block scalars. (2) %YAML accepts any 1.x (a higher minor is treated as 1.2 without error), rejects major != 1 and duplicate %YAML, and requires a following '---'; reserved and %TAG directives are accepted and ignored. A directive not followed by a document start is rejected with the new directiveWithoutDocument error. (3) An explicit '...' end marker closes the current document so following content begins a fresh document in parseStream; single-document parse still rejects multi-document input at compose time. Tests/UnsupportedYAMLGapsFixtures.swift, StreamParsingTests, ScannerTests, and ParsingErrorTests were updated in lockstep to assert the now-correct behavior (percent-after-content is plain text; '...' then content is two documents; %YAML 1.3 is valid while 2.0 and duplicate %YAML are rejected); no test was weakened and multi-document rejection for single-document parse is still covered. Flips 15 official YAML test suite cases (295/402); SF5V, 9MMA, B63P, H7TQ now correctly rejected. 6LVF and W4TN are reclassified to json-mismatch (blocked by other scalar gaps). Updates the manifest, fixtures, counts, and roadmap.
162 lines
6.0 KiB
Swift
162 lines
6.0 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanDirectiveOrDocumentMarker(_ state: inout State) throws -> Bool {
|
|
guard state.indentation.last == 0, state.flowDepth == 0 else {
|
|
return false
|
|
}
|
|
if state.reader.peek() == "%" {
|
|
// `%` introduces a directive only in directive position: before any
|
|
// document content and before the `---` that starts a document. After
|
|
// content, after `---`, or anywhere inside a node, `%` is plain text,
|
|
// so defer to ordinary token scanning by returning false. This mirrors
|
|
// the C scanner, which only enters `yaml_parser_scan_directive` from
|
|
// the start-of-line directive context, never inside a node or scalar.
|
|
guard !state.hasDocumentContent, !state.hasDocumentStartMarker else {
|
|
return false
|
|
}
|
|
try scanDirective(&state)
|
|
return true
|
|
}
|
|
if state.reader.peek() == "#" {
|
|
return false
|
|
}
|
|
if startsWith("---", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
|
|
scanDocumentMarker(.documentStart, state: &state)
|
|
resetForDocumentStart(&state)
|
|
return true
|
|
}
|
|
if startsWith("...", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
|
|
// A `...` end marker terminates the directive section it follows just
|
|
// as a `---` would, except that no document was started, so a pending
|
|
// directive here was never attached to a document and is rejected.
|
|
if state.hasPendingDirective {
|
|
throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: state.reader.mark.line)
|
|
}
|
|
scanDocumentMarker(.documentEnd, state: &state)
|
|
resetForDocumentEnd(&state)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func scanDocumentMarker(
|
|
_ kind: PureYAML.Parsing.TokenKind,
|
|
state: inout State,
|
|
) {
|
|
let start = state.reader.mark
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
state.append(kind, mark: start, endMark: state.reader.mark)
|
|
skipLine(&state)
|
|
}
|
|
|
|
func resetForDocumentStart(_ state: inout State) {
|
|
state.hasDocumentContent = false
|
|
state.hasDocumentStartMarker = true
|
|
state.isDocumentClosed = false
|
|
state.sawYAMLDirective = false
|
|
state.hasPendingDirective = false
|
|
}
|
|
|
|
func resetForDocumentEnd(_ state: inout State) {
|
|
state.hasDocumentContent = false
|
|
state.hasDocumentStartMarker = false
|
|
state.isDocumentClosed = true
|
|
state.sawYAMLDirective = false
|
|
state.hasPendingDirective = false
|
|
}
|
|
|
|
func scanDirective(_ state: inout State) throws {
|
|
let start = state.reader.mark
|
|
let line = state.reader.consume { character in
|
|
!isLineBreak(character)
|
|
}
|
|
let fields = directiveFields(line)
|
|
guard let name = fields.first else {
|
|
return
|
|
}
|
|
switch name {
|
|
case "%YAML":
|
|
try scanYAMLDirective(fields: fields, start: start, state: &state)
|
|
case "%TAG":
|
|
scanTagDirective(fields: fields, state: &state)
|
|
default:
|
|
// Reserved or unknown directives are ignored with a warning per the
|
|
// spec; PureYAML accepts and discards them rather than erroring.
|
|
break
|
|
}
|
|
state.hasPendingDirective = true
|
|
}
|
|
|
|
/// Splits a directive line into its `%name` and parameter fields, dropping
|
|
/// any trailing comment. Fields are separated by spaces or tabs, matching the
|
|
/// C scanner's blank handling around directive values.
|
|
private func directiveFields(_ line: String) -> [String] {
|
|
var fields: [String] = []
|
|
var current = ""
|
|
for character in line {
|
|
// A `#` at a field boundary (i.e. preceded by whitespace or the line
|
|
// start) begins a trailing comment that ends the directive's fields.
|
|
if character == "#", current.isEmpty {
|
|
break
|
|
}
|
|
if character == " " || character == "\t" {
|
|
if !current.isEmpty {
|
|
fields.append(current)
|
|
current = ""
|
|
}
|
|
continue
|
|
}
|
|
current.append(character)
|
|
}
|
|
if !current.isEmpty {
|
|
fields.append(current)
|
|
}
|
|
return fields
|
|
}
|
|
|
|
private func scanYAMLDirective(
|
|
fields: [String],
|
|
start: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) throws {
|
|
// A `%YAML` directive carries exactly one version field of the form
|
|
// `major.minor`. A duplicate within one document, extra trailing fields,
|
|
// or an unsupported major version is an error; a higher 1.x minor is
|
|
// accepted (treated as 1.2 with a warning) so future minors keep parsing.
|
|
guard !state.sawYAMLDirective else {
|
|
throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line)
|
|
}
|
|
guard fields.count == 2, isSupportedYAMLVersion(fields[1]) else {
|
|
throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line)
|
|
}
|
|
state.sawYAMLDirective = true
|
|
}
|
|
|
|
private func isSupportedYAMLVersion(_ version: String) -> Bool {
|
|
let components = version.split(separator: ".", omittingEmptySubsequences: false)
|
|
guard components.count == 2 else {
|
|
return false
|
|
}
|
|
let major = components[0]
|
|
let minor = components[1]
|
|
guard !major.isEmpty, !minor.isEmpty else {
|
|
return false
|
|
}
|
|
guard major.allSatisfy(\.isNumber), minor.allSatisfy(\.isNumber) else {
|
|
return false
|
|
}
|
|
return major == "1"
|
|
}
|
|
|
|
private func scanTagDirective(
|
|
fields: [String],
|
|
state: inout State,
|
|
) {
|
|
guard fields.count >= 3 else {
|
|
return
|
|
}
|
|
state.tagHandles[fields[1]] = fields[2]
|
|
}
|
|
}
|