mirror of
https://github.com/jackwener/wx-cli.git
synced 2026-10-09 00:45:45 +00:00
feat: Rust 完整重写 wx-cli(单一二进制,支持 macOS/Linux/Windows)
实现所有核心模块: - src/crypto/: SQLCipher 4 页解密 + WAL 应用(AES-256-CBC) - src/scanner/: 三平台内存扫描(macOS Mach VM / Linux /proc/mem / Windows ReadProcessMemory) - src/daemon/: tokio 异步 daemon,Unix socket IPC,mtime-aware DB 缓存,WAL 监听推送 - src/cli/: clap CLI,自动启动 daemon,完整命令实现 - src/config.rs: 跨平台配置加载,兼容 Python 版 config.json 格式 - src/ipc.rs: 换行符分隔 JSON 协议,与 Python 版兼容 - .github/workflows/release.yml: 四平台自动构建发布 cargo build --release 验证通过,生成 4.8MB macOS arm64 单一二进制
This commit is contained in:
+274
@@ -0,0 +1,274 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub db_dir: PathBuf,
|
||||
pub keys_file: PathBuf,
|
||||
pub decrypted_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
pub wechat_process: String,
|
||||
}
|
||||
|
||||
/// 从 <exe_dir>/config.json 或 $HOME/.wechat-cli/config.json 加载配置
|
||||
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 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")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
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")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
if p.is_absolute() { p } else { base_dir.join(p) }
|
||||
})
|
||||
.unwrap_or_else(|| base_dir.join("decrypted"));
|
||||
|
||||
let wechat_process = raw.get("wechat_process")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(default_wechat_process())
|
||||
.to_string();
|
||||
|
||||
Ok(Config {
|
||||
db_dir,
|
||||
keys_file,
|
||||
decrypted_dir,
|
||||
wechat_process,
|
||||
})
|
||||
}
|
||||
|
||||
/// 保存配置到文件
|
||||
pub fn save_config(config: &Config) -> Result<()> {
|
||||
let config_path = find_config_file().unwrap_or_else(|_| {
|
||||
std::env::current_exe()
|
||||
.unwrap_or_default()
|
||||
.parent()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join("config.json")
|
||||
});
|
||||
let content = serde_json::to_string_pretty(config)?;
|
||||
std::fs::write(&config_path, content)
|
||||
.with_context(|| format!("写入 config.json 失败: {}", config_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. 当前工作目录
|
||||
let cwd = std::env::current_dir().unwrap_or_default().join("config.json");
|
||||
if cwd.exists() {
|
||||
return Ok(cwd);
|
||||
}
|
||||
// 3. ~/.wechat-cli/config.json
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let p = home.join(".wechat-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"))
|
||||
}
|
||||
|
||||
pub fn cli_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
.join(".wechat-cli")
|
||||
}
|
||||
|
||||
pub fn sock_path() -> PathBuf {
|
||||
cli_dir().join("daemon.sock")
|
||||
}
|
||||
|
||||
pub fn pid_path() -> PathBuf {
|
||||
cli_dir().join("daemon.pid")
|
||||
}
|
||||
|
||||
pub fn log_path() -> PathBuf {
|
||||
cli_dir().join("daemon.log")
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> PathBuf {
|
||||
cli_dir().join("cache")
|
||||
}
|
||||
|
||||
pub fn mtime_file() -> PathBuf {
|
||||
cache_dir().join("_mtimes.json")
|
||||
}
|
||||
|
||||
fn default_db_dir() -> PathBuf {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files")
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("Documents/xwechat_files")
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
PathBuf::from(std::env::var("APPDATA").unwrap_or_default())
|
||||
.join("Tencent/xwechat")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
PathBuf::from(".")
|
||||
}
|
||||
}
|
||||
|
||||
fn default_wechat_process() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
{ "WeChat" }
|
||||
#[cfg(target_os = "linux")]
|
||||
{ "wechat" }
|
||||
#[cfg(target_os = "windows")]
|
||||
{ "Weixin.exe" }
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{ "WeChat" }
|
||||
}
|
||||
|
||||
/// 自动检测微信 db_storage 目录
|
||||
pub fn auto_detect_db_dir() -> Option<PathBuf> {
|
||||
detect_db_dir_impl()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
// 支持 sudo 环境
|
||||
let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") {
|
||||
if !sudo_user.is_empty() {
|
||||
PathBuf::from("/Users").join(&sudo_user)
|
||||
} else {
|
||||
home
|
||||
}
|
||||
} else {
|
||||
home
|
||||
};
|
||||
|
||||
let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
||||
if !base.exists() {
|
||||
return None;
|
||||
}
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&base) {
|
||||
for entry in entries.flatten() {
|
||||
let storage = entry.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort_by_key(|p| {
|
||||
std::fs::metadata(p)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
let sudo_home = std::env::var("SUDO_USER").ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|u| PathBuf::from("/home").join(u));
|
||||
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
||||
let xwechat = base_home.join("Documents/xwechat_files");
|
||||
if xwechat.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(&xwechat) {
|
||||
for entry in entries.flatten() {
|
||||
let storage = entry.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let old = base_home.join(".local/share/weixin/data/db_storage");
|
||||
if old.is_dir() {
|
||||
candidates.push(old);
|
||||
}
|
||||
}
|
||||
candidates.sort_by_key(|p| {
|
||||
std::fs::metadata(p)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let appdata = std::env::var("APPDATA").ok()?;
|
||||
let config_dir = PathBuf::from(&appdata).join("Tencent/xwechat/config");
|
||||
if !config_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&config_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "ini").unwrap_or(false) {
|
||||
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");
|
||||
if let Ok(entries2) = std::fs::read_dir(&pattern) {
|
||||
for entry2 in entries2.flatten() {
|
||||
let storage = entry2.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.into_iter().next()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user