diff --git a/CHANGELOG.md b/CHANGELOG.md index c8666a6..f271850 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 an under-indented or tab-indented continuation in a multi-line quoted + scalar. A quoted scalar's continuation must be indented past the parent block, so + `"a`\n`b"` (continuation in column 0) and `"bar`\n`\tbaz"` (tab indentation) are + malformed. The error is deferred until the closing quote is found, so an + unterminated quote still reports the missing quote (its root cause) rather than + the indentation symptom. Surpasses libyaml on QB6E and DK95/01, bringing + whole-suite conformance to 402/402. - Reject a tab used to indent flow-collection content. A tab is never indentation, so flow content reached through a tab below the continuation indent (`- [`\n `\tfoo`) is malformed, even though the tab inflates the column. A tab at or past diff --git a/Sources/Parsing/LibYAML.Scanner.FlowScalar.swift b/Sources/Parsing/LibYAML.Scanner.FlowScalar.swift index 992fb6c..d4f6f04 100644 --- a/Sources/Parsing/LibYAML.Scanner.FlowScalar.swift +++ b/Sources/Parsing/LibYAML.Scanner.FlowScalar.swift @@ -28,6 +28,11 @@ extension PureYAML.Parsing.LibYAML.Scanner { reader.skip() var state = FlowState() let quote: Unicode.Scalar = single ? "'" : "\"" + let indent = indentEngine.indent + 1 + // A continuation indented below `indent` is an error, but it is recorded + // and only raised once the quote is known to close; an unterminated quote + // reaches end of input first and reports that instead. + var indentError: PureYAML.Parsing.LibYAML.ScannerError? while true { if reader.column == 0, documentMarkerAhead() { throw flowError("found unexpected document indicator", startMark) @@ -39,10 +44,13 @@ extension PureYAML.Parsing.LibYAML.Scanner { if reader.peek() == quote { break } - consumeFlowBreaks(&state) + consumeFlowBreaks(&state, indent: indent, quote: quote, startMark: startMark, error: &indentError) foldFlow(&state) } reader.skip() + if let indentError { + throw indentError + } return PureYAML.Parsing.LibYAML.Token( type: .scalar(value: state.string, style: single ? .singleQuoted : .doubleQuoted), startMark: startMark, @@ -139,9 +147,18 @@ extension PureYAML.Parsing.LibYAML.Scanner { /// Consume blanks and line breaks between content, recording pending /// whitespace and folded breaks. - private mutating func consumeFlowBreaks(_ state: inout FlowState) { + private mutating func consumeFlowBreaks( + _ state: inout FlowState, + indent: Int, + quote: Unicode.Scalar, + startMark: PureYAML.Parsing.Mark, + error: inout PureYAML.Parsing.LibYAML.ScannerError?, + ) { while Classify.isBlank(reader.peek()) || Classify.isBreak(reader.peek()) { if Classify.isBlank(reader.peek()) { + if error == nil, state.leadingBlanks, reader.column < indent, Classify.isTab(reader.peek()), !blankLineAhead() { + error = flowError("found a tab character that violates indentation", startMark) + } if !state.leadingBlanks, let blank = reader.peek() { state.whitespaces.unicodeScalars.append(blank) } @@ -156,6 +173,11 @@ extension PureYAML.Parsing.LibYAML.Scanner { state.trailingBreaks += "\n" } } + // The continuation content must be indented at least `indent`; a less + // indented (non-closing, non-empty) line is malformed. + if error == nil, state.leadingBlanks, let next = reader.peek(), next != quote, !Classify.isBreakZ(next), reader.column < indent { + error = flowError("found wrong indentation in a multi-line quoted scalar", startMark) + } } /// Fold the pending breaks or whitespace into the value. diff --git a/Tests/Fixtures/yaml-test-suite-manifest.yaml b/Tests/Fixtures/yaml-test-suite-manifest.yaml index 4053908..aec0eda 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: 400 -gap: 2 +passing: 402 +gap: 0 cases: - id: "229Q" name: "Spec Example 2.4. Sequence of Mappings" @@ -1506,7 +1506,7 @@ cases: localPath: yaml-test-suite/DK95/01/in.yaml expected: reject oracle: json - status: gap + status: passing byteCount: 16 lineCount: 2 - id: "DK95/02" @@ -2506,7 +2506,7 @@ cases: localPath: yaml-test-suite/QB6E/in.yaml expected: reject oracle: none - status: gap + status: passing byteCount: 20 lineCount: 4 - id: "QF4Y"