fix(parsing): reject a document marker inside a multi-line scalar

A line that begins with a document marker ('---' or '...') followed by a
boundary is forbidden inside a multi-line scalar, since it is ambiguous
with a document boundary. A multi-line quoted scalar folded such a line
into its value instead of rejecting it.

scanQuotedScalar now checks, on crossing each line break, whether the next
line begins a document marker, and rejects the scalar if so. The check
requires the marker at the line start with a following boundary, so inline
dashes ('a --- b'), '---' with no boundary ('---b'), an indented '---',
and dots not at a line start stay ordinary content.

Measured against the official YAML test-suite, parse-level conformance
rises from 268/333 (80.5%) to 270/333 (81.1%) with no regression.
This commit is contained in:
Mihaela M 2026-06-27 23:20:52 +02:00
parent 28f549311f
commit c1dda8bbab
4 changed files with 43 additions and 0 deletions

View File

@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A document marker line (`---` or `...`) inside a multi-line quoted scalar is now rejected. Such
a line is ambiguous with a document boundary, so the spec forbids it within a scalar; the scanner
previously folded it into the value. The marker must begin a line and be followed by a boundary
to be rejected, so inline dashes (`"a --- b"`), a `---` with no following boundary (`---b`), and
an indented `---` remain ordinary content. Lifts official test-suite parse-level conformance from
268/333 (80.5%) to 270/333 (81.1%), with no regression.
- A block sequence can no longer start on the same line as its anchor or tag (`&a - x`). A block
collection must begin on the line after its node properties, so an anchor or tag immediately
before the first block entry is now rejected. An anchor or tag on its own line above the

View File

@ -29,6 +29,7 @@ public extension PureYAML.Parsing {
case commentInMultilineScalar(line: Int)
case aliasWithProperties(line: Int, column: Int)
case inlineBlockSequenceAfterProperty(line: Int, column: Int)
case documentMarkerInScalar(line: Int)
public var description: String {
switch self {
@ -88,6 +89,8 @@ public extension PureYAML.Parsing {
"an alias node cannot have an anchor or tag at line \(line), column \(column)"
case let .inlineBlockSequenceAfterProperty(line, column):
"a block sequence cannot start on the same line as its anchor or tag at line \(line), column \(column)"
case let .documentMarkerInScalar(line):
"a document marker cannot appear inside a multi-line scalar at line \(line)"
}
}
@ -149,6 +152,8 @@ public extension PureYAML.Parsing {
"aliasWithProperties"
case .inlineBlockSequenceAfterProperty:
"inlineBlockSequenceAfterProperty"
case .documentMarkerInScalar:
"documentMarkerInScalar"
}
}
@ -163,6 +168,7 @@ public extension PureYAML.Parsing {
let .unterminatedQuotedString(line),
let .unsupportedDirective(_, line),
let .unsupportedMultiDocumentStream(line),
let .documentMarkerInScalar(line),
let .invalidEscape(_, line),
let .malformedDirective(_, line),
let .duplicateDirective(_, line),
@ -198,6 +204,7 @@ public extension PureYAML.Parsing {
.unterminatedQuotedString,
.unsupportedDirective,
.unsupportedMultiDocumentStream,
.documentMarkerInScalar,
.invalidEscape,
.malformedDirective,
.duplicateDirective,

View File

@ -37,6 +37,11 @@ extension PureYAML.Parsing.Scanner {
if character != "\\" {
previousWasEscape = false
}
// A line that begins with a document marker (`---` / `...`) is forbidden inside a
// multi-line scalar: the marker would be ambiguous with a document boundary.
if isLineBreak(character), isAtDocumentMarker(state.reader) {
throw PureYAML.Parsing.ParseError.documentMarkerInScalar(line: state.reader.mark.line)
}
}
guard state.reader.peek() == quote else {
throw PureYAML.Parsing.ParseError.unterminatedQuotedString(line: start.line)

View File

@ -0,0 +1,25 @@
@testable import PureYAML
import Testing
@Suite("Document markers inside multi-line scalars")
struct DocumentMarkerInScalarTests {
@Test("A `---` or `...` line inside a multi-line quoted scalar is rejected", arguments: [
"---\n\"\n---\n\"\n", // a `---` line inside a double-quoted scalar
"---\n'\n...\n'\n", // a `...` line inside a single-quoted scalar
])
func test_documentMarkerInScalarRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse(yaml)
}
}
@Test("Dashes and dots that are not a line-start marker stay valid", arguments: [
"\"a --- b\"\n", // dashes inline, not at a line start
"\"a\n---b\"\n", // `---` without a following boundary is content
"\"a\nb\"\n", // an ordinary multi-line quoted scalar
"\"...x\"\n", // dots not at a line start
])
func test_nonMarkerContentStaysValid(_ yaml: String) throws {
_ = try PureYAML.parse(yaml)
}
}