use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use zeroize::Zeroizing;
use crate::cache::PageCache;
use crate::crypto::{self, KEY_LEN, NONCE_LEN};
use crate::error::{MnemoError, Result};
use crate::format::{PAGE_SIZE, PAYLOAD};
pub const DEFAULT_CACHE_PAGES: usize = 8192;
pub struct Pager {
file: File,
dek: Zeroizing<[u8; KEY_LEN]>,
pub write_counter: u64,
version: u16,
cache: PageCache,
}
impl Pager {
pub fn new(
file: File,
dek: Zeroizing<[u8; KEY_LEN]>,
write_counter: u64,
version: u16,
) -> Self {
Self {
file,
dek,
write_counter,
version,
cache: PageCache::new(DEFAULT_CACHE_PAGES),
}
}
pub(crate) fn set_version(&mut self, version: u16) {
self.version = version;
}
#[inline]
fn page_aad(&self, page_no: u64) -> Vec<u8> {
if self.version >= 6 {
page_no.to_le_bytes().to_vec()
} else {
Vec::new()
}
}
pub fn dek(&self) -> &[u8; KEY_LEN] {
&self.dek
}
pub fn file_mut(&mut self) -> &mut File {
&mut self.file
}
pub fn set_cache_capacity(&mut self, pages: usize) {
self.cache.set_capacity(pages);
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache.len(), self.cache.capacity())
}
pub fn write_raw(&mut self, page_no: u64, data: &[u8; PAGE_SIZE]) -> Result<()> {
self.file.seek(SeekFrom::Start(page_no * PAGE_SIZE as u64))?;
self.file.write_all(data)?;
Ok(())
}
#[allow(dead_code)]
pub fn read_raw(&mut self, page_no: u64) -> Result<[u8; PAGE_SIZE]> {
let mut buf = [0u8; PAGE_SIZE];
self.file.seek(SeekFrom::Start(page_no * PAGE_SIZE as u64))?;
self.file.read_exact(&mut buf)?;
Ok(buf)
}
pub fn seal_page(&mut self, page_no: u64, payload: &[u8]) -> Result<[u8; PAGE_SIZE]> {
if payload.len() > PAYLOAD {
return Err(MnemoError::Invalid(format!(
"payload {} exceeds page capacity {}",
payload.len(),
PAYLOAD
)));
}
let mut padded = vec![0u8; PAYLOAD];
padded[..payload.len()].copy_from_slice(payload);
self.write_counter += 1;
let nonce = crypto::page_nonce(page_no, self.write_counter);
let aad = self.page_aad(page_no);
let ciphertext = crypto::aead_encrypt(&self.dek, &nonce, &padded, &aad)?;
let mut disk = [0u8; PAGE_SIZE];
disk[..NONCE_LEN].copy_from_slice(&nonce);
disk[NONCE_LEN..].copy_from_slice(&ciphertext);
self.cache.insert(page_no, padded, false);
Ok(disk)
}
pub fn write_sealed(&mut self, page_no: u64, bytes: &[u8; PAGE_SIZE]) -> Result<()> {
self.file.seek(SeekFrom::Start(page_no * PAGE_SIZE as u64))?;
self.file.write_all(bytes)?;
Ok(())
}
pub fn sync(&mut self) -> Result<()> {
self.file.sync_all()?;
Ok(())
}
pub fn read_page(&mut self, page_no: u64) -> Result<Vec<u8>> {
if let Some(p) = self.cache.get(page_no) {
return Ok(p);
}
let mut disk = [0u8; PAGE_SIZE];
self.file.seek(SeekFrom::Start(page_no * PAGE_SIZE as u64))?;
self.file.read_exact(&mut disk)?;
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(&disk[0..NONCE_LEN]);
let ciphertext = &disk[NONCE_LEN..];
let aad = self.page_aad(page_no);
let payload = crypto::aead_decrypt(&self.dek, &nonce, ciphertext, &aad)
.map_err(|_| MnemoError::PageAuthFailed(page_no))?;
self.cache.insert(page_no, payload.clone(), false);
Ok(payload)
}
pub fn write_page(&mut self, page_no: u64, payload: &[u8]) -> Result<()> {
if payload.len() > PAYLOAD {
return Err(MnemoError::Invalid(format!(
"payload {} exceeds page capacity {}",
payload.len(),
PAYLOAD
)));
}
let mut padded = vec![0u8; PAYLOAD];
padded[..payload.len()].copy_from_slice(payload);
self.cache.insert(page_no, padded, true);
Ok(())
}
pub fn dirty_page_count(&self) -> usize {
self.cache.dirty_pages().len()
}
pub fn flush(&mut self) -> Result<()> {
let pages = self.cache.dirty_pages();
if pages.is_empty() {
return Ok(());
}
for page_no in pages {
let payload = self
.cache
.peek(page_no)
.expect("dirty page must be cached")
.to_vec();
self.write_counter += 1;
let nonce = crypto::page_nonce(page_no, self.write_counter);
let aad = self.page_aad(page_no);
let ciphertext = crypto::aead_encrypt(&self.dek, &nonce, &payload, &aad)?;
let mut disk = Vec::with_capacity(PAGE_SIZE);
disk.extend_from_slice(&nonce);
disk.extend_from_slice(&ciphertext);
debug_assert_eq!(disk.len(), PAGE_SIZE);
self.file.seek(SeekFrom::Start(page_no * PAGE_SIZE as u64))?;
self.file.write_all(&disk)?;
}
self.file.sync_all()?;
self.cache.mark_all_clean();
self.cache.trim();
Ok(())
}
}