///|
/// Magic 0x377f0683 checksums words as big-endian; 0x377f0682 uses
/// little-endian words. All structural fields remain big-endian.
pub(all) enum WalChecksumOrder {
BigEndian
LittleEndian
} derive(Eq, Debug)
///|
pub(all) struct WalChecksum {
first : UInt64
second : UInt64
} derive(Eq, Debug)
///|
pub(all) struct WalHeader {
magic : UInt64
format_version : UInt64
page_size : Int
checkpoint_sequence : UInt64
salt_first : UInt64
salt_second : UInt64
checksum : WalChecksum
computed_checksum : WalChecksum
checksum_order : WalChecksumOrder
checksum_valid : Bool
} derive(Eq, Debug)
///|
pub(all) struct WalFrame {
index : Int
offset : Int
page_number : UInt64
database_size_after_commit : UInt64
salt_first : UInt64
salt_second : UInt64
checksum : WalChecksum
computed_checksum : WalChecksum
page_data : Bytes
salt_valid : Bool
checksum_valid : Bool
} derive(Eq, Debug)
///|
pub fn WalFrame::is_commit(self : WalFrame) -> Bool {
self.database_size_after_commit != 0UL
}
///|
pub fn WalFrame::is_valid(self : WalFrame) -> Bool {
self.page_number != 0UL && self.salt_valid && self.checksum_valid
}
///|
pub(all) struct WalTransaction {
ordinal : Int
first_frame : Int
last_frame : Int
frame_count : Int
committed : Bool
database_size_after_commit : UInt64?
} derive(Eq, Debug)
///|
pub(all) struct WalFile {
header : WalHeader
frames : Array[WalFrame]
transactions : Array[WalTransaction]
diagnostics : Array[Diagnostic]
trailing_bytes : Int
last_commit_frame : Int?
} derive(Debug)
///|
pub fn WalFile::valid_frame_count(self : WalFile) -> Int {
let mut count = 0
for frame in self.frames {
if frame.is_valid() {
count = count + 1
}
}
count
}
///|
fn wal_read_word(
bytes : Bytes,
offset : Int,
order : WalChecksumOrder,
) -> UInt64 raise ParseError {
let reader = BinaryReader::range(bytes, offset, 4, "WAL checksum word")
match order {
BigEndian => reader.read_u32_be()
LittleEndian => reader.read_u32_le()
}
}
///|
/// Apply SQLite's rolling two-word WAL checksum. Initial values allow frame
/// checksums to continue the state established by the header and prior frames.
pub fn wal_checksum(
bytes : Bytes,
order : WalChecksumOrder,
initial_first? : UInt64 = 0UL,
initial_second? : UInt64 = 0UL,
) -> WalChecksum raise ParseError {
guard bytes.length() % 8 == 0 else {
raise InvalidValue(
0,
"WAL checksum input length",
bytes.length().to_string() + " is not a multiple of 8",
)
}
let mut first = initial_first & 0xffffffffUL
let mut second = initial_second & 0xffffffffUL
for offset = 0; offset < bytes.length(); offset = offset + 8 {
let word_first = wal_read_word(bytes, offset, order)
let word_second = wal_read_word(bytes, offset + 4, order)
first = (first + word_first + second) & 0xffffffffUL
second = (second + word_second + first) & 0xffffffffUL
}
{ first, second }
}
///|
fn valid_wal_page_size(size : UInt64) -> Bool {
if size < 512UL || size > 65536UL {
return false
}
(size & (size - 1UL)) == 0UL
}
///|
pub fn parse_wal_header(data : Bytes) -> WalHeader raise ParseError {
guard data.length() >= 32 else {
raise UnexpectedEnd(0, 32, data.length(), "SQLite WAL header")
}
let reader = BinaryReader::range(data, 0, 32, "SQLite WAL header")
let magic = reader.read_u32_be()
let checksum_order = match magic {
0x377f0683UL => BigEndian
0x377f0682UL => LittleEndian
_ => raise InvalidMagic("0x377f0682 or 0x377f0683", magic.to_string())
}
let format_version = reader.read_u32_be()
let raw_page_size = reader.read_u32_be()
let page_size = if raw_page_size == 1UL {
65536
} else {
guard valid_wal_page_size(raw_page_size) else {
raise InvalidValue(8, "WAL page size", raw_page_size.to_string())
}
raw_page_size.to_int()
}
let checkpoint_sequence = reader.read_u32_be()
let salt_first = reader.read_u32_be()
let salt_second = reader.read_u32_be()
let checksum : WalChecksum = {
first: reader.read_u32_be(),
second: reader.read_u32_be(),
}
let prefix = Bytes::makei(24, fn(index) { data[index] })
let computed_checksum = wal_checksum(prefix, checksum_order)
{
magic,
format_version,
page_size,
checkpoint_sequence,
salt_first,
salt_second,
checksum,
computed_checksum,
checksum_order,
checksum_valid: checksum == computed_checksum,
}
}
///|
fn frame_checksum_input(
data : Bytes,
frame_offset : Int,
page_size : Int,
) -> Bytes {
Bytes::makei(page_size + 8, fn(index) {
if index < 8 {
data[frame_offset + index]
} else {
data[frame_offset + 24 + index - 8]
}
})
}
///|
fn group_wal_transactions(frames : Array[WalFrame]) -> Array[WalTransaction] {
let transactions : Array[WalTransaction] = []
let mut start = 0
let mut ordinal = 1
for index = 0; index < frames.length(); index = index + 1 {
if frames[index].is_commit() {
transactions.push({
ordinal,
first_frame: start + 1,
last_frame: index + 1,
frame_count: index - start + 1,
committed: true,
database_size_after_commit: Some(
frames[index].database_size_after_commit,
),
})
ordinal = ordinal + 1
start = index + 1
}
}
if start < frames.length() {
transactions.push({
ordinal,
first_frame: start + 1,
last_frame: frames.length(),
frame_count: frames.length() - start,
committed: false,
database_size_after_commit: None,
})
}
transactions
}
///|
/// Parse a complete WAL, retaining invalid frames and attaching diagnostics so
/// an analyst can distinguish structural damage from uncommitted tail data.
pub fn parse_wal(data : Bytes) -> WalFile raise ParseError {
let header = parse_wal_header(data)
let frames : Array[WalFrame] = []
let diagnostics : Array[Diagnostic] = []
if !header.checksum_valid {
diagnostics.push(
Diagnostic::error("WAL_HEADER_CHECKSUM", "WAL header checksum is invalid"),
)
}
if header.format_version != 3007000UL {
diagnostics.push(
Diagnostic::warning(
"WAL_FORMAT_VERSION",
"unexpected WAL format version " + header.format_version.to_string(),
offset=4,
),
)
}
let frame_size = 24 + header.page_size
let body_length = data.length() - 32
let complete_frames = body_length / frame_size
let trailing_bytes = body_length % frame_size
let mut rolling = header.computed_checksum
for frame_index = 0
frame_index < complete_frames
frame_index = frame_index + 1 {
let offset = 32 + frame_index * frame_size
let reader = BinaryReader::range(data, offset, 24, "WAL frame header")
let page_number = reader.read_u32_be()
let database_size_after_commit = reader.read_u32_be()
let salt_first = reader.read_u32_be()
let salt_second = reader.read_u32_be()
let checksum : WalChecksum = {
first: reader.read_u32_be(),
second: reader.read_u32_be(),
}
let checksum_input = frame_checksum_input(data, offset, header.page_size)
rolling = wal_checksum(
checksum_input,
header.checksum_order,
initial_first=rolling.first,
initial_second=rolling.second,
)
let page_data = Bytes::makei(header.page_size, fn(index) {
data[offset + 24 + index]
})
let salt_valid = salt_first == header.salt_first &&
salt_second == header.salt_second
let checksum_valid = checksum == rolling
frames.push({
index: frame_index + 1,
offset,
page_number,
database_size_after_commit,
salt_first,
salt_second,
checksum,
computed_checksum: rolling,
page_data,
salt_valid,
checksum_valid,
})
if page_number == 0UL {
diagnostics.push(
Diagnostic::error(
"WAL_FRAME_PAGE_ZERO",
"WAL frame references page zero",
offset~,
),
)
}
if !salt_valid {
diagnostics.push(
Diagnostic::error(
"WAL_FRAME_SALT",
"WAL frame salt does not match the WAL header",
offset=offset + 8,
page_number~,
),
)
}
if !checksum_valid {
diagnostics.push(
Diagnostic::error(
"WAL_FRAME_CHECKSUM",
"WAL frame checksum is invalid",
offset=offset + 16,
page_number~,
),
)
}
}
if trailing_bytes != 0 {
diagnostics.push(
Diagnostic::warning(
"WAL_TRUNCATED_FRAME",
"WAL ends with " + trailing_bytes.to_string() + " byte(s) of a frame",
offset=32 + complete_frames * frame_size,
),
)
}
let transactions = group_wal_transactions(frames)
let mut last_commit_frame : Int? = None
for transaction in transactions {
if transaction.committed {
last_commit_frame = Some(transaction.last_frame)
} else {
diagnostics.push(
Diagnostic::warning(
"WAL_UNCOMMITTED_FRAMES",
"WAL contains " +
transaction.frame_count.to_string() +
" uncommitted frame(s)",
),
)
}
}
{
header,
frames,
transactions,
diagnostics,
trailing_bytes,
last_commit_frame,
}
}
///|
/// Return the newest valid occurrence of a page not newer than `max_frame`.
/// Frame indexes are one-based, matching the CLI and SQLite documentation.
pub fn WalFile::latest_frame_for_page(
self : WalFile,
page_number : UInt64,
max_frame? : Int,
) -> WalFrame? {
let limit = match max_frame {
Some(value) => value
None =>
match self.last_commit_frame {
Some(value) => value
None => 0
}
}
for index = self.frames.length() - 1; index >= 0; index = index - 1 {
let frame = self.frames[index]
if frame.index <= limit &&
frame.page_number == page_number &&
frame.is_valid() {
return Some(frame)
}
}
None
}