mirror of https://github.com/jackwener/wx-cli.git
74 lines
2.5 KiB
Rust
74 lines
2.5 KiB
Rust
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(())
|
||
}
|