extension PureYAML.Parsing.LibYAML { /// Port of libyaml's input buffer and position tracking (the `buffer`/`mark` /// fields with the `PEEK`, `SKIP`, and `SKIP_LINE` macros). It iterates /// `Unicode.Scalar`s rather than grapheme clusters so a CR and an LF are /// distinct, matching libyaml's byte-oriented scanning, and tracks the mark in /// libyaml's 0-based line and column convention; conversion to the package's /// 1-based `Mark` happens at token emission during integration. struct Reader { private var scalars: [Unicode.Scalar] private(set) var index = 0 private(set) var line = 0 private(set) var column = 0 /// Whether the buffered `scalars` are the complete input. True for every /// batch caller (the default), so `ranDry` can never be set and this type's /// behavior is unchanged for `scanAll()`. A resumable caller passes `false` /// and later calls `markFinal()` once the last chunk has been appended. private(set) var isFinal: Bool /// Set by `peek(_:)` when it was asked for a position past the buffered /// scalars while `!isFinal`: the query is inconclusive, not a real end of /// input, and the caller must suspend and retry after `append(_:)`. Batch /// callers (`isFinal == true`) never set this, since the guard below is /// unreachable when `isFinal` is true. private(set) var ranDry = false /// The index of the first non-`c-printable` scalar found in `scalars` /// (libyaml's reader-level check, `yaml_parser_update_buffer`'s printable /// validation), or nil if none has been found yet. Set once, by whichever /// of `init`/`append(_:)` first buffers the offending scalar; never /// cleared. `fetchNextToken()` checks this once per token and throws, /// mirroring libyaml rejecting raw control characters (anything outside /// `c-printable`, i.e. not a plain tab/CR/LF and not in the printable /// ranges `CharacterClass.isPrintable` implements) wherever they occur in /// the stream, not only inside specific constructs like anchor names. private(set) var invalidControlCharacterIndex: Int? init(_ string: String = "", isFinal: Bool = true) { scalars = Array(string.unicodeScalars) self.isFinal = isFinal recordFirstInvalidControlCharacter(in: 0 ..< scalars.count) } /// `PEEK_TOKEN` / `CHECK_AT`: the scalar `offset` ahead, or nil past the /// end (libyaml's `\0`). Past the buffered end while more chunks may still /// arrive, this flags `ranDry` instead of asserting true end of input. mutating func peek(_ offset: Int = 0) -> Unicode.Scalar? { let position = index + offset if position < scalars.count { return scalars[position] } if !isFinal { ranDry = true } return nil } /// Append a chunk of newly arrived input. Only meaningful before /// `markFinal()`. Clears `ranDry`: new data may satisfy whatever `peek(_:)` /// call set it, and nothing else ever clears it. The existing /// checkpoint-restore path gets this for free (the restored `Reader` is a /// copy from before the dry read), but a resumed scan that keeps its /// reader position instead of restoring a snapshot (`PlainScalarSuspended`) /// has no other point where a stale `true` would ever become false again. mutating func append(_ chunk: String) { let previousCount = scalars.count scalars.append(contentsOf: chunk.unicodeScalars) ranDry = false recordFirstInvalidControlCharacter(in: previousCount ..< scalars.count) } /// Declare that no further chunks are coming: a `peek` past the buffered /// end is now a real end of input, matching the batch reader's behavior. mutating func markFinal() { isFinal = true } var isAtEnd: Bool { index >= scalars.count } /// The current position. Column is 0-based, as in libyaml. var mark: PureYAML.Parsing.Mark { PureYAML.Parsing.Mark(line: line, column: column, index: index) } /// `SKIP`: consume one non-break scalar, advancing the column. mutating func skip() { guard index < scalars.count else { return } index += 1 column += 1 } /// `SKIP_LINE`: consume a line break and start a new line. A CR+LF pair is /// one break, matching libyaml. mutating func skipLine() { if peek() == "\r", peek(1) == "\n" { index += 2 } else if CharacterClass.isBreak(peek()) { index += 1 } else { return } line += 1 column = 0 } /// The 0-based line/column of `scalars[index]`, computed by counting line /// breaks from the start of the buffer. Not the hot `mark` path (that /// tracks `line`/`column` incrementally as scanning consumes characters); /// used only once, lazily, when `fetchNextToken()` actually needs to report /// `invalidControlCharacterIndex` as a `Mark`, since that index can be /// anywhere in the buffer, not necessarily at the reader's current /// scanning position. func mark(atIndex target: Int) -> PureYAML.Parsing.Mark { var line = 0 var column = 0 var cursor = 0 while cursor < target, cursor < scalars.count { if scalars[cursor] == "\r", cursor + 1 < scalars.count, scalars[cursor + 1] == "\n" { line += 1 column = 0 cursor += 2 } else if CharacterClass.isBreak(scalars[cursor]) { line += 1 column = 0 cursor += 1 } else { column += 1 cursor += 1 } } return PureYAML.Parsing.Mark(line: line, column: column, index: target) } /// Scans `range` (a slice of `scalars` just buffered by `init`/`append(_:)`) /// for the first scalar outside `CharacterClass.isPrintable`, recording its /// index if one is found and none was already recorded. Only the first /// occurrence overall matters (matching libyaml, which reports the first /// invalid byte and stops), so once `invalidControlCharacterIndex` is set /// this is a no-op. private mutating func recordFirstInvalidControlCharacter(in range: Range) { guard invalidControlCharacterIndex == nil else { return } for index in range where !CharacterClass.isPrintable(scalars[index]) { invalidControlCharacterIndex = index return } } } }