fix(parsing): reject empty entries in flow collections
In a flow sequence or mapping a comma separates entries, so one must
precede it. The parser advanced past a comma without checking that an
entry came before, accepting a leading comma ([ , a ]) or a repeated comma
([ a, , b ]) as empty entries. Each flow loop now tracks whether an entry
precedes the comma and rejects it otherwise. A single trailing comma
([ a, b, ]) and empty collections ([], {}) remain valid, since the closing
bracket is handled before that check.
Measured against the official YAML test-suite, parse-level conformance
rises from 249/333 (74.8%) to 251/333 (75.4%) with no regression.
This commit is contained in:
parent
b7bcfe1a01
commit
97584dc57c
@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A flow collection no longer accepts an empty entry. A comma separates entries, so one must
|
||||
precede it; a leading comma (`[ , a ]`), a repeated comma (`[ a, , b ]`), or the same in a flow
|
||||
mapping is now rejected, while a single trailing comma (`[ a, b, ]`) and empty collections
|
||||
(`[]`, `{}`) stay valid. The parser previously skipped a comma without checking that an entry
|
||||
came before it. Lifts official test-suite parse-level conformance from 249/333 (74.8%) to
|
||||
251/333 (75.4%), with no regression.
|
||||
- Double-quoted escape sequences now follow the full YAML 1.2 escape table. Previously the
|
||||
decoder recognized only a subset (`\" \\ \/ \b \f \n \r \t \u`) and silently passed any other
|
||||
character through, so invalid escapes such as `\.` and `\'` were accepted while valid ones such
|
||||
|
||||
@ -16,6 +16,7 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
style: .flow,
|
||||
mark: startMark,
|
||||
))
|
||||
var hasEntry = false
|
||||
while cursor.current != nil {
|
||||
if case .flowSequenceEnd = cursor.current?.kind {
|
||||
let end = try expect("flow sequence end") { kind in
|
||||
@ -29,10 +30,18 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
|
||||
}
|
||||
if case .flowEntry = cursor.current?.kind {
|
||||
// A comma separates entries, so one must precede it: a leading or repeated comma
|
||||
// is an empty entry, which is not allowed. A single trailing comma before the
|
||||
// closing bracket is fine, since the close is handled above regardless of `hasEntry`.
|
||||
guard hasEntry else {
|
||||
throw unexpectedToken(expected: "flow sequence entry or flow sequence end")
|
||||
}
|
||||
hasEntry = false
|
||||
_ = try cursor.advance()
|
||||
continue
|
||||
}
|
||||
endMark = try parseNode().endMark
|
||||
hasEntry = true
|
||||
if cursor.current?.kind.isFlowTerminator == false {
|
||||
throw unexpectedToken(expected: "flow entry or flow sequence end")
|
||||
}
|
||||
@ -57,6 +66,7 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
style: .flow,
|
||||
mark: startMark,
|
||||
))
|
||||
var hasEntry = false
|
||||
while cursor.current != nil {
|
||||
if case .flowMappingEnd = cursor.current?.kind {
|
||||
let end = try expect("flow mapping end") { kind in
|
||||
@ -70,10 +80,17 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
||||
}
|
||||
if case .flowEntry = cursor.current?.kind {
|
||||
// As in a flow sequence, a comma separates entries and so requires one before it;
|
||||
// a leading or repeated comma is an empty entry. A trailing comma is fine.
|
||||
guard hasEntry else {
|
||||
throw unexpectedToken(expected: "flow mapping entry or flow mapping end")
|
||||
}
|
||||
hasEntry = false
|
||||
_ = try cursor.advance()
|
||||
continue
|
||||
}
|
||||
endMark = try parseFlowMappingPair().endMark
|
||||
hasEntry = true
|
||||
if cursor.current?.kind.isFlowTerminator == false {
|
||||
throw unexpectedToken(expected: "flow entry or flow mapping end")
|
||||
}
|
||||
|
||||
25
Tests/FlowEmptyEntryTests.swift
Normal file
25
Tests/FlowEmptyEntryTests.swift
Normal file
@ -0,0 +1,25 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Flow collections reject empty entries")
|
||||
struct FlowEmptyEntryTests {
|
||||
@Test("A leading or repeated comma is an empty entry and is rejected", arguments: [
|
||||
"[ , a, b, c ]\n", // leading comma in a sequence
|
||||
"[ a, b, c, , ]\n", // repeated comma in a sequence
|
||||
"{ , a: 1 }\n", // leading comma in a mapping
|
||||
"{ a: 1, , b: 2 }\n", // repeated comma in a mapping
|
||||
])
|
||||
func test_emptyEntryIsRejected(_ yaml: String) throws {
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A single trailing comma and empty collections stay valid")
|
||||
func test_trailingCommaAndEmptyCollectionsAccepted() throws {
|
||||
#expect(try PureYAML.parse("[ a, b, ]\n") == .sequence(.init([.string("a"), .string("b")])))
|
||||
#expect(try PureYAML.parse("[]\n") == .sequence(.init([])))
|
||||
#expect(try PureYAML.parse("{ a: 1, }\n") == .mapping(.init([.init(key: "a", value: .int(1))])))
|
||||
#expect(try PureYAML.parse("{}\n") == .mapping(.init([])))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user