PureYAML/Sources/Parsing/Scanner.Scalars.swift
Mihaela M c1dda8bbab 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.
2026-06-27 23:20:52 +02:00

167 lines
6.7 KiB
Swift

extension PureYAML.Parsing.Scanner {
func scanComment(_ state: inout State) {
let start = state.reader.mark
state.reader.advance()
if state.reader.peek() == " " {
state.reader.advance()
}
let value = state.reader.consume { character in
!isLineBreak(character)
}
state.append(.comment(value), mark: start, endMark: state.reader.mark)
}
func scanQuotedScalar(
_ style: PureYAML.Parsing.ScalarStyle,
state: inout State,
) throws {
let start = state.reader.mark
let quote: Character = style == .doubleQuoted ? "\"" : "'"
state.reader.advance()
var value = ""
var previousWasEscape = false
while let character = state.reader.peek() {
if style == .singleQuoted, character == quote, state.reader.peek(offset: 1) == quote {
value.append(character)
state.reader.advance()
value.append(character)
state.reader.advance()
continue
}
if character == quote, !previousWasEscape {
break
}
value.append(character)
previousWasEscape = character == "\\" && style == .doubleQuoted && !previousWasEscape
state.reader.advance()
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)
}
state.reader.advance()
// Unlike a plain scalar (which consumes the space before a `:` into itself), a quoted scalar
// stops at its closing quote, so the reader sits at any spaces before a `:`. A space between
// an implicit key and its mapping-value indicator is allowed, so look past them.
if isAtMappingValueIndicatorAfterSpaces(state: state), canSynthesizeImplicitBlockKey(state) {
// A block implicit key must lie on a single line, so a quoted scalar that spanned a line
// break (its open and close quotes sit on different lines) cannot be one. The rule is
// block-only: a flow mapping key may span lines, so the check is gated on block context.
// An explicit key (`? ...`) carries no `:` directly after the scalar, so never reaches here.
guard start.line == state.reader.mark.line || state.flowDepth > 0 else {
throw PureYAML.Parsing.ParseError.multilineImplicitKey(line: start.line)
}
state.append(.mappingKey, mark: start, endMark: start)
}
state.append(.scalar(value: value, style: style), mark: start, endMark: state.reader.mark)
}
func scanBlockScalarHeader(
_ style: PureYAML.Parsing.BlockScalarStyle,
state: inout State,
) {
let start = state.reader.mark
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
var indentationIndicator: Int?
state.reader.advance()
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
if character == "-" {
chomping = .strip
} else if character == "+" {
chomping = .keep
} else if character.isNumber {
indentationIndicator = Int(String(character))
}
state.reader.advance()
}
state.append(
.blockScalarHeader(style: style, chomping: chomping),
mark: start,
endMark: state.reader.mark,
)
state.pendingBlockScalarIndentation = true
state.pendingBlockScalarIndentationIndicator = indentationIndicator
}
func scanBlockScalarContent(_ state: inout State) {
let start = state.reader.mark
let value = state.blockScalarContentPrefix + state.reader.consume { character in
!isLineBreak(character)
}
state.blockScalarContentPrefix = ""
state.append(.scalar(value: value, style: .plain), mark: start, endMark: state.reader.mark)
}
func scanNamedToken(
kind: (String) -> PureYAML.Parsing.TokenKind,
missingNameError: (Int) -> PureYAML.Parsing.ParseError,
state: inout State,
) throws {
let start = state.reader.mark
state.reader.advance()
let name = state.reader.consume { character in
!isTokenTerminator(character)
}
guard !name.isEmpty else {
throw missingNameError(start.line)
}
state.append(kind(name), mark: start, endMark: state.reader.mark)
}
func scanTag(_ state: inout State) throws {
let start = state.reader.mark
state.reader.advance()
var value = "!"
if state.reader.peek() == "<" {
value.append(state.reader.advance() ?? "<")
var didClose = false
while let character = state.reader.peek() {
value.append(character)
state.reader.advance()
if character == ">" {
didClose = true
break
}
}
guard didClose else {
throw PureYAML.Parsing.ParseError.unterminatedTag(line: start.line)
}
} else {
value += state.reader.consume { character in
!isTokenTerminator(character)
}
value = expandTag(value, state: state)
}
state.append(.tag(value), mark: start, endMark: state.reader.mark)
}
func scanPlainScalar(_ state: inout State) {
let start = state.reader.mark
var value = ""
while let character = state.reader.peek() {
if shouldEndPlainScalar(character, state: state, accumulated: value) {
break
}
value.append(character)
state.reader.advance()
}
var trimmed = trimTrailingSpaces(value)
let isMappingKey = appendMappingKeyIfNeeded(start: start, state: &state)
if !isMappingKey {
trimmed = scanPlainScalarContinuations(
after: trimmed,
firstLineIndent: start.column - 1,
state: &state,
)
}
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
}
}