#![forbid(unsafe_code)]
pub mod frame;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::pager::page::{Page, PageId, PAGE_SIZE};
use crate::platform::{remove_file_if_exists, FileBackend, FileHandle, SyncMode};
use crate::wal::frame::{
decode_frame_header_classified, encode_frame_header, frame_size_for, FrameDecode, FrameHeader,
FRAME_HEADER_SIZE, FRAME_SIZE, WAL_HEADER_SIZE, WAL_MAGIC,
};
#[cfg(feature = "encryption")]
use crate::wal::frame::{FRAME_AEAD_SUFFIX_SIZE, FRAME_SIZE_ENCRYPTED};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct Lsn(u64);
impl Lsn {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(1);
#[must_use]
pub const fn new(raw: u64) -> Self {
Self(raw)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
pub fn checked_next(self) -> Result<Self> {
self.0
.checked_add(1)
.map(Self)
.ok_or(Error::InvalidArgument("LSN overflow"))
}
#[must_use]
pub const fn prev_saturating(self) -> Self {
Self(self.0.saturating_sub(1))
}
}
pub const DEFAULT_WAL_SIZE_LIMIT: u64 = 64 * 1024 * 1024;
pub const DEFAULT_CHECKPOINT_THRESHOLD: u64 = 1_000;
#[derive(Debug, Clone, Copy)]
pub struct WalConfig {
pub sync_mode: SyncMode,
pub size_limit: u64,
pub checkpoint_threshold: u64,
}
impl Default for WalConfig {
fn default() -> Self {
Self {
sync_mode: SyncMode::Full,
size_limit: DEFAULT_WAL_SIZE_LIMIT,
checkpoint_threshold: DEFAULT_CHECKPOINT_THRESHOLD,
}
}
}
#[derive(Debug)]
pub struct Recovered {
pub view: HashMap<PageId, Page>,
pub header: Option<Page>,
pub next_lsn: Lsn,
pub salt: u32,
pub committed_frames: u64,
pub end_offset: u64,
}
impl Recovered {
#[must_use]
pub fn into_view(self) -> HashMap<PageId, Page> {
self.view
}
}
#[cfg_attr(not(feature = "encryption"), derive(Copy))]
#[derive(Clone)]
#[allow(dead_code)] pub(crate) struct WalKey(crate::pager::MasterKeyBytes);
impl WalKey {
#[must_use]
#[allow(dead_code)] pub(crate) fn new(bytes: [u8; 32]) -> Self {
Self(crate::pager::wrap_master_key(bytes))
}
#[inline]
#[allow(dead_code)] pub(crate) fn as_bytes(&self) -> &[u8; 32] {
let bytes: &[u8; 32] = &self.0;
bytes
}
}
impl std::fmt::Debug for WalKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("WalKey(<redacted>)")
}
}
#[derive(Debug)]
pub struct Wal<F: FileBackend = FileHandle> {
file: F,
path: PathBuf,
salt: u32,
next_lsn: Lsn,
end_offset: u64,
committed_frames: u64,
config: WalConfig,
key: Option<WalKey>,
}
#[derive(Debug)]
pub struct WalTxn<'a, F: FileBackend = FileHandle> {
wal: &'a mut Wal<F>,
staged: Vec<(PageId, Page)>,
is_header: Vec<bool>,
}
impl Wal<FileHandle> {
pub fn create_fresh(path: &Path, config: WalConfig) -> Result<Self> {
let file = FileHandle::open_or_create(path)?;
Self::create_fresh_with(file, path.to_path_buf(), config)
}
pub fn open_for_recovery(
path: &Path,
expected_salt: u32,
size_limit: u64,
) -> Result<Recovered> {
if !path.exists() {
return Ok(empty_recovered(expected_salt));
}
let file = FileHandle::open_or_create(path)?;
Self::open_for_recovery_with(&file, expected_salt, size_limit)
}
}
impl<F: FileBackend> Wal<F> {
pub fn create_fresh_with(file: F, path: PathBuf, config: WalConfig) -> Result<Self> {
file.set_len(0)?;
let salt = fresh_salt();
write_wal_header(&file, salt)?;
file.sync_data(config.sync_mode)?;
Ok(Self {
file,
path,
salt,
next_lsn: Lsn::ONE,
end_offset: WAL_HEADER_SIZE as u64,
committed_frames: 0,
config,
key: None,
})
}
pub(crate) fn set_key(&mut self, key: Option<[u8; 32]>) {
self.key = key.map(WalKey::new);
}
#[must_use]
pub fn from_recovered_meta(
file: F,
path: PathBuf,
salt: u32,
next_lsn: Lsn,
end_offset: u64,
committed_frames: u64,
config: WalConfig,
) -> Self {
Self {
file,
path,
salt,
next_lsn,
end_offset,
committed_frames,
config,
key: None,
}
}
pub fn open_for_recovery_with(
file: &F,
expected_salt: u32,
size_limit: u64,
) -> Result<Recovered> {
Self::open_for_recovery_with_key(file, expected_salt, size_limit, None)
}
pub fn open_for_recovery_with_key(
file: &F,
expected_salt: u32,
size_limit: u64,
key: Option<[u8; 32]>,
) -> Result<Recovered> {
let len = file.len()?;
if len < WAL_HEADER_SIZE as u64 {
return Ok(empty_recovered(expected_salt));
}
let header_salt = read_wal_header(file)?;
if header_salt != expected_salt {
return Ok(empty_recovered(expected_salt));
}
let key = key.map(WalKey::new);
walk_frames(file, header_salt, len, size_limit, key.as_ref())
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
fn frame_size_bytes(&self) -> usize {
frame_size_for(self.key.is_some())
}
#[must_use]
pub fn salt(&self) -> u32 {
self.salt
}
#[must_use]
pub fn next_lsn(&self) -> Lsn {
self.next_lsn
}
#[must_use]
pub fn committed_frames(&self) -> u64 {
self.committed_frames
}
#[must_use]
pub fn checkpoint_threshold(&self) -> u64 {
self.config.checkpoint_threshold
}
pub fn begin_txn(&mut self) -> WalTxn<'_, F> {
WalTxn {
wal: self,
staged: Vec::new(),
is_header: Vec::new(),
}
}
pub fn reset_after_checkpoint(&mut self) -> Result<()> {
let new_salt = next_salt(self.salt);
write_wal_header(&self.file, new_salt)?;
self.file.sync_data(self.config.sync_mode)?;
self.file.set_len(WAL_HEADER_SIZE as u64)?;
self.file.sync_data(self.config.sync_mode)?;
self.salt = new_salt;
self.next_lsn = Lsn::ONE;
self.end_offset = WAL_HEADER_SIZE as u64;
self.committed_frames = 0;
Ok(())
}
}
impl<F: FileBackend> WalTxn<'_, F> {
pub fn append(&mut self, page_id: PageId, page: &Page) -> Result<()> {
self.append_raw(page_id.get(), page)
}
pub fn append_header(&mut self, page: &Page) -> Result<()> {
self.append_raw(0, page)
}
fn append_raw(&mut self, page_id: u64, page: &Page) -> Result<()> {
let prospective_size = self
.wal
.end_offset
.checked_add(
(self
.staged
.len()
.checked_add(1)
.ok_or(Error::InvalidArgument("txn frame count overflow"))?
as u64)
.checked_mul(self.wal.frame_size_bytes() as u64)
.ok_or(Error::InvalidArgument("wal frame offset overflow"))?,
)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
if prospective_size > self.wal.config.size_limit {
return Err(Error::InvalidArgument("wal size limit exceeded"));
}
let staged_id = PageId::new(if page_id == 0 { 1 } else { page_id }).ok_or(
Error::InvalidArgument("internal: PageId::new returned None on a non-zero input"),
)?;
self.staged.push((staged_id, page.clone()));
self.is_header.push(page_id == 0);
debug_assert_eq!(
self.staged.len(),
self.is_header.len(),
"is_header must stay index-aligned with staged"
);
Ok(())
}
#[must_use]
pub fn staged_frame_count(&self) -> usize {
self.staged.len()
}
pub fn commit(self) -> Result<Lsn> {
if self.staged.is_empty() {
return Ok(self.wal.next_lsn.prev_saturating());
}
let last_index = self.staged.len() - 1;
let mut last_lsn: Lsn = Lsn::ZERO;
let mut offset = self.wal.end_offset;
let bound = self.staged.len();
debug_assert_eq!(
self.staged.len(),
self.is_header.len(),
"is_header must stay index-aligned with staged"
);
let mut scratch = [0u8; FRAME_SIZE];
for (index, (page_id, page)) in self.staged.iter().enumerate().take(bound) {
let lsn = self.wal.next_lsn;
self.wal.next_lsn = self.wal.next_lsn.checked_next()?;
let is_commit = index == last_index;
let wire_page_id = if self.is_header[index] {
0
} else {
page_id.get()
};
let header = FrameHeader {
page_id: wire_page_id,
lsn: lsn.get(),
salt: self.wal.salt,
commit: is_commit,
};
write_frame(
&self.wal.file,
offset,
&header,
page,
self.wal.key.as_ref(),
&mut scratch,
)?;
last_lsn = lsn;
offset = offset
.checked_add(self.wal.frame_size_bytes() as u64)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
}
self.wal.file.sync_data(self.wal.config.sync_mode)?;
self.wal.end_offset = offset;
let count_u64 = u64::try_from(self.staged.len())
.map_err(|_| Error::InvalidArgument("txn frame count overflow"))?;
self.wal.committed_frames = self
.wal
.committed_frames
.checked_add(count_u64)
.ok_or(Error::InvalidArgument("committed-frame count overflow"))?;
Ok(last_lsn)
}
#[must_use]
pub fn drain_staged(self) -> Vec<(PageId, Page)> {
self.staged
}
}
fn empty_recovered(salt: u32) -> Recovered {
Recovered {
view: HashMap::new(),
header: None,
next_lsn: Lsn::ONE,
salt,
committed_frames: 0,
end_offset: WAL_HEADER_SIZE as u64,
}
}
fn read_wal_header<F: FileBackend>(file: &F) -> Result<u32> {
let mut buf = [0u8; WAL_HEADER_SIZE];
file.read_exact_at(&mut buf, 0)?;
if buf[0..4] != WAL_MAGIC {
return Err(Error::InvalidFormat {
reason: "WAL magic does not match",
});
}
let major = u16::from_le_bytes([buf[4], buf[5]]);
if !crate::pager::header::is_supported_format_major(major) {
return Err(Error::InvalidFormat {
reason: "WAL format-major does not match",
});
}
let minor = u16::from_le_bytes([buf[6], buf[7]]);
if !crate::pager::header::is_supported_minor(major, minor) {
return Err(Error::InvalidFormat {
reason: "WAL format-minor is not supported",
});
}
let page_size = u16::from_le_bytes([buf[8], buf[9]]);
if usize::from(page_size) != PAGE_SIZE {
return Err(Error::InvalidFormat {
reason: "WAL page-size does not match this build",
});
}
Ok(u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]))
}
fn walk_frames<F: FileBackend>(
file: &F,
salt: u32,
file_len: u64,
size_limit: u64,
key: Option<&WalKey>,
) -> Result<Recovered> {
let frame_size = frame_size_for(key.is_some());
let frame_limit = bounded_frame_limit(size_limit, frame_size);
let scan_end = scan_aligned_end(file_len, frame_size);
let scan = find_last_commit_end(file, salt, scan_end, frame_limit, key, frame_size)?;
if key.is_some() && scan.salt_match_with_decrypt_failure {
return Err(Error::EncryptionKeyInvalid);
}
if scan.last_commit_end <= WAL_HEADER_SIZE as u64 {
return Ok(empty_recovered(salt));
}
replay_up_to_commit(
file,
salt,
scan.last_commit_end,
frame_limit,
key,
frame_size,
)
}
#[derive(Debug, Clone, Copy)]
struct ScanResult {
last_commit_end: u64,
salt_match_with_decrypt_failure: bool,
}
fn scan_aligned_end(file_len: u64, frame_size: usize) -> u64 {
if file_len < WAL_HEADER_SIZE as u64 {
return WAL_HEADER_SIZE as u64;
}
let payload = file_len - WAL_HEADER_SIZE as u64;
let aligned_frames = payload / frame_size as u64;
aligned_frames
.checked_mul(frame_size as u64)
.and_then(|product| product.checked_add(WAL_HEADER_SIZE as u64))
.unwrap_or(u64::MAX)
}
fn find_last_commit_end<F: FileBackend>(
file: &F,
salt: u32,
scan_end: u64,
frame_limit: u64,
key: Option<&WalKey>,
frame_size: usize,
) -> Result<ScanResult> {
let mut offset = WAL_HEADER_SIZE as u64;
let mut last_commit_end = WAL_HEADER_SIZE as u64;
let mut salt_match_with_decrypt_failure = false;
let mut walked: u64 = 0;
while let Some(frame_end) = offset.checked_add(frame_size as u64) {
if frame_end > scan_end {
break;
}
if walked > frame_limit {
return Err(Error::InvalidArgument(
"WAL exceeds size limit during recovery",
));
}
walked = walked.saturating_add(1);
let frame = read_plaintext_frame_diag(file, offset, key, frame_size, salt)?;
if let FrameDecode::Ok(header) = decode_frame_header_classified(&frame.buf, salt) {
if header.commit {
last_commit_end = offset
.checked_add(frame_size as u64)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
}
} else if frame.salt_matched_but_decrypt_failed {
salt_match_with_decrypt_failure = true;
}
offset = offset
.checked_add(frame_size as u64)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
}
Ok(ScanResult {
last_commit_end,
salt_match_with_decrypt_failure,
})
}
struct PlaintextFrame {
buf: Vec<u8>,
salt_matched_but_decrypt_failed: bool,
}
fn read_plaintext_frame_diag<F: FileBackend>(
file: &F,
offset: u64,
key: Option<&WalKey>,
frame_size: usize,
expected_salt: u32,
) -> Result<PlaintextFrame> {
let raw = read_frame_bytes(file, offset, frame_size)?;
let Some(key) = key else {
let _ = expected_salt;
return Ok(PlaintextFrame {
buf: raw,
salt_matched_but_decrypt_failed: false,
});
};
#[cfg(feature = "encryption")]
{
let mut out = vec![0u8; FRAME_SIZE];
out[..FRAME_HEADER_SIZE].copy_from_slice(&raw[..FRAME_HEADER_SIZE]);
let mut ad = [0u8; 16];
ad.copy_from_slice(&raw[..16]);
let mut ct = [0u8; PAGE_SIZE + FRAME_AEAD_SUFFIX_SIZE];
ct.copy_from_slice(&raw[FRAME_HEADER_SIZE..]);
let mut pt = [0u8; PAGE_SIZE];
let salt_matched_but_decrypt_failed = if wal_decrypt(key, &ad, &ct, &mut pt).is_ok() {
out[FRAME_HEADER_SIZE..].copy_from_slice(&pt);
false
} else {
let frame_salt = u32::from_le_bytes([raw[16], raw[17], raw[18], raw[19]]);
out[FRAME_HEADER_SIZE..].copy_from_slice(&ct[..PAGE_SIZE]);
frame_salt == expected_salt
};
Ok(PlaintextFrame {
buf: out,
salt_matched_but_decrypt_failed,
})
}
#[cfg(not(feature = "encryption"))]
{
let _ = (key, expected_salt);
Ok(PlaintextFrame {
buf: raw,
salt_matched_but_decrypt_failed: false,
})
}
}
fn replay_up_to_commit<F: FileBackend>(
file: &F,
salt: u32,
commit_end: u64,
frame_limit: u64,
key: Option<&WalKey>,
frame_size: usize,
) -> Result<Recovered> {
let mut state = WalkState::new();
let mut walked: u64 = 0;
while state.offset < commit_end {
if walked > frame_limit {
return Err(Error::InvalidArgument(
"WAL exceeds size limit during recovery",
));
}
walked = walked.saturating_add(1);
let buf = read_plaintext_frame(file, state.offset, key, frame_size)?;
match decode_frame_header_classified(&buf, salt) {
FrameDecode::Ok(header) => {
let mut page = Page::zeroed();
page.as_bytes_mut()
.copy_from_slice(&buf[FRAME_HEADER_SIZE..]);
state.absorb(header, page, frame_size)?;
}
FrameDecode::CrcInvalid => {
return Err(Error::WalCorruption {
frame_offset: state.offset,
});
}
FrameDecode::SaltMismatch | FrameDecode::Malformed => {
}
}
state.offset = state
.offset
.checked_add(frame_size as u64)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
}
Ok(state.into_recovered(salt))
}
struct WalkState {
view: HashMap<PageId, Page>,
pending: HashMap<PageId, Page>,
pending_count: u64,
pending_header: Option<Page>,
view_header: Option<Page>,
offset: u64,
next_lsn: Lsn,
committed_frames: u64,
last_committed_offset: u64,
}
impl WalkState {
fn new() -> Self {
Self {
view: HashMap::new(),
pending: HashMap::new(),
pending_count: 0,
pending_header: None,
view_header: None,
offset: WAL_HEADER_SIZE as u64,
next_lsn: Lsn::ONE,
committed_frames: 0,
last_committed_offset: WAL_HEADER_SIZE as u64,
}
}
fn absorb(&mut self, header: FrameHeader, page: Page, frame_size: usize) -> Result<bool> {
if header.page_id == 0 {
self.pending_header = Some(page);
} else {
let Some(page_id) = PageId::new(header.page_id) else {
return Ok(false);
};
self.pending.insert(page_id, page);
}
self.pending_count = self
.pending_count
.checked_add(1)
.ok_or(Error::InvalidArgument("pending frame count overflow"))?;
if header.commit {
promote_pending(&mut self.pending, &mut self.view);
if let Some(hp) = self.pending_header.take() {
self.view_header = Some(hp);
}
self.committed_frames = self
.committed_frames
.checked_add(self.pending_count)
.ok_or(Error::InvalidArgument("committed frame count overflow"))?;
self.pending_count = 0;
self.last_committed_offset = self
.offset
.checked_add(frame_size as u64)
.ok_or(Error::InvalidArgument("wal offset overflow"))?;
}
self.next_lsn = Lsn::new(header.lsn.saturating_add(1));
Ok(true)
}
fn into_recovered(self, salt: u32) -> Recovered {
Recovered {
view: self.view,
header: self.view_header,
next_lsn: self.next_lsn,
salt,
committed_frames: self.committed_frames,
end_offset: self.last_committed_offset,
}
}
}
fn promote_pending(pending: &mut HashMap<PageId, Page>, view: &mut HashMap<PageId, Page>) {
for (id, page) in pending.drain() {
view.insert(id, page);
}
}
fn read_frame_bytes<F: FileBackend>(file: &F, offset: u64, frame_size: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; frame_size];
file.read_exact_at(&mut buf, offset)?;
Ok(buf)
}
fn read_plaintext_frame<F: FileBackend>(
file: &F,
offset: u64,
key: Option<&WalKey>,
frame_size: usize,
) -> Result<Vec<u8>> {
let raw = read_frame_bytes(file, offset, frame_size)?;
let Some(key) = key else {
return Ok(raw);
};
#[cfg(feature = "encryption")]
{
let mut out = vec![0u8; FRAME_SIZE];
out[..FRAME_HEADER_SIZE].copy_from_slice(&raw[..FRAME_HEADER_SIZE]);
let mut ad = [0u8; 16];
ad.copy_from_slice(&raw[..16]);
let mut ct = [0u8; PAGE_SIZE + FRAME_AEAD_SUFFIX_SIZE];
ct.copy_from_slice(&raw[FRAME_HEADER_SIZE..]);
let mut pt = [0u8; PAGE_SIZE];
match wal_decrypt(key, &ad, &ct, &mut pt) {
Ok(()) => {
out[FRAME_HEADER_SIZE..].copy_from_slice(&pt);
}
Err(_) => {
out[FRAME_HEADER_SIZE..].copy_from_slice(&ct[..PAGE_SIZE]);
}
}
Ok(out)
}
#[cfg(not(feature = "encryption"))]
{
let _ = key;
Ok(raw)
}
}
fn bounded_frame_limit(size_limit: u64, frame_size: usize) -> u64 {
size_limit / frame_size as u64 + 1
}
fn fresh_salt() -> u32 {
let mut rng = rand::rng();
rng.next_u32()
}
fn next_salt(current: u32) -> u32 {
let mut candidate = fresh_salt();
if candidate == current {
candidate = current.wrapping_add(1);
}
candidate
}
fn write_wal_header<F: FileBackend>(file: &F, salt: u32) -> Result<()> {
let mut buf = [0u8; WAL_HEADER_SIZE];
buf[0..4].copy_from_slice(&WAL_MAGIC);
buf[4..6].copy_from_slice(&crate::pager::header::FORMAT_MAJOR.to_le_bytes());
buf[6..8].copy_from_slice(&crate::pager::header::FORMAT_MINOR.to_le_bytes());
let page_size_u16 =
u16::try_from(PAGE_SIZE).map_err(|_| Error::InvalidArgument("page size > u16"))?;
buf[8..10].copy_from_slice(&page_size_u16.to_le_bytes());
buf[12..16].copy_from_slice(&salt.to_le_bytes());
file.write_all_at(&buf, 0)
}
fn write_frame<F: FileBackend>(
file: &F,
offset: u64,
header: &FrameHeader,
page: &Page,
key: Option<&WalKey>,
scratch: &mut [u8],
) -> Result<()> {
debug_assert_eq!(
scratch.len(),
FRAME_SIZE,
"frame scratch must be FRAME_SIZE"
);
let frame_buf = scratch;
frame_buf[FRAME_HEADER_SIZE..].copy_from_slice(page.as_bytes());
encode_frame_header(header, frame_buf);
let Some(key) = key else {
return file.write_all_at(frame_buf, offset);
};
encrypt_frame_body(key, frame_buf, offset, file)
}
fn encrypt_frame_body<F: FileBackend>(
key: &WalKey,
plain_frame: &[u8],
offset: u64,
file: &F,
) -> Result<()> {
debug_assert_eq!(plain_frame.len(), FRAME_SIZE);
#[cfg(feature = "encryption")]
{
let mut out = [0u8; FRAME_SIZE_ENCRYPTED];
out[..FRAME_HEADER_SIZE].copy_from_slice(&plain_frame[..FRAME_HEADER_SIZE]);
let mut body_pt = [0u8; PAGE_SIZE];
body_pt.copy_from_slice(&plain_frame[FRAME_HEADER_SIZE..]);
let mut body_phys = [0u8; PAGE_SIZE + FRAME_AEAD_SUFFIX_SIZE];
let mut ad = [0u8; 16];
ad.copy_from_slice(&plain_frame[..16]);
wal_encrypt(key, &ad, &body_pt, &mut body_phys)?;
out[FRAME_HEADER_SIZE..].copy_from_slice(&body_phys);
file.write_all_at(&out, offset)
}
#[cfg(not(feature = "encryption"))]
{
let _ = (key, plain_frame, offset, file);
Err(Error::FormatFeatureUnsupported {
feature: "encryption",
})
}
}
#[cfg(feature = "encryption")]
fn wal_encrypt(
key: &WalKey,
ad: &[u8; 16],
plaintext: &[u8; PAGE_SIZE],
out: &mut [u8; PAGE_SIZE + FRAME_AEAD_SUFFIX_SIZE],
) -> Result<()> {
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
let mut nonce_bytes = [0u8; 24];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| Error::Io(std::io::Error::other(format!("getrandom failure: {e}"))))?;
let nonce = XNonce::from_slice(&nonce_bytes);
out[..PAGE_SIZE].copy_from_slice(plaintext);
let cipher = XChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
let tag = cipher
.encrypt_in_place_detached(nonce, ad, &mut out[..PAGE_SIZE])
.map_err(|_| Error::EncryptionKeyInvalid)?;
out[PAGE_SIZE..PAGE_SIZE + 24].copy_from_slice(&nonce_bytes);
out[PAGE_SIZE + 24..].copy_from_slice(&tag);
Ok(())
}
#[cfg(feature = "encryption")]
fn wal_decrypt(
key: &WalKey,
ad: &[u8; 16],
ciphertext: &[u8; PAGE_SIZE + FRAME_AEAD_SUFFIX_SIZE],
out: &mut [u8; PAGE_SIZE],
) -> Result<()> {
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
use chacha20poly1305::{Key, Tag, XChaCha20Poly1305, XNonce};
let mut nonce_bytes = [0u8; 24];
nonce_bytes.copy_from_slice(&ciphertext[PAGE_SIZE..PAGE_SIZE + 24]);
let nonce = XNonce::from_slice(&nonce_bytes);
let mut tag_bytes = [0u8; 16];
tag_bytes.copy_from_slice(&ciphertext[PAGE_SIZE + 24..]);
let tag = Tag::from_slice(&tag_bytes);
out.copy_from_slice(&ciphertext[..PAGE_SIZE]);
let cipher = XChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
cipher
.decrypt_in_place_detached(nonce, ad, out, tag)
.map_err(|_| Error::EncryptionKeyInvalid)?;
Ok(())
}
pub fn remove_wal(path: &Path) -> Result<()> {
remove_file_if_exists(path)
}
#[cfg(test)]
mod tests;