From eee7079efe49228834475bb8f2c39ede9c2cb294 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 20:45:12 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 6 +++++ Sources/Parsing/LibYAML.Scanner.Fetch.swift | 23 +++++++++++++++++++ .../Parsing/LibYAML.Scanner.SkipToToken.swift | 2 ++ Sources/Parsing/LibYAML.Scanner.swift | 4 ++++ Tests/Fixtures/yaml-test-suite-manifest.yaml | 8 +++---- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf1bdc7..d4eb133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Parsing/LibYAML.Scanner.Fetch.swift b/Sources/Parsing/LibYAML.Scanner.Fetch.swift index c0622fc..8f92dbc 100644 --- a/Sources/Parsing/LibYAML.Scanner.Fetch.swift +++ b/Sources/Parsing/LibYAML.Scanner.Fetch.swift @@ -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. diff --git a/Sources/Parsing/LibYAML.Scanner.SkipToToken.swift b/Sources/Parsing/LibYAML.Scanner.SkipToToken.swift index 27aaab0..654c370 100644 --- a/Sources/Parsing/LibYAML.Scanner.SkipToToken.swift +++ b/Sources/Parsing/LibYAML.Scanner.SkipToToken.swift @@ -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 } diff --git a/Sources/Parsing/LibYAML.Scanner.swift b/Sources/Parsing/LibYAML.Scanner.swift index 845715e..290e24c 100644 --- a/Sources/Parsing/LibYAML.Scanner.swift +++ b/Sources/Parsing/LibYAML.Scanner.swift @@ -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`. diff --git a/Tests/Fixtures/yaml-test-suite-manifest.yaml b/Tests/Fixtures/yaml-test-suite-manifest.yaml index 8b307c1..64c49be 100644 --- a/Tests/Fixtures/yaml-test-suite-manifest.yaml +++ b/Tests/Fixtures/yaml-test-suite-manifest.yaml @@ -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"