PureYAML/Sources/Parsing/Scanner.Tabs.swift
Mihaela Mihaljevic 1de1fcf2a8 fix(parsing): reject invalid input that was wrongly accepted
Tighten error detection per the YAML 1.2 spec across four rules: (1) a tab may never supply block-structure indentation (line-start indentation, block-scalar indentation, and separation before a nested block node are rejected; a tab separating an indicator from a scalar value stays valid); (2) a ---/... document marker inside an open quoted scalar or flow collection is rejected instead of silently ending the document; (3) a %YAML/%TAG line that opens a second explicit document with no intervening ... is rejected; (4) an anchor cannot be attached to an alias node.

This repairs 4 of the 5 error-detection regressions introduced by buckets 3-5 (MUS6/01, N782, SU74, Y79Y/009); 9MQT/01 is deferred (its invalidity is in same-line content after the --- marker, which needs document-content scanning that risks the baseline). Rejects 13 accepted-invalid suite cases and fixes 2 more by value (6BCT, Y79Y/010); 305/402 passing.

BEHAVIOR CHANGE: tab-indented block structure (previously parsed permissively) is now rejected per spec; no real-world corpus seed relied on this. Updates the manifest, fixtures, counts, two affected tests (with spec justification), and the roadmap (bucket 6 marked PARTIAL).
2026-06-23 22:13:35 +02:00

63 lines
2.9 KiB
Swift

extension PureYAML.Parsing.Scanner {
/// Rejects a tab that appears in separation position only when it supplies
/// indentation for a nested block node. YAML 1.2 allows a tab as in-line
/// separation (s-white) before a plain or quoted scalar value, but never as
/// indentation (s-indent). The two are distinguished by what follows the tab:
/// a block-entry `-`, an explicit-key `?`, or a scalar that is itself a mapping
/// key (a compact nested mapping) all start a nested block node whose column the
/// tab would be indenting, so they are rejected; a plain scalar *value* is
/// ordinary separation and is allowed.
///
/// Examples (yaml-test-suite): `-<TAB>-1` and `foo:<TAB> bar` are valid (scalar
/// value follows), while `-<TAB>-`, `?<TAB>key:`, and `:<TAB>key:` are rejected
/// (a nested indicator or key follows).
func rejectTabSeparation(_ state: inout State) throws {
guard state.flowDepth == 0, state.simpleKeyAllowed, state.reader.peek() == "\t" else {
return
}
if tabSeparationPrecedesNestedBlockNode(state) {
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
}
}
/// True when, after the tab(s) and spaces under the reader, the next token starts
/// a nested block node (a `-` block entry, a `?` explicit key, or a plain scalar
/// immediately followed by a `:` mapping-value indicator).
private func tabSeparationPrecedesNestedBlockNode(_ state: State) -> Bool {
var offset = 0
while state.reader.peek(offset: offset) == "\t" || state.reader.peek(offset: offset) == " " {
offset += 1
}
guard let character = state.reader.peek(offset: offset) else {
return false
}
if character == "-", isIndicatorBoundary(state.reader.peek(offset: offset + 1)) {
return true
}
if character == "?", isIndicatorBoundary(state.reader.peek(offset: offset + 1)) {
return true
}
return plainScalarIsMappingKey(in: state, startingAt: offset)
}
/// Scans a prospective plain scalar starting at `offset` (in lookahead only) and
/// reports whether it is immediately followed by a `:` block mapping-value
/// indicator, i.e. the scalar is a nested mapping key rather than a value.
private func plainScalarIsMappingKey(in state: State, startingAt offset: Int) -> Bool {
var cursor = offset
while let character = state.reader.peek(offset: cursor) {
if isLineBreak(character) {
return false
}
if character == ":", isBlockMappingValueBoundary(state.reader.peek(offset: cursor + 1)) {
return true
}
if character == " ", state.reader.peek(offset: cursor + 1) == "#" {
return false
}
cursor += 1
}
return false
}
}