// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0
///|
/// One integrity-checked event and the format active for it.
/// Views retain the input bytes. `raw` includes header/checksum; `body` excludes
/// both. For FDE, body still includes the checksum-algorithm descriptor.
/// An unknown type has validated framing, not validated body semantics.
pub struct BinlogFileEvent {
header : EventHeader
raw : BytesView
body : BytesView
format : FormatDescription
} derive(Debug, Eq)
///|
pub extend BinlogFileEvent with Eq::{equal, not_equal}
///|
pub extend BinlogFileEvent with @debug.Debug::{to_repr}
///|
/// Sequential traversal of a complete, in-memory unencrypted binlog file.
/// The first event must be FDE. Later FDEs replace the format context.
/// No OS I/O, transaction assembly or event-body dispatch is performed.
pub struct BinlogFileReader {
priv data : BytesView
priv max_event_size : UInt32
priv mut cursor : Int32
priv mut context : FormatDescription?
priv mut failed : Bool
}
///|
/// Validate the signature and configuration. Events are validated lazily.
/// The supplied view must start at the file signature, not a resume offset.
///
/// ```mbt check
/// test {
/// // A valid signature initializes the reader; next() still requires FDE.
/// let reader = @moonbinlog.BinlogFileReader::new([0xfe, 0x62, 0x69, 0x6e])
/// assert_eq(reader.position(), 4UL)
/// assert_eq(reader.format(), None)
/// }
/// ```
pub fn BinlogFileReader::new(
data : BytesView,
max_event_size~ : UInt32 = 16_777_216,
) -> BinlogFileReader raise DecodeError {
guard max_event_size >= 19 else { raise InvalidEventLimit(max_event_size~) }
validate_file_header(data)
{ data, max_event_size, cursor: 4, context: None, failed: false, }
}
///|
/// Physical offset of the next event, or the failing event after an error.
/// Offsets are relative to the supplied view, independent of next_position.
pub fn BinlogFileReader::position(self : BinlogFileReader) -> UInt64 {
self.cursor.reinterpret_as_uint().to_uint64()
}
///|
/// Current validated format; absent before FDE or after any traversal error.
pub fn BinlogFileReader::format(self : BinlogFileReader) -> FormatDescription? {
self.context
}
///|
fn BinlogFileReader::invalidate(self : BinlogFileReader) -> Unit {
self.failed = true
self.context = None
}
///|
// Shared by in-memory and event-at-a-time readers. The caller has already
// checked the complete declared event and supplies its physical header.
fn decode_file_event(
header : EventHeader,
raw : BytesView,
context : FormatDescription?,
max_event_size : UInt32,
) -> BinlogFileEvent raise DecodeError {
let file_offset = header.file_offset
let (format, body) = if header.event_type == 15 {
let format = FormatDescription::parse(raw, file_offset~, max_event_size~)
(format, raw[19:raw.length() - 4])
} else {
guard context is Some(format) else {
raise MissingFormatDescription(file_offset~)
}
(format, format.payload(raw, file_offset~, max_event_size~))
}
{ header, raw, body, format, }
}
///|
/// Return exactly one event, or None at a clean event-boundary EOF after FDE.
/// Any error permanently stops this reader and clears its format. Already
/// returned events remain valid, but do not imply a complete file/transaction.
/// Consume until None to validate the entire supplied input.
pub fn BinlogFileReader::next(
self : BinlogFileReader,
) -> BinlogFileEvent? raise DecodeError {
let file_offset = self.position()
guard !self.failed else { raise FileReaderFailed(file_offset~) }
errdefer self.invalidate()
if self.cursor == self.data.length() {
guard self.context is Some(_) else {
raise MissingFormatDescription(file_offset~)
}
return None
}
let remaining = self.data[self.cursor:]
let (header, raw) = complete_event(
remaining,
file_offset~,
max_event_size=self.max_event_size,
)
let event = decode_file_event(header, raw, self.context, self.max_event_size)
// complete_event proves raw fits in remaining; this sum cannot exceed length.
self.cursor += raw.length()
self.context = Some(event.format)
Some(event)
}