From 2e9975e977c4feceb8fd7d4da97540125d9999a8 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 20:00:21 +0200 Subject: [PATCH] fix(parsing): reject a comment with no preceding whitespace A '#' starts a comment only after a space or tab, or at the start of a line. A '#' adjacent to the previous token is not a comment: 'key: "v"#c', '[ a ]#c', '[ a,#c', and a '>#c' block-scalar header are malformed, but the port (like libyaml) accepted them. Validate the character immediately before the '#' (a plain scalar already consumes its own trailing blanks, so an adjacent '#' the next-token scan sees genuinely had no separator). Applied in scan_to_next_token and the block-scalar header tail. Surpasses libyaml on 9JBA, CVW2, SU5Z, X4QW; whole-suite conformance 388/402. Canonical event stream stays 308/308. --- CHANGELOG.md | 7 +++++ .../Parsing/LibYAML.Scanner.BlockScalar.swift | 5 ++++ Sources/Parsing/LibYAML.Scanner.Fetch.swift | 26 +++++++++++++++---- Tests/Fixtures/yaml-test-suite-manifest.yaml | 12 ++++----- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index defb752..6430b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Reject a comment that is not preceded by whitespace. A `#` only begins a comment + after a space, a tab, or at the start of a line; a `#` adjacent to the previous + token (`key: "value"# c`, `[ a ]#c`, `[ a,#c`, a `>#c` block scalar header) is + malformed. The check uses the character immediately before the `#`, since a plain + scalar consumes its own trailing blanks. libyaml accepts the adjacent form. + Surpasses libyaml on 9JBA, CVW2, SU5Z, and X4QW. Whole-suite conformance is now + 388/402. - Reject a directive that follows a document without an explicit `...` end marker. Directives apply to the document that follows, so the previous document must be closed with `...` before them; after an implicit end a `%YAML`/`%TAG`/reserved diff --git a/Sources/Parsing/LibYAML.Scanner.BlockScalar.swift b/Sources/Parsing/LibYAML.Scanner.BlockScalar.swift index 20974fa..8547778 100644 --- a/Sources/Parsing/LibYAML.Scanner.BlockScalar.swift +++ b/Sources/Parsing/LibYAML.Scanner.BlockScalar.swift @@ -96,6 +96,11 @@ extension PureYAML.Parsing.LibYAML.Scanner { reader.skip() } if reader.peek() == "#" { + // A comment needs a preceding space, so `>#` (a `#` adjacent to the + // block scalar indicator) is malformed, not a comment. libyaml accepts it. + if reader.column > 0, let previous = reader.peek(-1), !Classify.isBlank(previous) { + throw blockError("found a comment without a preceding space", startMark) + } while !Classify.isBreakZ(reader.peek()) { reader.skip() } diff --git a/Sources/Parsing/LibYAML.Scanner.Fetch.swift b/Sources/Parsing/LibYAML.Scanner.Fetch.swift index 34e239f..5e8bcc7 100644 --- a/Sources/Parsing/LibYAML.Scanner.Fetch.swift +++ b/Sources/Parsing/LibYAML.Scanner.Fetch.swift @@ -19,7 +19,7 @@ extension PureYAML.Parsing.LibYAML.Scanner { /// the block context. A tab is valid in-line separation, so it is skipped /// here; `tabbedSeparation` records when one preceded the token so a block /// indicator or implicit key (where the tab would be indentation) can reject. - private mutating func scanToNextToken() { + private mutating func scanToNextToken() throws { while true { tabbedSeparation = false if reader.column == 0, reader.peek() == "\u{FEFF}" { @@ -41,9 +41,7 @@ extension PureYAML.Parsing.LibYAML.Scanner { } } if reader.peek() == "#" { - while !Classify.isBreakZ(reader.peek()) { - reader.skip() - } + try consumeComment() } if Classify.isBreak(reader.peek()) { reader.skipLine() @@ -56,6 +54,24 @@ extension PureYAML.Parsing.LibYAML.Scanner { } } + /// Consume a comment to the end of the line. A comment must be preceded by + /// whitespace or start the line; a `#` adjacent to the previous token (e.g. + /// `"x"#`, `]#`, `,#`) is not a comment. The plain-scalar scanner consumes its + /// own trailing blanks, so the character just before the `#` is the reliable + /// signal. libyaml accepts the adjacent form. + private mutating func consumeComment() throws { + if reader.column > 0, let previous = reader.peek(-1), !Classify.isBlank(previous) { + throw ScannerError( + context: "while scanning for the next token", + problem: "found a comment without a preceding space", + mark: reader.mark, + ) + } + while !Classify.isBreakZ(reader.peek()) { + reader.skip() + } + } + /// Port of `yaml_parser_fetch_next_token`: emit STREAM-START first, otherwise /// skip to the next token, expire stale simple keys, unroll the indentation to /// the current column, and dispatch on the leading character. Constructs not @@ -66,7 +82,7 @@ extension PureYAML.Parsing.LibYAML.Scanner { return } - scanToNextToken() + try scanToNextToken() mark = reader.mark try staleSimpleKeys() indentEngine.unrollIndent(column: reader.column, mark: reader.mark) diff --git a/Tests/Fixtures/yaml-test-suite-manifest.yaml b/Tests/Fixtures/yaml-test-suite-manifest.yaml index d75ec62..3fdaa42 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: 384 -gap: 18 +passing: 388 +gap: 14 cases: - id: "229Q" name: "Spec Example 2.4. Sequence of Mappings" @@ -1034,7 +1034,7 @@ cases: localPath: yaml-test-suite/9JBA/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 25 lineCount: 2 - id: "9KAX" @@ -1354,7 +1354,7 @@ cases: localPath: yaml-test-suite/CVW2/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 25 lineCount: 3 - id: "CXX2" @@ -2722,7 +2722,7 @@ cases: localPath: yaml-test-suite/SU5Z/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 30 lineCount: 1 - id: "SU74" @@ -3002,7 +3002,7 @@ cases: localPath: yaml-test-suite/X4QW/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 27 lineCount: 2 - id: "X8DW"