From c236945323c6f8ec415be269e8963eb321a3501e Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 21:50:41 +0200 Subject: [PATCH] fix(parsing): reject a multi-line implicit key in a flow sequence A flow-sequence single-pair implicit key is single-line (c-s-implicit-json-key uses s-separate-in-line), so its ':' may not cross a line break: '[ "key"\n :value ]' is malformed. A flow MAPPING adjacent value may still span lines ('{ "foo"\n :bar }' stays valid), which is why the check is scoped to flow levels that are sequences. Track per-flow-level whether it is a sequence ('[' vs '{') in a flowIsSequence stack pushed/popped with the flow level, and in fetchValue reject a pending simple key whose ':' crossed a line break when the current flow level is a sequence. Surpasses libyaml on ZXT5 (5MUD, the flow-mapping analogue, stays valid); whole-suite conformance 397/402, canonical events 308/308. --- CHANGELOG.md | 6 ++++++ Sources/Parsing/LibYAML.Scanner.Fetch.swift | 11 ++++++++++- Sources/Parsing/LibYAML.Scanner.swift | 13 +++++++++++-- Tests/Fixtures/yaml-test-suite-manifest.yaml | 6 +++--- Tests/LibYAMLSimpleKeyTests.swift | 3 ++- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c9367..5146672 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 multi-line implicit key in a flow sequence. A flow-sequence single-pair + implicit key is single-line (`c-s-implicit-json-key`), so its `:` may not cross a + line break: `[ "key"\n :value ]` is malformed. A flow MAPPING adjacent value may + still span lines (`{ "foo"\n :bar }` stays valid), so the rejection is scoped to + flow levels that are sequences, tracked alongside the flow-level stack. Surpasses + libyaml on ZXT5. - Reject a block sequence that opens on the same line as an earlier node. A `-` entry mid-line after an inline mapping value (`key: - a`) or a node property (`&anchor - x`) is invalid, while a `-` after another `-` stays a valid compact diff --git a/Sources/Parsing/LibYAML.Scanner.Fetch.swift b/Sources/Parsing/LibYAML.Scanner.Fetch.swift index 0a28372..becfe59 100644 --- a/Sources/Parsing/LibYAML.Scanner.Fetch.swift +++ b/Sources/Parsing/LibYAML.Scanner.Fetch.swift @@ -188,7 +188,7 @@ extension PureYAML.Parsing.LibYAML.Scanner { if saved { simpleKeys[simpleKeys.count - 1].isJSON = true } - increaseFlowLevel() + increaseFlowLevel(isSequence: type == .flowSequenceStart) simpleKeyAllowed = true let startMark = reader.mark reader.skip() @@ -313,6 +313,15 @@ extension PureYAML.Parsing.LibYAML.Scanner { // and `:` on one line). An explicit `?` key's `:` has no pending simple key // (it went stale across the line break), so its value may open a sequence. lastValueWasInline = simpleKeys[top].possible + // A flow-sequence single-pair implicit key must be single-line, so its `:` + // may not cross a line break; a flow-mapping adjacent value may. + if simpleKeys[top].possible, crossedLineBreak, flowIsSequence.last == true { + throw ScannerError( + context: "while scanning a simple key", + problem: "could not find expected ':'", + mark: simpleKeys[top].mark, + ) + } if simpleKeys[top].possible { if simpleKeys[top].tabbed { throw tabIndentationError() diff --git a/Sources/Parsing/LibYAML.Scanner.swift b/Sources/Parsing/LibYAML.Scanner.swift index abc0344..dcc4b95 100644 --- a/Sources/Parsing/LibYAML.Scanner.swift +++ b/Sources/Parsing/LibYAML.Scanner.swift @@ -27,6 +27,11 @@ extension PureYAML.Parsing.LibYAML { /// sequence on that line (`key: - a`), whereas an explicit key whose `:` is /// on a later line may (`? k`\n`: - a`). var lastValueWasInline = false + /// For each open flow level, whether it is a sequence (`[`) rather than a + /// mapping (`{`). A flow-sequence single-pair implicit key is single-line + /// (`c-s-implicit-json-key`), so its `:` may not cross a line break, unlike + /// a flow-mapping adjacent value (`{ "a"`\n`: b }` is allowed). + var flowIsSequence: [Bool] = [] /// `parser->mark`, kept in sync with the reader at each fetch step. var mark: PureYAML.Parsing.Mark = .start /// `parser->stream_start_produced`. @@ -108,9 +113,10 @@ extension PureYAML.Parsing.LibYAML { } /// Port of `yaml_parser_increase_flow_level`. Push an empty simple key for - /// the new flow level. - mutating func increaseFlowLevel() { + /// the new flow level, recording whether the level is a sequence. + mutating func increaseFlowLevel(isSequence: Bool) { simpleKeys.append(.empty) + flowIsSequence.append(isSequence) indentEngine.flowLevel += 1 } @@ -120,6 +126,9 @@ extension PureYAML.Parsing.LibYAML { if indentEngine.flowLevel > 0 { indentEngine.flowLevel -= 1 simpleKeys.removeLast() + if !flowIsSequence.isEmpty { + flowIsSequence.removeLast() + } } } } diff --git a/Tests/Fixtures/yaml-test-suite-manifest.yaml b/Tests/Fixtures/yaml-test-suite-manifest.yaml index bf861bb..73a15f6 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: 396 -gap: 6 +passing: 397 +gap: 5 cases: - id: "229Q" name: "Spec Example 2.4. Sequence of Mappings" @@ -3226,6 +3226,6 @@ cases: localPath: yaml-test-suite/ZXT5/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 19 lineCount: 2 diff --git a/Tests/LibYAMLSimpleKeyTests.swift b/Tests/LibYAMLSimpleKeyTests.swift index 2f397d9..33e124d 100644 --- a/Tests/LibYAMLSimpleKeyTests.swift +++ b/Tests/LibYAMLSimpleKeyTests.swift @@ -56,9 +56,10 @@ struct LibYAMLSimpleKeyTests { func test_flowLevelStack() { var scanner = PureYAML.Parsing.LibYAML.Scanner() #expect(scanner.simpleKeys.count == 1) - scanner.increaseFlowLevel() + scanner.increaseFlowLevel(isSequence: true) #expect(scanner.flowLevel == 1) #expect(scanner.simpleKeys.count == 2) + #expect(scanner.flowIsSequence == [true]) scanner.decreaseFlowLevel() #expect(scanner.flowLevel == 0) #expect(scanner.simpleKeys.count == 1)