mirror of
https://github.com/jackwener/wx-cli.git
synced 2026-10-09 10:25:46 +00:00
fix(daemon,scanner,crypto): harden lifecycle, widen Windows page scan, fix SQLCipher short read (#54)
- daemon: write pid file only after IPC bound; clean sock+pid on normal return - transport: PidFile JSON metadata + identity verification (ps/QueryFullProcessImageNameW); SIGTERM with poll-timeout; backward-compat read for plain-text pid - daemon_cmd: status/stop work with both new JSON and legacy plain-text pid file - config: cwd → exe_dir → ~/.wx-cli config precedence matches `wx init` write order; Windows DB auto-detect picks newest by latest mtime - crypto: full_decrypt uses read_exact for intermediate pages, zero-pads only the final partial page; tests cover short-chunk reads and early EOF - scanner/windows: page protect check covers PAGE_READWRITE / PAGE_WRITECOPY / PAGE_EXECUTE_*WRITE* with modifier-bit stripping Cross-reviewed by @wx-cli-coder. Windows verified via `cargo check --target x86_64-pc-windows-gnu` (no Windows runtime test).
This commit is contained in:
+142
-56
@@ -11,38 +11,50 @@ pub struct Config {
|
||||
pub wechat_process: String,
|
||||
}
|
||||
|
||||
/// 从 <exe_dir>/config.json 或 $HOME/.wx-cli/config.json 加载配置
|
||||
/// 从当前工作目录 / <exe_dir> / $HOME/.wx-cli 加载配置
|
||||
pub fn load_config() -> Result<Config> {
|
||||
let config_path = find_config_file()?;
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("读取 config.json 失败: {}", config_path.display()))?;
|
||||
let raw: serde_json::Value = serde_json::from_str(&content)
|
||||
.with_context(|| "config.json 格式错误")?;
|
||||
let raw: serde_json::Value =
|
||||
serde_json::from_str(&content).with_context(|| "config.json 格式错误")?;
|
||||
|
||||
let db_dir = raw.get("db_dir")
|
||||
let db_dir = raw
|
||||
.get("db_dir")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(default_db_dir);
|
||||
|
||||
let base_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||
|
||||
let keys_file = raw.get("keys_file")
|
||||
let keys_file = raw
|
||||
.get("keys_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
if p.is_absolute() { p } else { base_dir.join(p) }
|
||||
if p.is_absolute() {
|
||||
p
|
||||
} else {
|
||||
base_dir.join(p)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| base_dir.join("all_keys.json"));
|
||||
|
||||
let decrypted_dir = raw.get("decrypted_dir")
|
||||
let decrypted_dir = raw
|
||||
.get("decrypted_dir")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
if p.is_absolute() { p } else { base_dir.join(p) }
|
||||
if p.is_absolute() {
|
||||
p
|
||||
} else {
|
||||
base_dir.join(p)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| base_dir.join("decrypted"));
|
||||
|
||||
let wechat_process = raw.get("wechat_process")
|
||||
let wechat_process = raw
|
||||
.get("wechat_process")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(default_wechat_process())
|
||||
.to_string();
|
||||
@@ -56,35 +68,56 @@ pub fn load_config() -> Result<Config> {
|
||||
}
|
||||
|
||||
fn find_config_file() -> Result<PathBuf> {
|
||||
// 1. 优先查找可执行文件同目录
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let p = dir.join("config.json");
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
let cwd_dir = std::env::current_dir().ok();
|
||||
let exe_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(PathBuf::from));
|
||||
let cli_home = cli_home_dir();
|
||||
let home_dir = (cli_home != PathBuf::from("/tmp")).then_some(cli_home.as_path());
|
||||
|
||||
if let Some(path) = find_existing_config_path(cwd_dir.as_deref(), exe_dir.as_deref(), home_dir)
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
// 2. 当前工作目录
|
||||
let cwd = std::env::current_dir().unwrap_or_default().join("config.json");
|
||||
if cwd.exists() {
|
||||
return Ok(cwd);
|
||||
}
|
||||
// 3. ~/.wx-cli/config.json
|
||||
let home = cli_home_dir();
|
||||
if home != PathBuf::from("/tmp") {
|
||||
let p = home.join(".wx-cli").join("config.json");
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
// 返回默认路径(可能不存在,调用方负责处理)
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
return Ok(dir.join("config.json"));
|
||||
}
|
||||
}
|
||||
Ok(PathBuf::from("config.json"))
|
||||
|
||||
Ok(default_config_path(
|
||||
cwd_dir.as_deref(),
|
||||
exe_dir.as_deref(),
|
||||
home_dir,
|
||||
))
|
||||
}
|
||||
|
||||
fn find_existing_config_path(
|
||||
cwd_dir: Option<&Path>,
|
||||
exe_dir: Option<&Path>,
|
||||
home_dir: Option<&Path>,
|
||||
) -> Option<PathBuf> {
|
||||
let candidates = [
|
||||
cwd_dir.map(config_path_in_dir),
|
||||
exe_dir.map(config_path_in_dir),
|
||||
home_dir.map(home_config_path),
|
||||
];
|
||||
candidates.into_iter().flatten().find(|path| path.exists())
|
||||
}
|
||||
|
||||
fn default_config_path(
|
||||
cwd_dir: Option<&Path>,
|
||||
exe_dir: Option<&Path>,
|
||||
home_dir: Option<&Path>,
|
||||
) -> PathBuf {
|
||||
cwd_dir
|
||||
.map(config_path_in_dir)
|
||||
.or_else(|| exe_dir.map(config_path_in_dir))
|
||||
.or_else(|| home_dir.map(home_config_path))
|
||||
.unwrap_or_else(|| PathBuf::from("config.json"))
|
||||
}
|
||||
|
||||
fn config_path_in_dir(dir: &Path) -> PathBuf {
|
||||
dir.join("config.json")
|
||||
}
|
||||
|
||||
fn home_config_path(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".wx-cli").join("config.json")
|
||||
}
|
||||
|
||||
pub fn cli_dir() -> PathBuf {
|
||||
@@ -163,8 +196,7 @@ fn default_db_dir() -> PathBuf {
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
PathBuf::from(std::env::var("APPDATA").unwrap_or_default())
|
||||
.join("Tencent/xwechat")
|
||||
PathBuf::from(std::env::var("APPDATA").unwrap_or_default()).join("Tencent/xwechat")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
@@ -174,13 +206,21 @@ fn default_db_dir() -> PathBuf {
|
||||
|
||||
fn default_wechat_process() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
{ "WeChat" }
|
||||
{
|
||||
"WeChat"
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{ "wechat" }
|
||||
{
|
||||
"wechat"
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{ "Weixin.exe" }
|
||||
{
|
||||
"Weixin.exe"
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{ "WeChat" }
|
||||
{
|
||||
"WeChat"
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动检测微信 db_storage 目录
|
||||
@@ -244,6 +284,7 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
/// 递归查找 db_storage 目录下所有 .db 文件的最新 mtime
|
||||
fn latest_db_mtime(dir: &Path) -> Option<std::time::SystemTime> {
|
||||
let mut latest = None;
|
||||
@@ -253,7 +294,10 @@ fn latest_db_mtime(dir: &Path) -> Option<std::time::SystemTime> {
|
||||
let mtime = if path.is_dir() {
|
||||
latest_db_mtime(&path).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
} else if path.extension().and_then(|s| s.to_str()) == Some("db") {
|
||||
entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
entry
|
||||
.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
@@ -278,8 +322,7 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
let data_root = content.trim().to_string();
|
||||
if PathBuf::from(&data_root).is_dir() {
|
||||
let pattern = PathBuf::from(&data_root)
|
||||
.join("xwechat_files");
|
||||
let pattern = PathBuf::from(&data_root).join("xwechat_files");
|
||||
if let Ok(entries2) = std::fs::read_dir(&pattern) {
|
||||
for entry2 in entries2.flatten() {
|
||||
let storage = entry2.path().join("db_storage");
|
||||
@@ -293,7 +336,8 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.into_iter().next()
|
||||
candidates.sort_by_key(|p| latest_db_mtime(p).unwrap_or(std::time::SystemTime::UNIX_EPOCH));
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
@@ -303,24 +347,66 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::resolve_cli_home;
|
||||
use super::{
|
||||
config_path_in_dir, default_config_path, find_existing_config_path, home_config_path,
|
||||
resolve_cli_home,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let unique = format!(
|
||||
"wx-cli-config-test-{}-{}-{}",
|
||||
name,
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
);
|
||||
let dir = std::env::temp_dir().join(unique);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_cli_home_prefers_sudo_home_when_present() {
|
||||
let home = resolve_cli_home(
|
||||
PathBuf::from("/root"),
|
||||
Some(PathBuf::from("/Users/alice")),
|
||||
);
|
||||
let home = resolve_cli_home(PathBuf::from("/root"), Some(PathBuf::from("/Users/alice")));
|
||||
assert_eq!(home, PathBuf::from("/Users/alice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_cli_home_falls_back_to_default_home() {
|
||||
let home = resolve_cli_home(
|
||||
PathBuf::from("/root"),
|
||||
None,
|
||||
);
|
||||
let home = resolve_cli_home(PathBuf::from("/root"), None);
|
||||
assert_eq!(home, PathBuf::from("/root"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_path_prefers_cwd_over_exe_and_home() {
|
||||
let cwd = temp_dir("cwd");
|
||||
let exe = temp_dir("exe");
|
||||
let home = temp_dir("home");
|
||||
fs::write(config_path_in_dir(&cwd), "{}").unwrap();
|
||||
fs::write(config_path_in_dir(&exe), "{}").unwrap();
|
||||
fs::create_dir_all(home.join(".wx-cli")).unwrap();
|
||||
fs::write(home_config_path(&home), "{}").unwrap();
|
||||
|
||||
let path = find_existing_config_path(Some(&cwd), Some(&exe), Some(&home)).unwrap();
|
||||
assert_eq!(path, config_path_in_dir(&cwd));
|
||||
|
||||
fs::remove_dir_all(cwd).unwrap();
|
||||
fs::remove_dir_all(exe).unwrap();
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_path_matches_init_write_order() {
|
||||
let cwd = PathBuf::from("/tmp/cwd");
|
||||
let exe = PathBuf::from("/tmp/exe");
|
||||
let home = PathBuf::from("/tmp/home");
|
||||
|
||||
let path = default_config_path(Some(&cwd), Some(&exe), Some(&home));
|
||||
assert_eq!(path, cwd.join("config.json"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user