From c1dda8bbabec93a8ebab836d94653b21c75be7cd Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sat, 27 Jun 2026 23:20:52 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 6 ++++++ Sources/Parsing/ParseError.swift | 7 +++++++ Sources/Parsing/Scanner.Scalars.swift | 5 +++++ Tests/DocumentMarkerInScalarTests.swift | 25 +++++++++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 Tests/DocumentMarkerInScalarTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d08fdb..cd9d29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/ParseError.swift b/Sources/Parsing/ParseError.swift index 9df5668..547ad37 100644 --- a/Sources/Parsing/ParseError.swift +++ b/Sources/Parsing/ParseError.swift @@ -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, diff --git a/Sources/Parsing/Scanner.Scalars.swift b/Sources/Parsing/Scanner.Scalars.swift index 05c57e5..b9dabfb 100644 --- a/Sources/Parsing/Scanner.Scalars.swift +++ b/Sources/Parsing/Scanner.Scalars.swift @@ -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) diff --git a/Tests/DocumentMarkerInScalarTests.swift b/Tests/DocumentMarkerInScalarTests.swift new file mode 100644 index 0000000..bfccade --- /dev/null +++ b/Tests/DocumentMarkerInScalarTests.swift @@ -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) + } +}