fix(parsing): require an explicit document after a directive
The YAML grammar requires every directive to head an explicit document, so a directive must be followed by a '---' marker. The scanner accepted a directive with no document at all: '%YAML 1.2' alone, or '%YAML 1.2' then a '...' document-end marker, parsed without error. The scanner now records the line of a directive awaiting its document, clears it at a document start, and rejects a still-pending directive at a document-end marker and at stream end. Both scan drivers finalize through the same stream-end path, so the check lives in one helper called from each. The rule applies to any directive, including an unrecognized one (which is otherwise still ignored); the directive test that relied on an unrecognized directive parsing without a '---' is corrected to include one, since that leniency is not permitted by the grammar. Measured against the official YAML test-suite, parse-level conformance rises from 253/333 (76.0%) to 255/333 (76.6%) with no regression.
This commit is contained in:
parent
4c77b17f9f
commit
27c423dbb9
@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A directive must now be followed by an explicit `---` document start. A stream that ends, or
|
||||
reaches a `...` document-end marker, while a directive is still awaiting its document
|
||||
(`%YAML 1.2` alone, or `%YAML 1.2` then `...`) is now rejected, as the YAML grammar requires a
|
||||
directive to head an explicit document. This applies to any directive, including an unrecognized
|
||||
one, which is still otherwise ignored. A directive followed by `---` (with content, inline, or
|
||||
empty) is unaffected. Lifts official test-suite parse-level conformance from 253/333 (76.0%) to
|
||||
255/333 (76.6%), with no regression.
|
||||
- The `%YAML` directive is now validated. It takes exactly one parameter, the version, optionally
|
||||
followed by a comment, and at most one may appear per document; an extra parameter
|
||||
(`%YAML 1.2 foo`) or a second `%YAML` directive is now rejected. Previously any tokens after the
|
||||
|
||||
@ -22,6 +22,7 @@ public extension PureYAML.Parsing {
|
||||
case invalidEscape(sequence: String, line: Int)
|
||||
case malformedDirective(name: String, line: Int)
|
||||
case duplicateDirective(name: String, line: Int)
|
||||
case directiveWithoutDocument(line: Int)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
@ -67,6 +68,8 @@ public extension PureYAML.Parsing {
|
||||
"malformed directive '\(name)' at line \(line)"
|
||||
case let .duplicateDirective(name, line):
|
||||
"duplicate directive '\(name)' at line \(line)"
|
||||
case let .directiveWithoutDocument(line):
|
||||
"a directive must be followed by a document at line \(line)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,6 +117,8 @@ public extension PureYAML.Parsing {
|
||||
"malformedDirective"
|
||||
case .duplicateDirective:
|
||||
"duplicateDirective"
|
||||
case .directiveWithoutDocument:
|
||||
"directiveWithoutDocument"
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +135,8 @@ public extension PureYAML.Parsing {
|
||||
let .unsupportedMultiDocumentStream(line),
|
||||
let .invalidEscape(_, line),
|
||||
let .malformedDirective(_, line),
|
||||
let .duplicateDirective(_, line):
|
||||
let .duplicateDirective(_, line),
|
||||
let .directiveWithoutDocument(line):
|
||||
line
|
||||
case let .expectedNode(line, _),
|
||||
let .expectedScalarKey(line, _),
|
||||
@ -158,7 +164,8 @@ public extension PureYAML.Parsing {
|
||||
.unsupportedMultiDocumentStream,
|
||||
.invalidEscape,
|
||||
.malformedDirective,
|
||||
.duplicateDirective:
|
||||
.duplicateDirective,
|
||||
.directiveWithoutDocument:
|
||||
nil
|
||||
case let .expectedNode(_, column),
|
||||
let .expectedScalarKey(_, column),
|
||||
|
||||
@ -19,6 +19,8 @@ extension PureYAML.Parsing.Scanner {
|
||||
guard !state.isDocumentClosed else {
|
||||
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line)
|
||||
}
|
||||
// A `...` ends a document, so a directive still awaiting its `---` never gets one.
|
||||
try enforceDirectiveHasDocument(state)
|
||||
scanDocumentMarker(.documentEnd, state: &state)
|
||||
resetForDocumentEnd(&state)
|
||||
return true
|
||||
@ -59,6 +61,15 @@ extension PureYAML.Parsing.Scanner {
|
||||
state.hasDocumentStartMarker = true
|
||||
state.isDocumentClosed = false
|
||||
state.hasYAMLDirective = false
|
||||
state.pendingDirectiveLine = nil
|
||||
}
|
||||
|
||||
/// A directive must be followed by an explicit `---` document start. Called at a document end
|
||||
/// and at stream end, where a still-pending directive means none ever came.
|
||||
func enforceDirectiveHasDocument(_ state: State) throws {
|
||||
if let line = state.pendingDirectiveLine {
|
||||
throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: line)
|
||||
}
|
||||
}
|
||||
|
||||
func resetForDocumentEnd(_ state: inout State) {
|
||||
@ -80,6 +91,8 @@ extension PureYAML.Parsing.Scanner {
|
||||
guard !state.hasDocumentContent, !state.hasDocumentStartMarker, !state.isDocumentClosed else {
|
||||
throw PureYAML.Parsing.ParseError.unsupportedDirective(name: name, line: start.line)
|
||||
}
|
||||
// Every directive must be followed by an explicit `---`; record that one is now awaiting it.
|
||||
state.pendingDirectiveLine = start.line
|
||||
switch name {
|
||||
case "%YAML":
|
||||
guard parts.indices.contains(1), parts[1] == "1.1" || parts[1] == "1.2" else {
|
||||
|
||||
@ -33,6 +33,7 @@ extension PureYAML.Parsing {
|
||||
}
|
||||
|
||||
closeIndentation(&state)
|
||||
try enforceDirectiveHasDocument(state)
|
||||
state.append(.streamEnd)
|
||||
return state.tokens
|
||||
}
|
||||
@ -49,6 +50,10 @@ extension PureYAML.Parsing.Scanner {
|
||||
var hasDocumentStartMarker = false
|
||||
var isDocumentClosed = false
|
||||
var hasYAMLDirective = false
|
||||
// The line of a directive awaiting its document. A directive must be followed by an explicit
|
||||
// `---`; this is set when one is scanned, cleared at a document start, and checked at a
|
||||
// document end and at stream end so a directive with no document is rejected.
|
||||
var pendingDirectiveLine: Int?
|
||||
var flowDepth = 0
|
||||
var flowContexts: [PureYAML.Parsing.ScannerFlowContext] = []
|
||||
var validImplicitIndentation: Set<Int> = []
|
||||
|
||||
@ -80,6 +80,7 @@ extension PureYAML.Parsing {
|
||||
while scannerBox.state.tokens.isEmpty {
|
||||
guard !scannerBox.state.reader.isAtEnd else {
|
||||
scanner.closeIndentation(&scannerBox.state)
|
||||
try scanner.enforceDirectiveHasDocument(scannerBox.state)
|
||||
scannerBox.state.append(.streamEnd)
|
||||
drainScannedTokens(from: &scannerBox.state)
|
||||
self.scannerBox = nil
|
||||
|
||||
@ -6,10 +6,11 @@ struct DirectiveTests {
|
||||
@Test("An unrecognized directive is ignored, not rejected")
|
||||
func test_unknownDirectiveIsIgnored() throws {
|
||||
// The YAML spec ignores a directive it does not recognize (with a warning), so the
|
||||
// document still parses.
|
||||
// document still parses. Directives must still be followed by an explicit `---`, so these
|
||||
// cases include one: the point here is that `%FOO` itself does not cause a rejection.
|
||||
#expect(try PureYAML.parse("%FOO bar baz\n---\n\"foo\"\n") == .string("foo"))
|
||||
#expect(
|
||||
try PureYAML.parse("%FOO bar\nvalue: one\n")
|
||||
try PureYAML.parse("%FOO bar\n---\nvalue: one\n")
|
||||
== .mapping(.init([.init(key: "value", value: .string("one"))])),
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,4 +20,21 @@ struct YAMLDirectiveTests {
|
||||
#expect(try PureYAML.parse("%YAML 1.2\n---\nx\n") == expected)
|
||||
#expect(try PureYAML.parse("%YAML 1.2 # version\n---\nx\n") == expected)
|
||||
}
|
||||
|
||||
@Test("A directive with no following document is rejected", arguments: [
|
||||
"%YAML 1.2\n", // directive then end of stream
|
||||
"%YAML 1.2\n...\n", // directive then a document-end marker
|
||||
"%TAG !e! tag:example,0:\n", // any directive, not just %YAML
|
||||
])
|
||||
func test_directiveWithoutDocumentRejected(_ yaml: String) throws {
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A directive followed by an explicit document start is accepted")
|
||||
func test_directiveWithDocumentAccepted() throws {
|
||||
#expect(try PureYAML.parse("%YAML 1.2\n---\n") == .null)
|
||||
#expect(try PureYAML.parse("%TAG !e! tag:example,0:\n---\nx\n") == .string("x"))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user