fix(parsing): require flow continuation lines to be indented

YAML requires multi-line flow content to be indented past the enclosing
block context (s-flow-line-prefix). libyaml does not enforce this, so
'flow: [a,' / 'b]' with 'b' in column 0, and the equivalent multi-line
flow mapping, wrongly parsed.

Track whether reaching a token crossed a line break, and in flow context
reject a token at or below the parent block indent. A closing ']' / '}'
or a leading ',' may sit at any indentation, so those are exempt (a flow
mapping's closing brace in column 0 stays valid).

Surpasses libyaml on 9C9N, VJP3/00; whole-suite conformance 394/402,
canonical events 308/308.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 20:45:12 +02:00
parent 4a6b168565
commit eee7079efe
5 changed files with 39 additions and 4 deletions

View File

@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Reject a flow-collection continuation line that is not indented past the parent
block. YAML requires multi-line flow content to be more indented than the
enclosing block context, so `flow: [a,` / `b]` (with `b` in column 0) and the
equivalent multi-line flow mapping are malformed; libyaml does not enforce it. A
closing `]`/`}` or a leading `,` may still sit at any indentation. Surpasses
libyaml on 9C9N and VJP3/00.
- Reject content after a `...` document-end marker on the same line. Only
whitespace and a comment may follow `...`; unlike `--- x`, a `... x` cannot
carry inline content. Surpasses libyaml on 3HFZ.

View File

@ -25,6 +25,7 @@ extension PureYAML.Parsing.LibYAML.Scanner {
}
try scanToNextToken()
try checkFlowContinuationIndent()
mark = reader.mark
try staleSimpleKeys()
indentEngine.unrollIndent(column: reader.column, mark: reader.mark)
@ -106,6 +107,28 @@ extension PureYAML.Parsing.LibYAML.Scanner {
}
}
/// Reject a flow-collection continuation line that is not indented past the
/// enclosing block context. YAML requires `s-flow-line-prefix` to be more
/// indented than the parent block; libyaml does not enforce it, so multi-line
/// flow content in column 0 (`[a,\nb]` under a key) wrongly parses.
private mutating func checkFlowContinuationIndent() throws {
guard flowLevel > 0, crossedLineBreak, let next = reader.peek() else {
return
}
// A closing `]`/`}` (or a `,` separator) may sit at any indentation; only
// flow CONTENT must be indented past the parent block.
if next == "]" || next == "}" || next == "," {
return
}
if reader.column <= indentEngine.indent {
throw ScannerError(
context: "while parsing a flow node",
problem: "found insufficiently indented line in a flow collection",
mark: reader.mark,
)
}
}
/// The error raised when a tab is used where indentation is required (before a
/// block indicator or an implicit mapping key). A tab is valid separation but
/// never indentation.

View File

@ -8,6 +8,7 @@ extension PureYAML.Parsing.LibYAML.Scanner {
/// here; `tabbedSeparation` records when one preceded the token so a block
/// indicator or implicit key (where the tab would be indentation) can reject.
mutating func scanToNextToken() throws {
crossedLineBreak = false
while true {
tabbedSeparation = false
if reader.column == 0, reader.peek() == "\u{FEFF}" {
@ -33,6 +34,7 @@ extension PureYAML.Parsing.LibYAML.Scanner {
}
if Classify.isBreak(reader.peek()) {
reader.skipLine()
crossedLineBreak = true
if flowLevel == 0 {
simpleKeyAllowed = true
}

View File

@ -18,6 +18,10 @@ extension PureYAML.Parsing.LibYAML {
/// tab is valid in-line separation but never indentation, so a block
/// indicator or implicit mapping key reached through one is an error.
var tabbedSeparation = false
/// Whether reaching the pending token crossed at least one line break. In
/// flow context a continuation line must be indented past the parent block,
/// so a token on a new line at or below that indent is an error.
var crossedLineBreak = false
/// `parser->mark`, kept in sync with the reader at each fetch step.
var mark: PureYAML.Parsing.Mark = .start
/// `parser->stream_start_produced`.

View File

@ -10,8 +10,8 @@ source:
# oracle: json if in.json present, else none
# status: passing (PureYAML conformant today) | gap (not yet conformant)
total: 402
passing: 392
gap: 10
passing: 394
gap: 8
cases:
- id: "229Q"
name: "Spec Example 2.4. Sequence of Mappings"
@ -986,7 +986,7 @@ cases:
localPath: yaml-test-suite/9C9N/in.yaml
expected: reject
oracle: none
status: gap
status: passing
byteCount: 20
lineCount: 4
- id: "9CWY"
@ -2938,7 +2938,7 @@ cases:
localPath: yaml-test-suite/VJP3/00/in.yaml
expected: reject
oracle: none
status: gap
status: passing
byteCount: 13
lineCount: 5
- id: "VJP3/01"