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) + } +}