#![cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use crate::Result;
use crate::errors::PagedbError;
use super::SnapshotStats;
const MAGIC: &[u8; 8] = b"PGDBSNAP";
const MANIFEST_RESERVED_SIZE: usize = 240;
#[allow(dead_code)]
const KIND_FULL: u8 = 0;
#[allow(dead_code)]
const KIND_INCREMENTAL: u8 = 1;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone)]
pub struct SnapshotManifest {
pub version: u32,
pub kind: u8,
pub target_commit: u64,
pub base_commit: u64,
pub file_id: [u8; 16],
pub mk_epoch: u64,
pub kek_salt: [u8; 16],
pub cipher_id: u8,
pub page_size: u32,
pub next_page_id_at_target: u64,
pub segments_count: u32,
pub realm_id: [u8; 16],
pub target_active_root_page_id: u64,
pub target_catalog_root_page_id: u64,
}
#[must_use]
pub fn encode_manifest(m: &SnapshotManifest, hk_key: &[u8; 32]) -> [u8; MANIFEST_RESERVED_SIZE] {
let mut buf = [0u8; MANIFEST_RESERVED_SIZE];
buf[..8].copy_from_slice(MAGIC);
buf[8..12].copy_from_slice(&m.version.to_le_bytes());
buf[12] = m.kind;
buf[13..21].copy_from_slice(&m.target_commit.to_le_bytes());
buf[21..29].copy_from_slice(&m.base_commit.to_le_bytes());
buf[29..45].copy_from_slice(&m.file_id);
buf[45..53].copy_from_slice(&m.mk_epoch.to_le_bytes());
buf[53..69].copy_from_slice(&m.kek_salt);
buf[69] = m.cipher_id;
buf[70..74].copy_from_slice(&m.page_size.to_le_bytes());
buf[74..82].copy_from_slice(&m.next_page_id_at_target.to_le_bytes());
buf[82..86].copy_from_slice(&m.segments_count.to_le_bytes());
buf[86..102].copy_from_slice(&m.realm_id);
buf[102..110].copy_from_slice(&m.target_active_root_page_id.to_le_bytes());
buf[110..118].copy_from_slice(&m.target_catalog_root_page_id.to_le_bytes());
let mac = compute_manifest_mac(&buf[..224], hk_key);
buf[224..240].copy_from_slice(&mac);
buf
}
pub fn decode_manifest(
buf: &[u8; MANIFEST_RESERVED_SIZE],
hk_key: &[u8; 32],
) -> Result<SnapshotManifest> {
if &buf[..8] != MAGIC {
return Err(PagedbError::snapshot_artifact_invalid("manifest.magic"));
}
let expected_mac = compute_manifest_mac(&buf[..224], hk_key);
if buf[224..240] != expected_mac {
return Err(PagedbError::snapshot_artifact_invalid("manifest.hk_mac"));
}
let version = u32::from_le_bytes(buf[8..12].try_into().unwrap_or([0; 4]));
let kind = buf[12];
let target_commit = u64::from_le_bytes(buf[13..21].try_into().unwrap_or([0; 8]));
let base_commit = u64::from_le_bytes(buf[21..29].try_into().unwrap_or([0; 8]));
let mut file_id = [0u8; 16];
file_id.copy_from_slice(&buf[29..45]);
let mk_epoch = u64::from_le_bytes(buf[45..53].try_into().unwrap_or([0; 8]));
let mut kek_salt = [0u8; 16];
kek_salt.copy_from_slice(&buf[53..69]);
let cipher_id = buf[69];
let page_size = u32::from_le_bytes(buf[70..74].try_into().unwrap_or([0; 4]));
let next_page_id_at_target = u64::from_le_bytes(buf[74..82].try_into().unwrap_or([0; 8]));
let segments_count = u32::from_le_bytes(buf[82..86].try_into().unwrap_or([0; 4]));
let mut realm_id = [0u8; 16];
realm_id.copy_from_slice(&buf[86..102]);
let target_active_root_page_id = u64::from_le_bytes(buf[102..110].try_into().unwrap_or([0; 8]));
let target_catalog_root_page_id =
u64::from_le_bytes(buf[110..118].try_into().unwrap_or([0; 8]));
Ok(SnapshotManifest {
version,
kind,
target_commit,
base_commit,
file_id,
mk_epoch,
kek_salt,
cipher_id,
page_size,
next_page_id_at_target,
segments_count,
realm_id,
target_active_root_page_id,
target_catalog_root_page_id,
})
}
fn compute_manifest_mac(data: &[u8], hk_key: &[u8; 32]) -> [u8; 16] {
let mut mac = <HmacSha256 as Mac>::new_from_slice(hk_key).expect("HMAC can take any key size");
mac.update(data);
let full = mac.finalize().into_bytes();
let mut out = [0u8; 16];
out.copy_from_slice(&full[..16]);
out
}
async fn ensure_empty_destination(path: &Path) -> Result<()> {
match fs::read_dir(path).await {
Ok(mut entries) => {
if entries
.next_entry()
.await
.map_err(PagedbError::Io)?
.is_some()
{
return Err(PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("snapshot destination is not empty: {}", path.display()),
)));
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(PagedbError::Io(error)),
}
Ok(())
}
async fn copy_file_to(src_path: &Path, dst_path: &Path) -> Result<u64> {
let mut src = fs::File::open(src_path).await.map_err(PagedbError::Io)?;
let mut dst = fs::File::create(dst_path).await.map_err(PagedbError::Io)?;
let mut buf = vec![0u8; 64 * 1024];
let mut total = 0u64;
loop {
let n = src.read(&mut buf).await.map_err(PagedbError::Io)?;
if n == 0 {
break;
}
dst.write_all(&buf[..n]).await.map_err(PagedbError::Io)?;
total += n as u64;
}
dst.flush().await.map_err(PagedbError::Io)?;
dst.sync_all().await.map_err(PagedbError::Io)?;
Ok(total)
}
pub async fn snapshot_full(
src_db_root: &Path,
dst_path: &Path,
manifest: &SnapshotManifest,
hk_key: &[u8; 32],
segment_ids: &[[u8; 16]],
highest_required_main_page: u64,
) -> Result<SnapshotStats> {
ensure_empty_destination(dst_path).await?;
fs::create_dir_all(dst_path)
.await
.map_err(PagedbError::Io)?;
let seg_dst = dst_path.join("seg");
fs::create_dir_all(&seg_dst)
.await
.map_err(PagedbError::Io)?;
let manifest_bytes = encode_manifest(manifest, hk_key);
let manifest_dst = dst_path.join("manifest");
let mut mf = fs::File::create(&manifest_dst)
.await
.map_err(PagedbError::Io)?;
mf.write_all(&manifest_bytes)
.await
.map_err(PagedbError::Io)?;
mf.flush().await.map_err(PagedbError::Io)?;
mf.sync_all().await.map_err(PagedbError::Io)?;
let mut total_bytes: u64 = MANIFEST_RESERVED_SIZE as u64;
let main_src = src_db_root.join("main.db");
let main_dst = dst_path.join("main.db");
let main_bytes = copy_file_to(&main_src, &main_dst).await?;
total_bytes += main_bytes;
let page_size = u64::from(manifest.page_size);
let highest_required_page = highest_required_main_page.max(1).max(
manifest
.target_active_root_page_id
.max(manifest.target_catalog_root_page_id),
);
let required_main_bytes = highest_required_page
.checked_add(1)
.and_then(|page_count| page_count.checked_mul(page_size))
.ok_or_else(|| PagedbError::snapshot_artifact_invalid("main.db.length"))?;
if main_bytes < required_main_bytes {
return Err(PagedbError::Io(std::io::Error::from(
std::io::ErrorKind::UnexpectedEof,
)));
}
let pages_written = main_bytes.checked_div(page_size).unwrap_or(0);
let mut segments_written: u32 = 0;
for seg_id in segment_ids {
let hex = crate::hex::to_hex_lower(seg_id);
let seg_src = src_db_root.join("seg").join(&hex);
let seg_dst_file = seg_dst.join(&hex);
total_bytes += copy_file_to(&seg_src, &seg_dst_file).await?;
segments_written += 1;
}
Ok(SnapshotStats {
pages_written,
segments_written,
bytes: total_bytes,
})
}
pub async fn snapshot_incremental(
src_db_root: &Path,
dst_path: &Path,
manifest: &SnapshotManifest,
hk_key: &[u8; 32],
segment_ids: &[[u8; 16]],
base_next_page_id: u64,
changed_page_ids: &[u64],
) -> Result<SnapshotStats> {
ensure_empty_destination(dst_path).await?;
fs::create_dir_all(dst_path)
.await
.map_err(PagedbError::Io)?;
let seg_dst = dst_path.join("seg");
fs::create_dir_all(&seg_dst)
.await
.map_err(PagedbError::Io)?;
let manifest_bytes = encode_manifest(manifest, hk_key);
let manifest_dst = dst_path.join("manifest");
let mut mf = fs::File::create(&manifest_dst)
.await
.map_err(PagedbError::Io)?;
mf.write_all(&manifest_bytes)
.await
.map_err(PagedbError::Io)?;
mf.flush().await.map_err(PagedbError::Io)?;
mf.sync_all().await.map_err(PagedbError::Io)?;
let mut total_bytes: u64 = MANIFEST_RESERVED_SIZE as u64;
let page_size = manifest.page_size as usize;
let delta_dst = dst_path.join("pages.delta");
let main_src = src_db_root.join("main.db");
let mut main_file = fs::File::open(&main_src).await.map_err(PagedbError::Io)?;
let mut delta_file = fs::File::create(&delta_dst)
.await
.map_err(PagedbError::Io)?;
let mut pages_written: u64 = 0;
let mut page_buf = vec![0u8; page_size];
let mut page_ids = changed_page_ids.to_vec();
page_ids.sort_unstable();
page_ids.dedup();
for page_id in &page_ids {
if *page_id < 2 {
continue;
}
let offset = page_id
.checked_mul(page_size as u64)
.ok_or_else(|| PagedbError::Io(std::io::Error::other("page offset overflow")))?;
main_file
.seek(std::io::SeekFrom::Start(offset))
.await
.map_err(PagedbError::Io)?;
main_file
.read_exact(&mut page_buf)
.await
.map_err(PagedbError::Io)?;
delta_file
.write_all(&page_id.to_be_bytes())
.await
.map_err(PagedbError::Io)?;
delta_file
.write_all(&page_buf)
.await
.map_err(PagedbError::Io)?;
total_bytes += 8 + page_size as u64;
pages_written += 1;
}
delta_file.flush().await.map_err(PagedbError::Io)?;
delta_file.sync_all().await.map_err(PagedbError::Io)?;
let _ = base_next_page_id;
let mut segments_written: u32 = 0;
for seg_id in segment_ids {
let hex = crate::hex::to_hex_lower(seg_id);
let seg_src = src_db_root.join("seg").join(&hex);
let seg_dst_file = seg_dst.join(&hex);
total_bytes += copy_file_to(&seg_src, &seg_dst_file).await?;
segments_written += 1;
}
Ok(SnapshotStats {
pages_written,
segments_written,
bytes: total_bytes,
})
}
pub fn derive_snapshot_hk_key(
kek: &[u8; 32],
kek_salt: &[u8; 16],
mk_epoch: u64,
) -> Result<[u8; 32]> {
let mk = crate::crypto::kdf::derive_mk(kek, kek_salt, mk_epoch)?;
let hk = crate::crypto::kdf::derive_hk(&mk)?;
Ok(*hk.as_bytes())
}
pub async fn open_manifest(manifest_path: &Path, kek: &[u8; 32]) -> Result<SnapshotManifest> {
let mut f = fs::File::open(manifest_path)
.await
.map_err(PagedbError::Io)?;
if f.metadata().await.map_err(PagedbError::Io)?.len() != MANIFEST_RESERVED_SIZE as u64 {
return Err(PagedbError::snapshot_artifact_invalid("manifest.length"));
}
let mut buf = [0u8; MANIFEST_RESERVED_SIZE];
f.read_exact(&mut buf).await.map_err(PagedbError::Io)?;
let mut kek_salt = [0u8; 16];
kek_salt.copy_from_slice(&buf[53..69]);
let mk_epoch_bytes: [u8; 8] = buf[45..53].try_into().unwrap_or([0u8; 8]);
let mk_epoch = u64::from_le_bytes(mk_epoch_bytes);
let hk_key = derive_snapshot_hk_key(kek, &kek_salt, mk_epoch)?;
decode_manifest(&buf, &hk_key)
}