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.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 21:50:41 +02:00
parent 57adb14f15
commit c236945323
5 changed files with 32 additions and 7 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()
}
}
}
}

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: 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

View File

@ -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)