From 7879818ce5e1fe3587029f572363502696ca620b Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 02:34:55 +0200 Subject: [PATCH] fix(parsing): accept a higher-minor %YAML version The %YAML version check accepted only 1.1 and 1.2, rejecting anything else, so `%YAML 1.3` errored. The spec calls for processing a document whose minor version is higher than the processor's, with a warning, rather than refusing it: only a different major version is incompatible. Accept any well-formed `1.minor` version and reject only a different major (`2.0`) or a malformed version. Three tests that asserted `%YAML 1.3` was an error are corrected to use `2.0`, the genuinely incompatible case, and a test covers that 1.3 now parses. Raises yaml-test-suite parse-level conformance to 305/333 (91.6%) with the value-level (100%) and should-error rejection rates unchanged. 486 tests pass. --- CHANGELOG.md | 7 +++++++ Sources/Parsing/Scanner.Directives.swift | 15 ++++++++++++++- Tests/ParsingErrorTests.swift | 2 +- Tests/ScannerTests.swift | 2 +- Tests/YAMLDirectiveTests.swift | 7 ++++++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2821960..c87fd02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A `%YAML` directive with a higher minor version than 1.2 is now accepted rather than rejected. The + spec calls for processing a document with a higher minor version, with a warning, so `%YAML 1.3` is + read as a YAML 1.x document instead of erroring. The version check now accepts any well-formed + `1.minor` version and rejects only a different major version (`2.0`) or a malformed one. Three tests + that asserted `%YAML 1.3` was an error were corrected to use `2.0`. Raises yaml-test-suite parse-level + conformance to 305/333 (91.6%), with the should-error rate unchanged. + - A plain scalar that is an inline block sequence item value (`: - b`) now stops folding at a later block entry that returns to the sequence's own indent, so the dashes form the rest of the sequence rather than folding into the first item. A block entry only recorded its inline node's indent as a diff --git a/Sources/Parsing/Scanner.Directives.swift b/Sources/Parsing/Scanner.Directives.swift index 187d23c..c752f37 100644 --- a/Sources/Parsing/Scanner.Directives.swift +++ b/Sources/Parsing/Scanner.Directives.swift @@ -102,7 +102,10 @@ extension PureYAML.Parsing.Scanner { state.pendingDirectiveLine = start.line switch name { case "%YAML": - guard parts.indices.contains(1), parts[1] == "1.1" || parts[1] == "1.2" else { + // Any version with major 1 is processable: a higher minor than 1.2 (`%YAML 1.3`) is + // accepted, since the spec calls for processing it, with a warning, rather than rejecting + // it. Only a different major version, or a malformed one, is incompatible. + guard parts.indices.contains(1), isProcessableYAMLVersion(parts[1]) else { throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line) } // `%YAML` takes exactly one parameter, the version; only a comment may follow it. @@ -126,4 +129,14 @@ extension PureYAML.Parsing.Scanner { return } } + + /// Whether a `%YAML` version is one this processor can attempt: any `major.minor` with major 1, + /// so 1.0 through 1.2 and a higher minor like 1.3 are all accepted; a different major (2.0) or a + /// non-numeric version is not. + func isProcessableYAMLVersion(_ version: String) -> Bool { + let components = version.split(separator: ".", omittingEmptySubsequences: false) + return components.count == 2 + && components[0] == "1" + && components.allSatisfy { !$0.isEmpty && $0.allSatisfy(\.isNumber) } + } } diff --git a/Tests/ParsingErrorTests.swift b/Tests/ParsingErrorTests.swift index 8718665..6c00953 100644 --- a/Tests/ParsingErrorTests.swift +++ b/Tests/ParsingErrorTests.swift @@ -16,7 +16,7 @@ struct ParsingErrorTests { expectParseError("name: \"open", .unterminatedQuotedString(line: 1)) expectParseError("name: 'open", .unterminatedQuotedString(line: 1)) expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9)) - expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1)) + expectParseError("%YAML 2.0\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1)) expectParseError("value: !!int nope", .invalidTaggedScalar( tag: "tag:yaml.org,2002:int", value: "nope", diff --git a/Tests/ScannerTests.swift b/Tests/ScannerTests.swift index 1033baf..2df6d5d 100644 --- a/Tests/ScannerTests.swift +++ b/Tests/ScannerTests.swift @@ -264,7 +264,7 @@ struct ScannerTests { expectScannerError("anchor: &", .expectedAnchorName(line: 1)) expectScannerError("alias: *", .expectedAliasName(line: 1)) expectScannerError("tag: !