wx-cli/src/crypto/wal.rs

74 lines
2.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

use anyhow::Result;
use std::io::{SeekFrom, Seek, Write};
use std::path::Path;
use super::{decrypt_page, PAGE_SZ};
pub const WAL_HDR_SZ: usize = 32;
pub const WAL_FRAME_HDR: usize = 24;
/// 将 WAL 文件中的变更应用到已解密的数据库文件
///
/// WAL 格式SQLite 标准SQLCipher 4 的 WAL 帧也被加密):
/// - WAL header (32 bytes): magic(4) + format(4) + page_sz(4) + ckpt_seq(4) + salt1(4) + salt2(4) + cksum1(4) + cksum2(4)
/// - 每帧frame_header(24 bytes) + page_data(PAGE_SZ bytes)
/// - frame_header: pgno(4) + commit_pgcnt(4) + salt1(4) + salt2(4) + cksum1(4) + cksum2(4)
pub fn apply_wal(wal_path: &Path, out_path: &Path, enc_key: &[u8; 32]) -> Result<()> {
if !wal_path.exists() {
return Ok(());
}
let wal_data = std::fs::read(wal_path)?;
if wal_data.len() <= WAL_HDR_SZ {
return Ok(());
}
// 读取 WAL 头中的 salt1 / salt2
let s1 = u32::from_be_bytes(wal_data[16..20].try_into().unwrap());
let s2 = u32::from_be_bytes(wal_data[20..24].try_into().unwrap());
let frame_size = WAL_FRAME_HDR + PAGE_SZ;
let frame_area = &wal_data[WAL_HDR_SZ..];
// 打开输出文件做随机写
let mut db_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(out_path)?;
let mut pos = 0usize;
while pos + frame_size <= frame_area.len() {
let fh = &frame_area[pos..pos + WAL_FRAME_HDR];
let page_data = &frame_area[pos + WAL_FRAME_HDR..pos + frame_size];
let pgno = u32::from_be_bytes(fh[0..4].try_into().unwrap());
let fs1 = u32::from_be_bytes(fh[8..12].try_into().unwrap());
let fs2 = u32::from_be_bytes(fh[12..16].try_into().unwrap());
pos += frame_size;
// 跳过无效页码
if pgno == 0 || pgno > 1_000_000 {
continue;
}
// salt 不匹配的帧属于已检查点或旧事务
if fs1 != s1 || fs2 != s2 {
continue;
}
let mut page_buf = page_data.to_vec();
if page_buf.len() < PAGE_SZ {
page_buf.resize(PAGE_SZ, 0);
}
// WAL 帧中的页数据不含 SALT 头,所以对 pgno=1 的帧也用普通页解密路径
// (区别于主数据库第一页需要跳过 SALT 并写入 SQLite 魔数)
let dec = decrypt_page(enc_key, &page_buf, if pgno == 1 { 2 } else { pgno })?;
let file_offset = (pgno as u64 - 1) * PAGE_SZ as u64;
db_file.seek(SeekFrom::Start(file_offset))?;
db_file.write_all(&dec)?;
}
Ok(())
}