diff --git a/CHANGELOG.md b/CHANGELOG.md index b83d1fe..f4e8fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/TokenEventParser.Flow.swift b/Sources/Parsing/TokenEventParser.Flow.swift index 6291315..a983d99 100644 --- a/Sources/Parsing/TokenEventParser.Flow.swift +++ b/Sources/Parsing/TokenEventParser.Flow.swift @@ -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") } diff --git a/Tests/FlowEmptyEntryTests.swift b/Tests/FlowEmptyEntryTests.swift new file mode 100644 index 0000000..cbc1576 --- /dev/null +++ b/Tests/FlowEmptyEntryTests.swift @@ -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([]))) + } +}