use crate::Result;
use crate::crypto::aad::{Aad, AadFields};
use crate::crypto::cipher::Cipher;
use crate::crypto::kdf::derive_spill_key;
use crate::crypto::nonce::Nonce;
use crate::errors::{PagedbError, QuotaKind};
use crate::vfs::types::OpenMode;
use crate::vfs::{Vfs, VfsFile, read_exact_at, write_all_at};
use super::txn::WriteTxn;
#[derive(Debug, Clone, Copy)]
pub struct ScratchOffset(u64);
pub(crate) const SPILL_TAG_LEN: usize = 16;
#[derive(Clone)]
pub(crate) struct SpillSegmentMeta {
pub offset: u64,
pub plaintext_len: u32,
pub ciphertext_len: u32,
pub nonce_bytes: [u8; 12],
}
pub struct SpillScope<'scope, 'db, V: Vfs + Clone> {
pub(super) txn: &'scope mut WriteTxn<'db, V>,
}
impl<V: Vfs + Clone> SpillScope<'_, '_, V> {
pub async fn append(&mut self, bytes: &[u8]) -> Result<ScratchOffset> {
let limit = self.txn.db.options.scratch_bytes as u64;
let segment_index = self.txn.spill_segments.len() as u64;
let mk_epoch = self
.txn
.db
.mk_epoch
.load(std::sync::atomic::Ordering::SeqCst);
let realm_id = self.txn.db.realm_id;
let file_id = self.txn.db.file_id;
let nonce = self.txn.next_spill_nonce(segment_index);
self.txn.ensure_spill_cipher()?;
let cipher_id_byte = self
.txn
.spill_cipher
.as_ref()
.expect("just derived")
.id()
.as_byte();
let aad = Aad::from_fields(AadFields {
cipher_id: cipher_id_byte,
page_kind: 0xFE,
mk_epoch,
page_id: segment_index,
realm_id,
segment_id: file_id,
});
let mut body = bytes.to_vec();
let tag = self
.txn
.spill_cipher
.as_ref()
.expect("derived above")
.encrypt(&nonce, &aad, &mut body)?;
let pers_len = body.len() as u64 + SPILL_TAG_LEN as u64;
let new_total = self.txn.spill_bytes_used.saturating_add(pers_len);
if new_total > limit {
return Err(PagedbError::quota(
self.txn.db.realm_id,
QuotaKind::ScratchPages,
new_total,
limit,
));
}
let path = self.txn.ensure_spill_path().await?;
let mut file = self.txn.db.vfs.open(&path, OpenMode::CreateOrOpen).await?;
let body_offset = self.txn.spill_bytes_used;
let ciphertext_len = body.len();
body.extend_from_slice(&tag);
write_all_at(&mut file, body_offset, &body).await?;
file.sync().await?;
let plaintext_len = u32::try_from(bytes.len()).map_err(|_| PagedbError::PayloadTooLarge)?;
let ciphertext_len =
u32::try_from(ciphertext_len).map_err(|_| PagedbError::PayloadTooLarge)?;
self.txn.spill_segments.push(SpillSegmentMeta {
offset: body_offset,
plaintext_len,
ciphertext_len,
nonce_bytes: *nonce.as_bytes(),
});
self.txn.spill_bytes_used = new_total;
self.txn
.db
.spill_bytes_in_use
.store(new_total, std::sync::atomic::Ordering::Relaxed);
Ok(ScratchOffset(segment_index))
}
pub async fn read(&self, handle: ScratchOffset) -> Result<Vec<u8>> {
let idx = usize::try_from(handle.0).map_err(|_| PagedbError::NotFound)?;
let meta = self
.txn
.spill_segments
.get(idx)
.ok_or(PagedbError::NotFound)?
.clone();
let path = self.txn.spill_path.as_ref().ok_or(PagedbError::NotFound)?;
let mut file = self.txn.db.vfs.open(path, OpenMode::Read).await?;
let body_len = meta.ciphertext_len as usize;
let frame_len = body_len
.checked_add(SPILL_TAG_LEN)
.ok_or_else(|| PagedbError::arithmetic_overflow("spill frame length"))?;
let mut frame = vec![0u8; frame_len];
read_exact_at(&mut file, meta.offset, &mut frame).await?;
let (body, tag_bytes) = frame.split_at_mut(body_len);
let mut tag = [0u8; SPILL_TAG_LEN];
tag.copy_from_slice(tag_bytes);
let cipher = self.txn.spill_cipher_readonly()?;
let nonce = Nonce::from_bytes(meta.nonce_bytes);
let aad = Aad::from_fields(AadFields {
cipher_id: cipher.id().as_byte(),
page_kind: 0xFE,
mk_epoch: self
.txn
.db
.mk_epoch
.load(std::sync::atomic::Ordering::SeqCst),
page_id: handle.0,
realm_id: self.txn.db.realm_id,
segment_id: self.txn.db.file_id,
});
cipher.decrypt(&nonce, &aad, body, &tag)?;
frame.truncate(meta.plaintext_len as usize);
Ok(frame)
}
}
impl<'db, V: Vfs + Clone> WriteTxn<'db, V> {
pub fn spill_scope(&mut self) -> SpillScope<'_, 'db, V> {
SpillScope { txn: self }
}
pub(crate) fn ensure_spill_cipher(&mut self) -> Result<&Cipher> {
if self.spill_cipher.is_none() {
let pager_mk = self.db.pager.mk()?;
let key = derive_spill_key(
&pager_mk,
&self.db.file_id,
&self.db.spill_epoch,
self.txn_seq,
)?;
self.spill_cipher = Some(Cipher::new_aes_gcm(&key));
}
self.spill_cipher.as_ref().ok_or(PagedbError::NotFound)
}
pub(crate) fn spill_cipher_readonly(&self) -> Result<&Cipher> {
self.spill_cipher.as_ref().ok_or(PagedbError::NotFound)
}
pub(crate) async fn ensure_spill_path(&mut self) -> Result<String> {
if self.spill_path.is_none() {
self.db.vfs.mkdir_all("tmp").await?;
let path = format!("tmp/scratch-{}", self.txn_seq);
self.spill_path = Some(path);
}
Ok(self.spill_path.clone().expect("just set"))
}
pub(crate) fn next_spill_nonce(&self, segment_index: u64) -> Nonce {
let mut bytes = [0u8; 12];
let seq_le = self.txn_seq.to_le_bytes();
let idx_le = segment_index.to_le_bytes();
bytes[..6].copy_from_slice(&seq_le[..6]);
bytes[6..].copy_from_slice(&idx_le[..6]);
Nonce::from_bytes(bytes)
}
pub(crate) async fn cleanup_spill_async(&mut self) {
if let Some(path) = self.spill_path.take() {
let _ = self.db.vfs.remove(&path).await;
}
}
}