Fix three should-parse conformance gaps: a repeated document-end marker (M7A3), a root block scalar ended by a document marker (W4TN), and an empty explicit key in a flow mapping (DFF7). Threaded the located parse end mark through the new explicit-key emit. Parse-level suite conformance reaches 318/333.
168 lines
7.9 KiB
Swift
168 lines
7.9 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanDirectiveOrDocumentMarker(_ state: inout State) throws -> Bool {
|
|
guard state.indentation.last == 0 else {
|
|
return false
|
|
}
|
|
// A `%` opens a directive only outside a block scalar. Inside one (a zero-indent literal or
|
|
// folded scalar sits at column 0, where this directive check runs) a leading `%` is literal
|
|
// content, so let the main scanner read the line. Elsewhere the directive path still runs, so
|
|
// a stray `%` in other content is rejected as before, not silently accepted.
|
|
if state.reader.peek() == "%", state.blockScalarIndentation == nil {
|
|
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)) {
|
|
// A `...` after a document is already closed (only comments or blank lines since the
|
|
// last `...`) ends nothing: it is a repeated document-end marker, which YAML allows. Skip
|
|
// it as a no-op rather than emitting a second documentEnd (which would compose a phantom
|
|
// null document) or rejecting a valid stream. Content after it is still an error.
|
|
if state.isDocumentClosed {
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
state.reader.advance()
|
|
while state.reader.peek() == " " {
|
|
state.reader.advance()
|
|
}
|
|
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
|
|
throw PureYAML.Parsing.ParseError.contentAfterDocumentEnd(line: state.reader.mark.line)
|
|
}
|
|
skipLine(&state)
|
|
return true
|
|
}
|
|
// 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
|
|
}
|
|
// Content after a document-end marker (`...`) begins a new bare document, so reopen the
|
|
// stream rather than rejecting it.
|
|
if state.isDocumentClosed {
|
|
state.isDocumentClosed = false
|
|
}
|
|
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
|
|
// The directives that preceded this `---` apply to this document only: merge them onto the
|
|
// defaults, then clear them so they do not leak into the next document.
|
|
state.tagHandles = State.defaultTagHandles.merging(state.pendingTagHandles) { _, new in new }
|
|
state.pendingTagHandles = [:]
|
|
}
|
|
|
|
/// 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
|
|
// A document boundary ends the scope of that document's tag handles.
|
|
state.tagHandles = State.defaultTagHandles
|
|
state.pendingTagHandles = [:]
|
|
}
|
|
|
|
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
|
|
}
|
|
// A directive after a document-end marker (`...`) begins the next document's directive
|
|
// section, so it reopens the stream for a new document rather than being rejected.
|
|
if state.isDocumentClosed {
|
|
state.isDocumentClosed = false
|
|
}
|
|
guard !state.hasDocumentContent, !state.hasDocumentStartMarker 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":
|
|
// 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.
|
|
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.pendingTagHandles[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
|
|
}
|
|
}
|
|
|
|
/// 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) }
|
|
}
|
|
}
|