use std::fmt;
const HEADER_BYTES: usize = 32;
const WAL_VERSION: u32 = 3_007_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameFreeWalError {
InvalidDatabasePageSize,
PartialHeader,
FrameBytesPresent,
InvalidHeader,
InvalidChecksum,
PageSizeMismatch,
NotGenerationBinding,
}
impl fmt::Display for FrameFreeWalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidDatabasePageSize => "invalid main database page size",
Self::PartialHeader => "partial WAL header is not recovery authority",
Self::FrameBytesPresent => {
"WAL contains frame bytes; committed, uncheckpointed changes must be preserved"
}
Self::InvalidHeader => "invalid WAL header",
Self::InvalidChecksum => "invalid WAL header checksum",
Self::PageSizeMismatch => "WAL and main database page sizes differ",
Self::NotGenerationBinding => {
"WAL change is not a zero-byte to valid-header generation binding"
}
})
}
}
impl std::error::Error for FrameFreeWalError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameFreeWalHeader {
bytes: [u8; HEADER_BYTES],
}
impl FrameFreeWalHeader {
#[must_use]
pub const fn as_bytes(&self) -> &[u8; HEADER_BYTES] {
&self.bytes
}
#[must_use]
pub fn page_size(&self) -> u32 {
word(&self.bytes, 8)
}
#[must_use]
pub fn checkpoint_sequence(&self) -> u32 {
word(&self.bytes, 12)
}
#[must_use]
pub fn salts(&self) -> [u32; 2] {
[word(&self.bytes, 16), word(&self.bytes, 20)]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameFreeWalState {
Absent,
Empty,
Header(FrameFreeWalHeader),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameFreeWalTransition {
Unchanged,
HeaderBound(FrameFreeWalHeader),
}
const fn valid_page_size(size: u32) -> bool {
size >= 512 && size <= 65_536 && size.is_power_of_two()
}
fn word(bytes: &[u8; HEADER_BYTES], offset: usize) -> u32 {
u32::from_be_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
pub fn inspect_frame_free_wal(
bytes: Option<&[u8]>,
database_page_size: u32,
) -> Result<FrameFreeWalState, FrameFreeWalError> {
if !valid_page_size(database_page_size) {
return Err(FrameFreeWalError::InvalidDatabasePageSize);
}
let Some(bytes) = bytes else {
return Ok(FrameFreeWalState::Absent);
};
match bytes.len() {
0 => return Ok(FrameFreeWalState::Empty),
1..HEADER_BYTES => return Err(FrameFreeWalError::PartialHeader),
HEADER_BYTES => {}
_ => return Err(FrameFreeWalError::FrameBytesPresent),
}
let mut header = [0; HEADER_BYTES];
header.copy_from_slice(bytes);
let magic = word(&header, 0);
let page_size = word(&header, 8);
if !matches!(magic, 0x377f_0682 | 0x377f_0683)
|| word(&header, 4) != WAL_VERSION
|| !valid_page_size(page_size)
{
return Err(FrameFreeWalError::InvalidHeader);
}
if page_size != database_page_size {
return Err(FrameFreeWalError::PageSizeMismatch);
}
let big_endian = magic & 1 != 0;
let mut checksum = [0_u32; 2];
for pair in header[..24].as_chunks::<8>().0 {
let decode = |at: usize| {
let value = [pair[at], pair[at + 1], pair[at + 2], pair[at + 3]];
if big_endian {
u32::from_be_bytes(value)
} else {
u32::from_le_bytes(value)
}
};
checksum[0] = checksum[0]
.wrapping_add(decode(0))
.wrapping_add(checksum[1]);
checksum[1] = checksum[1]
.wrapping_add(decode(4))
.wrapping_add(checksum[0]);
}
if checksum != [word(&header, 24), word(&header, 28)] {
return Err(FrameFreeWalError::InvalidChecksum);
}
Ok(FrameFreeWalState::Header(FrameFreeWalHeader {
bytes: header,
}))
}
pub fn verify_frame_free_wal_transition(
before: Option<&[u8]>,
after: Option<&[u8]>,
database_page_size: u32,
) -> Result<FrameFreeWalTransition, FrameFreeWalError> {
let before = inspect_frame_free_wal(before, database_page_size)?;
let after = inspect_frame_free_wal(after, database_page_size)?;
if before == after {
return Ok(FrameFreeWalTransition::Unchanged);
}
match (before, after) {
(FrameFreeWalState::Empty, FrameFreeWalState::Header(header)) => {
Ok(FrameFreeWalTransition::HeaderBound(header))
}
_ => Err(FrameFreeWalError::NotGenerationBinding),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn header(page_size: u32, big_endian: bool, sequence: u32) -> [u8; 32] {
let fields = [
if big_endian { 0x377f_0683 } else { 0x377f_0682 },
WAL_VERSION,
page_size,
sequence,
0x1234_5678,
0x9abc_def0,
];
let mut bytes = [0; 32];
for (field, slot) in fields.iter().zip(bytes.as_chunks_mut::<4>().0) {
slot.copy_from_slice(&field.to_be_bytes());
}
let mut a = 0_u32;
let mut b = 0_u32;
for pair in bytes[..24].as_chunks::<8>().0 {
let decode = |slice: &[u8]| {
let array: [u8; 4] = slice.try_into().unwrap();
if big_endian {
u32::from_be_bytes(array)
} else {
u32::from_le_bytes(array)
}
};
a = a.wrapping_add(decode(&pair[..4])).wrapping_add(b);
b = b.wrapping_add(decode(&pair[4..])).wrapping_add(a);
}
bytes[24..28].copy_from_slice(&a.to_be_bytes());
bytes[28..32].copy_from_slice(&b.to_be_bytes());
bytes
}
#[test]
fn accepts_only_valid_frame_free_observations_in_both_orders() {
for size in [512, 1024, 2048, 4096, 8192, 16_384, 32_768, 65_536] {
assert_eq!(
inspect_frame_free_wal(None, size),
Ok(FrameFreeWalState::Absent)
);
assert_eq!(
inspect_frame_free_wal(Some(&[]), size),
Ok(FrameFreeWalState::Empty)
);
for big_endian in [false, true] {
let bytes = header(size, big_endian, 7);
let FrameFreeWalState::Header(proof) =
inspect_frame_free_wal(Some(&bytes), size).unwrap()
else {
panic!("expected a verified header");
};
assert_eq!(proof.as_bytes(), &bytes);
assert_eq!(proof.page_size(), size);
assert_eq!(proof.checkpoint_sequence(), 7);
assert_eq!(proof.salts(), [0x1234_5678, 0x9abc_def0]);
}
}
}
#[test]
fn rejects_every_partial_header_and_any_trailing_bytes() {
let bytes = header(4096, false, 0);
for length in 1..32 {
assert_eq!(
inspect_frame_free_wal(Some(&bytes[..length]), 4096),
Err(FrameFreeWalError::PartialHeader)
);
}
for extra in [1, 23, 24, 24 + 4096, 2 * (24 + 4096)] {
let mut framed = bytes.to_vec();
framed.resize(32 + extra, 0);
assert_eq!(
inspect_frame_free_wal(Some(&framed), 4096),
Err(FrameFreeWalError::FrameBytesPresent)
);
assert!(verify_frame_free_wal_transition(Some(&framed), Some(&framed), 4096).is_err());
assert!(verify_frame_free_wal_transition(Some(&framed), Some(&bytes), 4096).is_err());
}
}
#[test]
fn rejects_every_single_bit_header_corruption() {
for big_endian in [false, true] {
let original = header(4096, big_endian, 3);
for byte in 0..32 {
for bit in 0..8 {
let mut corrupt = original;
corrupt[byte] ^= 1 << bit;
assert!(
inspect_frame_free_wal(Some(&corrupt), 4096).is_err(),
"byte={byte}, bit={bit}, big_endian={big_endian}"
);
assert!(
verify_frame_free_wal_transition(Some(&corrupt), Some(&corrupt), 4096)
.is_err()
);
}
}
}
}
#[test]
fn rejects_invalid_or_mismatched_page_sizes_and_versions() {
for size in [0, 1, 256, 513, 4095, 65_537, 131_072, u32::MAX] {
assert_eq!(
inspect_frame_free_wal(None, size),
Err(FrameFreeWalError::InvalidDatabasePageSize)
);
assert_eq!(
inspect_frame_free_wal(Some(&header(size, false, 0)), 4096),
Err(FrameFreeWalError::InvalidHeader)
);
}
assert_eq!(
inspect_frame_free_wal(Some(&header(8192, false, 0)), 4096),
Err(FrameFreeWalError::PageSizeMismatch)
);
let mut version = header(4096, false, 0);
version[4..8].copy_from_slice(&3_007_001_u32.to_be_bytes());
assert_eq!(
inspect_frame_free_wal(Some(&version), 4096),
Err(FrameFreeWalError::InvalidHeader)
);
}
#[test]
fn binding_is_one_way_and_never_an_absence_or_reset_exception() {
for big_endian in [false, true] {
let first = header(4096, big_endian, 1);
let next = header(4096, big_endian, 2);
let states: [Option<&[u8]>; 4] = [None, Some(&[]), Some(&first), Some(&next)];
for (i, before) in states.iter().enumerate() {
for (j, after) in states.iter().enumerate() {
let result = verify_frame_free_wal_transition(*before, *after, 4096);
if i == j {
assert_eq!(result, Ok(FrameFreeWalTransition::Unchanged));
} else if i == 1 && j >= 2 {
assert!(matches!(result, Ok(FrameFreeWalTransition::HeaderBound(_))));
} else {
assert_eq!(result, Err(FrameFreeWalError::NotGenerationBinding));
}
}
}
}
}
}