A document-end marker takes no node on its own line: only a comment or blank may follow. The scanner skipped the rest of the marker's line, so content such as the 'invalid' in '... invalid' was silently dropped and the document accepted. scanDocumentMarker now distinguishes the two markers after skipping spaces: a document start may carry the root node on its line and leaves it for the main loop, while a document end rejects any real content (a comment or blank still passes). A '...' not followed by an indicator boundary is not a marker at all and remains an ordinary plain scalar, so '...junk' still parses as content. Measured against the official YAML test-suite, parse-level conformance rises from 255/333 (76.6%) to 256/333 (76.9%) with no regression.
123 lines
5.1 KiB
Swift
123 lines
5.1 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanDirectiveOrDocumentMarker(_ state: inout State) throws -> Bool {
|
|
guard state.indentation.last == 0 else {
|
|
return false
|
|
}
|
|
if state.reader.peek() == "%" {
|
|
try scanDirective(&state)
|
|
return true
|
|
}
|
|
if state.reader.peek() == "#" {
|
|
return false
|
|
}
|
|
if startsWith("---", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
|
|
try scanDocumentMarker(.documentStart, state: &state)
|
|
resetForDocumentStart(&state)
|
|
return true
|
|
}
|
|
if startsWith("...", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
|
|
guard !state.isDocumentClosed else {
|
|
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line)
|
|
}
|
|
// A `...` ends a document, so a directive still awaiting its `---` never gets one.
|
|
try enforceDirectiveHasDocument(state)
|
|
try scanDocumentMarker(.documentEnd, state: &state)
|
|
resetForDocumentEnd(&state)
|
|
return true
|
|
}
|
|
guard !state.isDocumentClosed else {
|
|
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func scanDocumentMarker(
|
|
_ kind: PureYAML.Parsing.TokenKind,
|
|
state: inout State,
|
|
) throws {
|
|
let start = state.reader.mark
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
state.append(kind, mark: start, endMark: state.reader.mark)
|
|
while state.reader.peek() == " " {
|
|
state.reader.advance()
|
|
}
|
|
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
|
|
// The marker's own line carries real content. A document start may carry the root node
|
|
// here (e.g. `--- >`, `--- foo`, `--- {a: 1}`), so leave it for the main loop to scan.
|
|
if case .documentStart = kind {
|
|
state.isAtLineStart = false
|
|
return
|
|
}
|
|
// A document end (`...`) takes no same-line node; only a comment or blank may follow it.
|
|
throw PureYAML.Parsing.ParseError.contentAfterDocumentEnd(line: state.reader.mark.line)
|
|
}
|
|
skipLine(&state)
|
|
}
|
|
|
|
func resetForDocumentStart(_ state: inout State) {
|
|
state.hasDocumentContent = false
|
|
state.hasDocumentStartMarker = true
|
|
state.isDocumentClosed = false
|
|
state.hasYAMLDirective = false
|
|
state.pendingDirectiveLine = nil
|
|
}
|
|
|
|
/// A directive must be followed by an explicit `---` document start. Called at a document end
|
|
/// and at stream end, where a still-pending directive means none ever came.
|
|
func enforceDirectiveHasDocument(_ state: State) throws {
|
|
if let line = state.pendingDirectiveLine {
|
|
throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: line)
|
|
}
|
|
}
|
|
|
|
func resetForDocumentEnd(_ state: inout State) {
|
|
state.hasDocumentContent = false
|
|
state.hasDocumentStartMarker = false
|
|
state.isDocumentClosed = true
|
|
state.hasYAMLDirective = false
|
|
}
|
|
|
|
func scanDirective(_ state: inout State) throws {
|
|
let start = state.reader.mark
|
|
let line = state.reader.consume { character in
|
|
!isLineBreak(character)
|
|
}
|
|
let parts = line.split(separator: " ").map(String.init)
|
|
guard let name = parts.first else {
|
|
return
|
|
}
|
|
guard !state.hasDocumentContent, !state.hasDocumentStartMarker, !state.isDocumentClosed else {
|
|
throw PureYAML.Parsing.ParseError.unsupportedDirective(name: name, line: start.line)
|
|
}
|
|
// Every directive must be followed by an explicit `---`; record that one is now awaiting it.
|
|
state.pendingDirectiveLine = start.line
|
|
switch name {
|
|
case "%YAML":
|
|
guard parts.indices.contains(1), parts[1] == "1.1" || parts[1] == "1.2" else {
|
|
throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line)
|
|
}
|
|
// `%YAML` takes exactly one parameter, the version; only a comment may follow it.
|
|
guard parts.count == 2 || parts[2].hasPrefix("#") else {
|
|
throw PureYAML.Parsing.ParseError.malformedDirective(name: name, line: start.line)
|
|
}
|
|
// At most one `%YAML` directive is allowed per document.
|
|
guard !state.hasYAMLDirective else {
|
|
throw PureYAML.Parsing.ParseError.duplicateDirective(name: name, line: start.line)
|
|
}
|
|
state.hasYAMLDirective = true
|
|
case "%TAG":
|
|
guard parts.count >= 3 else {
|
|
return
|
|
}
|
|
state.tagHandles[parts[1]] = parts[2]
|
|
default:
|
|
// A YAML processor ignores a directive it does not recognize (the spec calls for a
|
|
// warning and otherwise no effect), rather than rejecting the document. The line has
|
|
// already been consumed, so there is nothing more to do.
|
|
return
|
|
}
|
|
}
|
|
}
|