gengx
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_wal_merge.py - 验证 WAL 帧合并方案(引擎 B 实时性增强)
|
||||
=============================================================
|
||||
思路:企微运行时新消息在 message.db-wal(未 checkpoint)。
|
||||
WAL 帧数据与主库同算法加密(wxSQLite3 aes128cbc,帧头明文)。
|
||||
本脚本:解密主库 → 解密 WAL 帧 → 按页号覆盖合并 → SQLite 打开验证。
|
||||
|
||||
用法: python test_wal_merge.py <message.db> <wal_path> <key_hex>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from wxwork_crypto import (
|
||||
decrypt_db_to_file,
|
||||
decrypt_page,
|
||||
read_page_header,
|
||||
SQLITE_FILE_HEADER,
|
||||
)
|
||||
|
||||
|
||||
def wal_read_frames(wal_path: str, key: bytes):
|
||||
"""读取 WAL 帧并解密,返回 {pgno: bytes}(仅 salt 匹配的帧,后帧覆盖前帧)。
|
||||
|
||||
返回 (pages, meta):pages 为页号→解密页映射;meta 含 pgsz/salt/frame_count。
|
||||
"""
|
||||
pages: dict[int, bytes] = {}
|
||||
with open(wal_path, "rb") as f:
|
||||
hdr = f.read(32)
|
||||
if len(hdr) < 32:
|
||||
return pages, {}
|
||||
magic, ver, pgsz, ckpt, salt1, salt2, _, _ = struct.unpack(">IIIIIIII", hdr)
|
||||
meta = {"pgsz": pgsz, "salt1": salt1, "salt2": salt2, "frames": 0, "applied": 0}
|
||||
while True:
|
||||
fh = f.read(24)
|
||||
if len(fh) < 24:
|
||||
break
|
||||
pgno, commit, fs1, fs2, _, _ = struct.unpack(">IIIIII", fh)
|
||||
data = f.read(pgsz)
|
||||
if len(data) < pgsz:
|
||||
break
|
||||
meta["frames"] += 1
|
||||
if fs1 != salt1 or fs2 != salt2:
|
||||
continue # 非当前 WAL 的帧(旧事务/已回滚),跳过
|
||||
if pgno == 0 or pgno > (1 << 31):
|
||||
continue
|
||||
dec = decrypt_page(key, pgno, data, pgsz)
|
||||
if dec is None:
|
||||
continue
|
||||
pages[pgno] = dec
|
||||
meta["applied"] += 1
|
||||
return pages, meta
|
||||
|
||||
|
||||
def merge_wal_into_db(db_path: str, wal_path: str, key: bytes, out_path: str) -> bool:
|
||||
"""解密主库 + 合并 WAL 帧 → out_path。返回是否成功。"""
|
||||
# 1. 解密主库
|
||||
if not decrypt_db_to_file(db_path, key, out_path):
|
||||
return False
|
||||
pages, meta = wal_read_frames(wal_path, key)
|
||||
if not pages:
|
||||
return True # 无有效帧,主库副本即最终结果
|
||||
|
||||
pgsz = meta["pgsz"]
|
||||
# 2. 按页号覆盖
|
||||
with open(out_path, "r+b") as f:
|
||||
size = os.path.getsize(out_path)
|
||||
n_pages = (size + pgsz - 1) // pgsz
|
||||
for pgno, data in pages.items():
|
||||
if pgno > n_pages:
|
||||
# 新页:扩展文件
|
||||
f.seek(0, os.SEEK_END)
|
||||
f.write(b"\x00" * ((pgno - n_pages) * pgsz))
|
||||
n_pages = pgno
|
||||
f.seek((pgno - 1) * pgsz)
|
||||
f.write(data)
|
||||
# 3. 更新 SQLite 头页数字段(offset 28,4 字节大端)
|
||||
if n_pages > 0:
|
||||
f.seek(28)
|
||||
f.write(struct.pack(">I", n_pages))
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print("用法: python test_wal_merge.py <message.db> <wal_path> <key_hex>")
|
||||
sys.exit(1)
|
||||
db_path, wal_path, key_hex = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
key = bytes.fromhex(key_hex)
|
||||
|
||||
t0 = time.time()
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".db", prefix="wal_merge_")
|
||||
os.close(tmp_fd)
|
||||
try:
|
||||
ok = merge_wal_into_db(db_path, wal_path, key, tmp_path)
|
||||
print(f"[+] 合并完成 ok={ok} 耗时 {time.time()-t0:.2f}s")
|
||||
conn = sqlite3.connect(f"file:{tmp_path}?mode=ro", uri=True)
|
||||
conn.text_factory = lambda b: b.decode("utf-8", errors="replace")
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("PRAGMA quick_check")
|
||||
print("[+] quick_check:", cur.fetchone())
|
||||
cur.execute("SELECT COUNT(*) FROM message_table")
|
||||
print("[+] message_table 行数:", cur.fetchone()[0])
|
||||
cur.execute("SELECT MAX(send_time) FROM message_table")
|
||||
mx = cur.fetchone()[0]
|
||||
print("[+] MAX(send_time):", mx,
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mx)) if mx else "-")
|
||||
# 与仅主库对比
|
||||
cur.execute("SELECT MAX(send_time) FROM message_table WHERE send_time <= (SELECT MAX(send_time) FROM message_table)")
|
||||
finally:
|
||||
conn.close()
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user