use super::{Page, PageSource};
use crate::error::{Error, Result};
use crate::format::header::HEADER_LEN;
use crate::format::{DatabaseHeader, TextEncoding};
use crate::vfs::File;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
const JOURNAL_MAGIC: &[u8; 8] = b"GSQLJRN1";
pub struct WritePager {
file: Box<dyn File>,
journal: Option<Box<dyn File>>,
header: DatabaseHeader,
page_size: usize,
disk_pages: u32,
page_count: u32,
overlay: BTreeMap<u32, Vec<u8>>,
}
impl WritePager {
pub fn open(mut file: Box<dyn File>, mut journal: Option<Box<dyn File>>) -> Result<WritePager> {
if let Some(j) = journal.as_mut() {
Self::recover(file.as_mut(), j.as_mut())?;
}
let file_size = file.size()?;
if file_size < HEADER_LEN as u64 {
return Err(Error::Corrupt("file too small to be a database".into()));
}
let mut head = [0u8; HEADER_LEN];
file.read_exact_at(&mut head, 0)?;
let header = DatabaseHeader::parse(&head)?;
let page_size = header.page_size as usize;
if file_size % page_size as u64 != 0 {
return Err(Error::Corrupt(
"file size not a multiple of page size".into(),
));
}
let pages = (file_size / page_size as u64) as u32;
Ok(WritePager {
file,
journal,
header,
page_size,
disk_pages: pages,
page_count: pages,
overlay: BTreeMap::new(),
})
}
pub fn create(
file: Box<dyn File>,
journal: Option<Box<dyn File>>,
page_size: u32,
) -> Result<WritePager> {
if page_size < 512 || !page_size.is_power_of_two() {
return Err(Error::Error(format!("invalid page size {page_size}")));
}
let header = DatabaseHeader {
page_size,
write_version: 1,
read_version: 1,
reserved_space: 0,
change_counter: 1,
size_in_pages: 1,
freelist_trunk: 0,
freelist_count: 0,
schema_cookie: 0,
schema_format: 4,
default_cache_size: 0,
largest_root_page: 0,
text_encoding: TextEncoding::Utf8,
user_version: 0,
incremental_vacuum: 0,
application_id: 0,
version_valid_for: 1,
sqlite_version_number: 3_053_002,
};
let mut wp = WritePager {
file,
journal,
header,
page_size: page_size as usize,
disk_pages: 0,
page_count: 1,
overlay: BTreeMap::new(),
};
let mut page1 = vec![0u8; page_size as usize];
wp.header.write_to(&mut page1)?;
write_empty_leaf_header(&mut page1, HEADER_LEN, page_size);
wp.overlay.insert(1, page1);
Ok(wp)
}
pub fn header_mut(&mut self) -> &mut DatabaseHeader {
&mut self.header
}
pub fn read_page(&self, number: u32) -> Result<Vec<u8>> {
if let Some(bytes) = self.overlay.get(&number) {
return Ok(bytes.clone());
}
if number == 0 || number > self.disk_pages {
return Err(Error::Corrupt(format!("page {number} out of range")));
}
let mut buf = vec![0u8; self.page_size];
self.file
.read_exact_at(&mut buf, (number as u64 - 1) * self.page_size as u64)?;
Ok(buf)
}
pub fn write_page(&mut self, number: u32, bytes: Vec<u8>) -> Result<()> {
if bytes.len() != self.page_size {
return Err(Error::Error("page image has wrong size".into()));
}
self.overlay.insert(number, bytes);
Ok(())
}
pub fn allocate_page(&mut self) -> Result<u32> {
if self.header.freelist_count > 0 && self.header.freelist_trunk != 0 {
return self.alloc_from_freelist();
}
self.page_count += 1;
let n = self.page_count;
self.overlay.insert(n, vec![0u8; self.page_size]);
Ok(n)
}
fn alloc_from_freelist(&mut self) -> Result<u32> {
let trunk = self.header.freelist_trunk;
let mut tbytes = self.read_page(trunk)?;
let leaf_count = be32(&tbytes, 4);
if leaf_count > 0 {
let idx = 8 + 4 * (leaf_count as usize - 1);
let leaf = be32(&tbytes, idx);
put32(&mut tbytes, 4, leaf_count - 1);
self.write_page(trunk, tbytes)?;
self.header.freelist_count -= 1;
self.overlay.insert(leaf, vec![0u8; self.page_size]);
Ok(leaf)
} else {
let next = be32(&tbytes, 0);
self.header.freelist_trunk = next;
self.header.freelist_count -= 1;
self.overlay.insert(trunk, vec![0u8; self.page_size]);
Ok(trunk)
}
}
pub fn free_page(&mut self, page: u32) -> Result<()> {
let trunk = self.header.freelist_trunk;
let max_leaves = (self.usable_size() / 4).saturating_sub(2) as u32;
if trunk != 0 {
let mut tbytes = self.read_page(trunk)?;
let leaf_count = be32(&tbytes, 4);
if leaf_count < max_leaves {
let idx = 8 + 4 * leaf_count as usize;
put32(&mut tbytes, idx, page);
put32(&mut tbytes, 4, leaf_count + 1);
self.write_page(trunk, tbytes)?;
self.header.freelist_count += 1;
return Ok(());
}
}
let mut nb = vec![0u8; self.page_size];
put32(&mut nb, 0, trunk); put32(&mut nb, 4, 0); self.write_page(page, nb)?;
self.header.freelist_trunk = page;
self.header.freelist_count += 1;
Ok(())
}
pub fn rollback(&mut self) {
self.overlay.clear();
self.page_count = self.disk_pages;
}
pub fn commit(&mut self) -> Result<()> {
if self.overlay.is_empty() {
return Ok(());
}
self.header.change_counter = self.header.change_counter.wrapping_add(1);
self.header.size_in_pages = self.page_count;
self.header.version_valid_for = self.header.change_counter;
let mut page1 = self.overlay.get(&1).cloned().unwrap_or(self.read_page(1)?);
self.header.write_to(&mut page1)?;
self.overlay.insert(1, page1);
if self.journal.is_some() {
self.write_journal()?;
}
let page_size = self.page_size as u64;
let pages: Vec<(u32, Vec<u8>)> =
self.overlay.iter().map(|(k, v)| (*k, v.clone())).collect();
for (n, bytes) in &pages {
self.file.write_all_at(bytes, (*n as u64 - 1) * page_size)?;
}
self.file.truncate(self.page_count as u64 * page_size)?;
self.file.sync()?;
if let Some(j) = self.journal.as_mut() {
j.truncate(0)?;
j.sync()?;
}
self.disk_pages = self.page_count;
self.overlay.clear();
Ok(())
}
fn write_journal(&mut self) -> Result<()> {
let mut originals: Vec<(u32, Vec<u8>)> = Vec::new();
for &n in self.overlay.keys() {
if n <= self.disk_pages {
let mut buf = vec![0u8; self.page_size];
self.file
.read_exact_at(&mut buf, (n as u64 - 1) * self.page_size as u64)?;
originals.push((n, buf));
}
}
let j = self.journal.as_mut().unwrap();
j.truncate(0)?;
let mut hdr = Vec::with_capacity(16);
hdr.extend_from_slice(JOURNAL_MAGIC);
hdr.extend_from_slice(&self.disk_pages.to_be_bytes());
hdr.extend_from_slice(&(self.page_size as u32).to_be_bytes());
j.write_all_at(&hdr, 0)?;
let mut off = hdr.len() as u64;
for (n, bytes) in &originals {
j.write_all_at(&n.to_be_bytes(), off)?;
off += 4;
j.write_all_at(bytes, off)?;
off += self.page_size as u64;
}
j.sync()?;
Ok(())
}
fn recover(file: &mut dyn File, journal: &mut dyn File) -> Result<()> {
let jsize = journal.size()?;
if jsize < 16 {
return Ok(()); }
let mut hdr = [0u8; 16];
journal.read_exact_at(&mut hdr, 0)?;
if &hdr[0..8] != JOURNAL_MAGIC {
return Ok(()); }
let orig_pages = u32::from_be_bytes([hdr[8], hdr[9], hdr[10], hdr[11]]);
let page_size = u32::from_be_bytes([hdr[12], hdr[13], hdr[14], hdr[15]]) as usize;
let mut off = 16u64;
while off + 4 + page_size as u64 <= jsize {
let mut nb = [0u8; 4];
journal.read_exact_at(&mut nb, off)?;
off += 4;
let n = u32::from_be_bytes(nb);
let mut buf = vec![0u8; page_size];
journal.read_exact_at(&mut buf, off)?;
off += page_size as u64;
file.write_all_at(&buf, (n as u64 - 1) * page_size as u64)?;
}
file.truncate(orig_pages as u64 * page_size as u64)?;
file.sync()?;
journal.truncate(0)?;
journal.sync()?;
Ok(())
}
}
impl PageSource for WritePager {
fn page(&self, number: u32) -> Result<Page> {
Ok(Page::from_bytes(number, self.read_page(number)?))
}
fn header(&self) -> &DatabaseHeader {
&self.header
}
fn usable_size(&self) -> usize {
self.header.usable_size() as usize
}
fn page_count(&self) -> u32 {
self.page_count
}
}
#[inline]
fn be32(b: &[u8], at: usize) -> u32 {
u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}
#[inline]
fn put32(b: &mut [u8], at: usize, v: u32) {
b[at..at + 4].copy_from_slice(&v.to_be_bytes());
}
fn write_empty_leaf_header(page: &mut [u8], offset: usize, page_size: u32) {
page[offset] = 0x0d; page[offset + 1] = 0; page[offset + 2] = 0;
page[offset + 3] = 0; page[offset + 4] = 0;
let ccs: u16 = if page_size == 65536 {
0
} else {
page_size as u16
};
page[offset + 5] = (ccs >> 8) as u8;
page[offset + 6] = ccs as u8;
page[offset + 7] = 0; }
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{memory::MemoryVfs, OpenFlags, Vfs};
fn mem_wp() -> WritePager {
let vfs = MemoryVfs::new();
let file = vfs.open("db", OpenFlags::READ_WRITE_CREATE).unwrap();
WritePager::create(file, None, 4096).unwrap()
}
#[test]
fn create_yields_valid_empty_db() {
let mut wp = mem_wp();
wp.commit().unwrap();
let p1 = wp.read_page(1).unwrap();
let h = DatabaseHeader::parse(&p1).unwrap();
assert_eq!(h.page_size, 4096);
assert_eq!(h.size_in_pages, 1);
assert_eq!(p1[100], 0x0d); }
#[test]
fn allocate_and_readback() {
let mut wp = mem_wp();
let n = wp.allocate_page().unwrap();
assert_eq!(n, 2);
let mut img = vec![0u8; 4096];
img[0] = 0x0d;
wp.write_page(n, img).unwrap();
wp.commit().unwrap();
assert_eq!(wp.page_count(), 2);
assert_eq!(wp.read_page(2).unwrap()[0], 0x0d);
}
#[test]
fn rollback_discards_overlay() {
let mut wp = mem_wp();
wp.commit().unwrap(); wp.allocate_page().unwrap();
wp.rollback();
assert_eq!(wp.page_count(), 1);
}
#[test]
fn journal_recovery_restores_originals() {
let vfs = MemoryVfs::new();
{
let file = vfs.open("db", OpenFlags::READ_WRITE_CREATE).unwrap();
let jf = vfs
.open("db-journal", OpenFlags::READ_WRITE_CREATE)
.unwrap();
let mut wp = WritePager::create(file, Some(jf), 4096).unwrap();
wp.commit().unwrap(); }
let orig_p1 = {
let f = vfs.open("db", OpenFlags::READ_ONLY).unwrap();
let mut b = vec![0u8; 4096];
f.read_exact_at(&mut b, 0).unwrap();
b
};
{
let mut j = vfs.open("db-journal", OpenFlags::READ_WRITE).unwrap();
let mut hdr = Vec::new();
hdr.extend_from_slice(JOURNAL_MAGIC);
hdr.extend_from_slice(&1u32.to_be_bytes()); hdr.extend_from_slice(&4096u32.to_be_bytes());
j.write_all_at(&hdr, 0).unwrap();
j.write_all_at(&1u32.to_be_bytes(), 16).unwrap();
j.write_all_at(&orig_p1, 20).unwrap();
j.sync().unwrap();
}
{
let mut f = vfs.open("db", OpenFlags::READ_WRITE).unwrap();
f.write_all_at(&[0xFFu8; 16], 0).unwrap();
}
let file = vfs.open("db", OpenFlags::READ_WRITE).unwrap();
let jf = vfs.open("db-journal", OpenFlags::READ_WRITE).unwrap();
let wp = WritePager::open(file, Some(jf)).unwrap();
assert_eq!(wp.read_page(1).unwrap(), orig_p1);
}
}