fix(parsing): reject content after a document end marker
A document-end marker takes no node on its own line: only a comment or blank may follow. The scanner skipped the rest of the marker's line, so content such as the 'invalid' in '... invalid' was silently dropped and the document accepted. scanDocumentMarker now distinguishes the two markers after skipping spaces: a document start may carry the root node on its line and leaves it for the main loop, while a document end rejects any real content (a comment or blank still passes). A '...' not followed by an indicator boundary is not a marker at all and remains an ordinary plain scalar, so '...junk' still parses as content. Measured against the official YAML test-suite, parse-level conformance rises from 255/333 (76.6%) to 256/333 (76.9%) with no regression.
This commit is contained in:
parent
27c423dbb9
commit
fcba4e1f57
@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A `...` document-end marker no longer accepts content on its own line. Only a comment or blank
|
||||
may follow the marker, so `... invalid` is now rejected; the scanner previously skipped the rest
|
||||
of the line. A bare `...`, a trailing comment (`... # done`), and a `...` not followed by a
|
||||
boundary (`...junk`, which is an ordinary plain scalar, not a marker) are unaffected. Lifts
|
||||
official test-suite parse-level conformance from 255/333 (76.6%) to 256/333 (76.9%), with no
|
||||
regression.
|
||||
- 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
|
||||
|
||||
@ -23,6 +23,7 @@ public extension PureYAML.Parsing {
|
||||
case malformedDirective(name: String, line: Int)
|
||||
case duplicateDirective(name: String, line: Int)
|
||||
case directiveWithoutDocument(line: Int)
|
||||
case contentAfterDocumentEnd(line: Int)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
@ -70,6 +71,8 @@ public extension PureYAML.Parsing {
|
||||
"duplicate directive '\(name)' at line \(line)"
|
||||
case let .directiveWithoutDocument(line):
|
||||
"a directive must be followed by a document at line \(line)"
|
||||
case let .contentAfterDocumentEnd(line):
|
||||
"no content may follow a document end marker at line \(line)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +122,8 @@ public extension PureYAML.Parsing {
|
||||
"duplicateDirective"
|
||||
case .directiveWithoutDocument:
|
||||
"directiveWithoutDocument"
|
||||
case .contentAfterDocumentEnd:
|
||||
"contentAfterDocumentEnd"
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +141,8 @@ public extension PureYAML.Parsing {
|
||||
let .invalidEscape(_, line),
|
||||
let .malformedDirective(_, line),
|
||||
let .duplicateDirective(_, line),
|
||||
let .directiveWithoutDocument(line):
|
||||
let .directiveWithoutDocument(line),
|
||||
let .contentAfterDocumentEnd(line):
|
||||
line
|
||||
case let .expectedNode(line, _),
|
||||
let .expectedScalarKey(line, _),
|
||||
@ -165,7 +171,8 @@ public extension PureYAML.Parsing {
|
||||
.invalidEscape,
|
||||
.malformedDirective,
|
||||
.duplicateDirective,
|
||||
.directiveWithoutDocument:
|
||||
.directiveWithoutDocument,
|
||||
.contentAfterDocumentEnd:
|
||||
nil
|
||||
case let .expectedNode(_, column),
|
||||
let .expectedScalarKey(_, column),
|
||||
|
||||
@ -11,7 +11,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
return false
|
||||
}
|
||||
if startsWith("---", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
|
||||
scanDocumentMarker(.documentStart, state: &state)
|
||||
try scanDocumentMarker(.documentStart, state: &state)
|
||||
resetForDocumentStart(&state)
|
||||
return true
|
||||
}
|
||||
@ -21,7 +21,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
}
|
||||
// A `...` ends a document, so a directive still awaiting its `---` never gets one.
|
||||
try enforceDirectiveHasDocument(state)
|
||||
scanDocumentMarker(.documentEnd, state: &state)
|
||||
try scanDocumentMarker(.documentEnd, state: &state)
|
||||
resetForDocumentEnd(&state)
|
||||
return true
|
||||
}
|
||||
@ -34,24 +34,24 @@ extension PureYAML.Parsing.Scanner {
|
||||
func scanDocumentMarker(
|
||||
_ kind: PureYAML.Parsing.TokenKind,
|
||||
state: inout State,
|
||||
) {
|
||||
) throws {
|
||||
let start = state.reader.mark
|
||||
state.reader.advance()
|
||||
state.reader.advance()
|
||||
state.reader.advance()
|
||||
state.append(kind, mark: start, endMark: state.reader.mark)
|
||||
// The document's root node may begin on the marker's own line (e.g. `--- >`, `--- foo`,
|
||||
// `--- {a: 1}`, `--- !!str x`). After a document start, skip only the spaces and leave any
|
||||
// real content for the main loop to scan; an empty rest-of-line or a comment is consumed.
|
||||
// A document end (`...`) takes no same-line node, so its line is always skipped.
|
||||
if case .documentStart = kind {
|
||||
while state.reader.peek() == " " {
|
||||
state.reader.advance()
|
||||
}
|
||||
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
|
||||
while state.reader.peek() == " " {
|
||||
state.reader.advance()
|
||||
}
|
||||
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
|
||||
// The marker's own line carries real content. A document start may carry the root node
|
||||
// here (e.g. `--- >`, `--- foo`, `--- {a: 1}`), so leave it for the main loop to scan.
|
||||
if case .documentStart = kind {
|
||||
state.isAtLineStart = false
|
||||
return
|
||||
}
|
||||
// A document end (`...`) takes no same-line node; only a comment or blank may follow it.
|
||||
throw PureYAML.Parsing.ParseError.contentAfterDocumentEnd(line: state.reader.mark.line)
|
||||
}
|
||||
skipLine(&state)
|
||||
}
|
||||
|
||||
27
Tests/DocumentEndContentTests.swift
Normal file
27
Tests/DocumentEndContentTests.swift
Normal file
@ -0,0 +1,27 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Content after a document end marker")
|
||||
struct DocumentEndContentTests {
|
||||
@Test("Real content on a `...` marker's line is rejected")
|
||||
func test_contentAfterDocumentEndRejected() throws {
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse("---\nkey: value\n... invalid\n")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A bare `...`, or one followed only by a comment, is accepted", arguments: [
|
||||
"---\nx\n...\n", // nothing after the marker
|
||||
"---\nx\n... # comment\n", // only a comment follows
|
||||
"---\nx\n... \n", // only trailing spaces follow
|
||||
])
|
||||
func test_bareDocumentEndAccepted(_ yaml: String) throws {
|
||||
#expect(try PureYAML.parse(yaml) == .string("x"))
|
||||
}
|
||||
|
||||
@Test("`...` not followed by a boundary is a plain scalar, not a marker")
|
||||
func test_dotsWithoutBoundaryIsScalar() throws {
|
||||
// `...junk` is not a document-end marker, so it continues the plain scalar `x`.
|
||||
#expect(try PureYAML.parse("---\nx\n...junk\n") == .string("x ...junk"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user