use std::collections::HashMap;
use crate::error::{Result, SqliteError};
use crate::source::Source;
pub const WAL_HEADER_LEN: usize = 32;
pub const FRAME_HEADER_LEN: usize = 24;
const MAGIC_LITTLE: u32 = 0x377f_0682;
const MAGIC_BIG: u32 = 0x377f_0683;
pub struct Wal<S: Source> {
source: S,
page_size: u32,
pages: HashMap<u32, u64>,
page_count: u32,
committed_frames: u32,
total_frames: u32,
}
impl<S: Source> Wal<S> {
pub fn open(source: S) -> Result<Wal<S>> {
let len = source.len()?;
if len < WAL_HEADER_LEN as u64 {
return Ok(Wal {
source,
page_size: 0,
pages: HashMap::new(),
page_count: 0,
committed_frames: 0,
total_frames: 0,
});
}
let mut header = [0u8; WAL_HEADER_LEN];
source.read_at(0, &mut header)?;
let magic = be32(&header, 0);
let big_endian = match magic {
MAGIC_LITTLE => false,
MAGIC_BIG => true,
other => {
return Err(SqliteError::malformed(
None,
format!("the wal file starts with {other:#010x}, not a wal magic number"),
))
}
};
let running = checksum(big_endian, (0, 0), &header[..24]);
let expected = (be32(&header, 24), be32(&header, 28));
if running != expected {
return Err(SqliteError::malformed(
None,
"the wal header checksum does not match its contents",
));
}
let format = be32(&header, 4);
if format != 3_007_000 {
return Err(SqliteError::unsupported(format!(
"wal format version {format} is not the one this reader knows (3007000)"
)));
}
let page_size = be32(&header, 8);
if page_size < 512 || !page_size.is_power_of_two() {
return Err(SqliteError::malformed(
None,
format!("the wal header gives a page size of {page_size}"),
));
}
let salt = (be32(&header, 16), be32(&header, 20));
let frame_len = FRAME_HEADER_LEN as u64 + page_size as u64;
let total_frames =
u32::try_from((len - WAL_HEADER_LEN as u64) / frame_len).unwrap_or(u32::MAX);
let mut pages: HashMap<u32, u64> = HashMap::new();
let mut running = running;
let mut committed_frames = 0u32;
let mut page_count = 0u32;
let mut pending: Vec<(u32, u64)> = Vec::new();
let mut frame_header = [0u8; FRAME_HEADER_LEN];
let mut page = vec![0u8; page_size as usize];
for index in 0..total_frames {
let at = WAL_HEADER_LEN as u64 + index as u64 * frame_len;
source.read_at(at, &mut frame_header)?;
let number = be32(&frame_header, 0);
let after_commit = be32(&frame_header, 4);
let frame_salt = (be32(&frame_header, 8), be32(&frame_header, 12));
if frame_salt != salt {
break;
}
source.read_at(at + FRAME_HEADER_LEN as u64, &mut page)?;
let after_header = checksum(big_endian, running, &frame_header[..8]);
let computed = checksum(big_endian, after_header, &page);
if computed != (be32(&frame_header, 16), be32(&frame_header, 20)) {
break;
}
running = computed;
if number == 0 {
return Err(SqliteError::malformed(
None,
format!("wal frame {index} claims to hold page 0"),
));
}
pending.push((number, at + FRAME_HEADER_LEN as u64));
if after_commit != 0 {
for (number, offset) in pending.drain(..) {
pages.insert(number, offset);
}
committed_frames = index + 1;
page_count = after_commit;
}
}
pages.retain(|number, _| *number <= page_count);
Ok(Wal {
source,
page_size,
pages,
page_count,
committed_frames,
total_frames,
})
}
pub fn is_empty(&self) -> bool {
self.pages.is_empty()
}
pub fn page_size(&self) -> u32 {
self.page_size
}
pub fn page_count(&self) -> u32 {
self.page_count
}
pub fn frame_counts(&self) -> (u32, u32) {
(self.total_frames, self.committed_frames)
}
pub fn read_page_part(&self, number: u32, offset: u32, buf: &mut [u8]) -> Result<bool> {
let Some(at) = self.pages.get(&number) else {
return Ok(false);
};
let end = offset as u64 + buf.len() as u64;
if end > self.page_size as u64 {
return Err(SqliteError::malformed(
number,
format!(
"a read of {} bytes at {offset} runs past the {} byte wal page",
buf.len(),
self.page_size
),
));
}
self.source.read_at(at + offset as u64, buf)?;
Ok(true)
}
}
fn checksum(big_endian: bool, start: (u32, u32), data: &[u8]) -> (u32, u32) {
let (mut s0, mut s1) = start;
for chunk in data.as_chunks::<8>().0 {
let (x, y) = if big_endian {
(
u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
u32::from_be_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
)
} else {
(
u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
)
};
s0 = s0.wrapping_add(x).wrapping_add(s1);
s1 = s1.wrapping_add(y).wrapping_add(s0);
}
(s0, s1)
}
fn be32(bytes: &[u8], at: usize) -> u32 {
u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
pub struct WalSource<S: Source, W: Source> {
main: S,
log: Option<Wal<W>>,
page_size: u32,
page_count: u32,
}
impl<S: Source, W: Source> WalSource<S, W> {
pub fn new(main: S, log: Option<W>) -> Result<WalSource<S, W>> {
let mut header = [0u8; crate::header::HEADER_LEN];
if main.len()? < header.len() as u64 {
return Err(SqliteError::not_sqlite(
"the file is shorter than a database header",
));
}
main.read_at(0, &mut header)?;
let page_size = crate::header::Header::parse(&header)?.page_size;
let log = match log {
Some(source) => {
let wal = Wal::open(source)?;
if wal.page_size() != 0 && wal.page_size() != page_size {
return Err(SqliteError::malformed(
None,
format!(
"the log is written in {} byte pages and the database in {page_size}",
wal.page_size()
),
));
}
if wal.is_empty() {
None
} else {
Some(wal)
}
}
None => None,
};
let page_count = match &log {
Some(wal) => wal.page_count(),
None => u32::try_from(main.len()? / page_size as u64).unwrap_or(u32::MAX),
};
Ok(WalSource {
main,
log,
page_size,
page_count,
})
}
pub fn has_log(&self) -> bool {
self.log.is_some()
}
}
impl<S: Source, W: Source> Source for WalSource<S, W> {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
let Some(log) = &self.log else {
return self.main.read_at(offset, buf);
};
let page_size = self.page_size as u64;
let mut done = 0usize;
while done < buf.len() {
let at = offset + done as u64;
let page = (at / page_size) + 1;
let within = (at % page_size) as u32;
let take = ((page_size - within as u64) as usize).min(buf.len() - done);
let slice = &mut buf[done..done + take];
let served = match u32::try_from(page) {
Ok(number) => log.read_page_part(number, within, slice)?,
Err(_) => false,
};
if !served {
self.main.read_at(at, slice)?;
}
done += take;
}
Ok(())
}
fn len(&self) -> Result<u64> {
match &self.log {
Some(_) => Ok(self.page_count as u64 * self.page_size as u64),
None => self.main.len(),
}
}
fn accounts_for_wal(&self) -> bool {
true
}
}