From c8f90385a34481c2f52b7216d0dd852f31552ebb Mon Sep 17 00:00:00 2001 From: pandorafuture Date: Fri, 10 Jul 2026 19:29:29 +0800 Subject: [PATCH] chore: restore fmt and clippy CI checks --- crates/wx-cli/src/cmd/contacts.rs | 4 +- crates/wx-cli/src/cmd/decrypt.rs | 26 ++- crates/wx-cli/src/cmd/export.rs | 9 +- crates/wx-cli/src/cmd/export_media.rs | 5 +- crates/wx-cli/src/cmd/export_task.rs | 88 +++++---- crates/wx-cli/src/cmd/key.rs | 34 ++-- crates/wx-cli/src/cmd/paths.rs | 8 +- crates/wx-cli/src/cmd/query.rs | 4 +- crates/wx-cli/src/cmd/search.rs | 12 +- crates/wx-cli/src/cmd/serve/bridge.rs | 54 ++--- crates/wx-cli/src/cmd/serve/media.rs | 13 +- crates/wx-cli/src/cmd/serve/mod.rs | 8 +- crates/wx-cli/src/cmd/serve/refresh.rs | 6 +- crates/wx-cli/src/cmd/server/manager.rs | 13 +- crates/wx-cli/src/cmd/sessions.rs | 4 +- crates/wx-cli/src/cmd/watch.rs | 16 +- crates/wx-cli/src/schema.rs | 53 ++++- crates/wx-cli/src/util.rs | 25 +-- crates/wx-cli/tests/ignore-filters.rs | 241 ++++++++++++++++------- crates/wx-cli/tests/serve-media.rs | 19 +- crates/wx-context/src/cache.rs | 5 +- crates/wx-context/src/visibility.rs | 12 +- crates/wx-db/src/decode.rs | 21 +- crates/wx-db/src/fts.rs | 10 +- crates/wx-db/src/messages.rs | 8 +- crates/wx-db/src/xml_extract.rs | 15 +- crates/wx-keychain/src/lib.rs | 6 +- crates/wx-keychain/src/lldb.rs | 6 +- crates/wx-keychain/src/process.rs | 11 +- crates/wx-keychain/src/store.rs | 7 +- crates/wx-media/src/ffmpeg.rs | 16 +- crates/wx-media/src/hardlink.rs | 2 +- crates/wx-media/src/key.rs | 4 +- crates/wx-media/src/lib.rs | 8 +- crates/wx-media/tests/audio-transcode.rs | 9 +- crates/wx-media/tests/voice.rs | 5 +- crates/wx-monitor/src/cache.rs | 6 +- crates/wx-monitor/tests/integration.rs | 5 +- crates/wx-paths/src/lib.rs | 30 ++- crates/wx-paths/src/migration.rs | 15 +- crates/wx-paths/src/platform.rs | 12 +- 41 files changed, 505 insertions(+), 350 deletions(-) diff --git a/crates/wx-cli/src/cmd/contacts.rs b/crates/wx-cli/src/cmd/contacts.rs index fe03e06..7f9b5cf 100644 --- a/crates/wx-cli/src/cmd/contacts.rs +++ b/crates/wx-cli/src/cmd/contacts.rs @@ -6,7 +6,9 @@ use wx_db::Contact; use super::thin_client::{ThinClientCliArgs, ThinClientOptions}; use crate::output::JsonEnvelope; use crate::settings::Settings; -use crate::util::{effective_limit_all, open_db_core, print_cache_stats, print_detection_note, try_remote_or_local}; +use crate::util::{ + effective_limit_all, open_db_core, print_cache_stats, print_detection_note, try_remote_or_local, +}; use crate::visibility_projection::project_contacts_envelope; use crate::OutputFormat; diff --git a/crates/wx-cli/src/cmd/decrypt.rs b/crates/wx-cli/src/cmd/decrypt.rs index d83d9e4..b3085b9 100644 --- a/crates/wx-cli/src/cmd/decrypt.rs +++ b/crates/wx-cli/src/cmd/decrypt.rs @@ -74,17 +74,15 @@ pub fn cmd_decrypt( KeyMaterial::EncKey { key, salt } => { wx_decrypt::decrypt_db_direct(db_path, &out_path, key, salt, params) } - KeyMaterial::EncKeys(pairs) => { - match wx_decrypt::read_main_db_salt_for_path(db_path) { - Ok(db_salt) => match pairs.iter().find(|p| p.salt == db_salt) { - Some(pair) => wx_decrypt::decrypt_db_direct( - db_path, &out_path, &pair.key, &pair.salt, params, - ), - None => Err(wx_decrypt::DecryptError::NoMatchingEncKey), - }, - Err(e) => Err(e), - } - } + KeyMaterial::EncKeys(pairs) => match wx_decrypt::read_main_db_salt_for_path(db_path) { + Ok(db_salt) => match pairs.iter().find(|p| p.salt == db_salt) { + Some(pair) => wx_decrypt::decrypt_db_direct( + db_path, &out_path, &pair.key, &pair.salt, params, + ), + None => Err(wx_decrypt::DecryptError::NoMatchingEncKey), + }, + Err(e) => Err(e), + }, }; match db_result { @@ -98,9 +96,9 @@ pub fn cmd_decrypt( KeyMaterial::RawKey(key) => { wx_decrypt::decrypt_wal(&wal_path, &out_path, key, params) } - KeyMaterial::EncKey { key, salt } => wx_decrypt::decrypt_wal_direct( - &wal_path, &out_path, key, salt, params, - ), + KeyMaterial::EncKey { key, salt } => { + wx_decrypt::decrypt_wal_direct(&wal_path, &out_path, key, salt, params) + } KeyMaterial::EncKeys(pairs) => { match wx_decrypt::read_main_db_salt_for_path(&wal_path) { Ok(db_salt) => match pairs.iter().find(|p| p.salt == db_salt) { diff --git a/crates/wx-cli/src/cmd/export.rs b/crates/wx-cli/src/cmd/export.rs index 84a5a31..1fb5860 100644 --- a/crates/wx-cli/src/cmd/export.rs +++ b/crates/wx-cli/src/cmd/export.rs @@ -7,9 +7,9 @@ use wx_context::{ }; use wx_db::{is_group_chat, MessageContent, MessageQuery, SortOrder, MAX_QUERY_LIMIT}; +use crate::cmd::contacts::build_visibility; use crate::cmd::export_media::{MediaKind, MediaStats}; use crate::cmd::query::resolve_talker; -use crate::cmd::contacts::build_visibility; use crate::output::{JsonEnvelope, PagingMeta, StatsMeta}; use crate::schema::{enrich_message, project_message_items, EnrichedMessage}; use crate::util::{ @@ -247,10 +247,9 @@ pub fn cmd_export( } // Resolve media via parallel pipeline (or skip) - let (media_map, media_stats, _media_errors) = if no_media || cache.is_none() { + let (media_map, media_stats, _media_errors) = if no_media { (vec![vec![]; projected.len()], MediaStats::default(), None) - } else { - let c = cache.as_ref().unwrap(); + } else if let Some(c) = cache.as_ref() { let attach_dir = acct.data_dir.join("msg").join("attach"); let decrypted_media = c.decrypted_root().join("message"); let hardlink_db = c.decrypted_root().join("hardlink").join("hardlink.db"); @@ -294,6 +293,8 @@ pub fn cmd_export( }; combined.print_report(); (media_map, stats, Some(combined)) + } else { + (vec![vec![]; projected.len()], MediaStats::default(), None) }; let total_media: usize = media_map.iter().map(Vec::len).sum(); diff --git a/crates/wx-cli/src/cmd/export_media.rs b/crates/wx-cli/src/cmd/export_media.rs index daef437..bd97042 100644 --- a/crates/wx-cli/src/cmd/export_media.rs +++ b/crates/wx-cli/src/cmd/export_media.rs @@ -421,7 +421,10 @@ impl MediaBridge { } } -pub fn export_image_bytes(decoded_data: Vec, decoded_ext: &str) -> (Vec, String, bool, bool) { +pub fn export_image_bytes( + decoded_data: Vec, + decoded_ext: &str, +) -> (Vec, String, bool, bool) { if decoded_ext != "wxgf" { return (decoded_data, decoded_ext.to_string(), false, false); } diff --git a/crates/wx-cli/src/cmd/export_task.rs b/crates/wx-cli/src/cmd/export_task.rs index de3a3a1..8d074ab 100644 --- a/crates/wx-cli/src/cmd/export_task.rs +++ b/crates/wx-cli/src/cmd/export_task.rs @@ -197,10 +197,9 @@ impl VoiceConnectionPool { fn open_all(&self) -> Vec { let mut conns = Vec::new(); for path in &self.db_paths { - if let Ok(conn) = Connection::open_with_flags( - path, - rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, - ) { + if let Ok(conn) = + Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY) + { conns.push(conn); } } @@ -209,7 +208,7 @@ impl VoiceConnectionPool { pub fn with_connections(&self, f: impl FnOnce(&[Connection]) -> R) -> R { thread_local! { - static CONNS: RefCell)>> = RefCell::new(None); + static CONNS: RefCell)>> = const { RefCell::new(None) }; } CONNS.with(|cell| { let mut borrow = cell.borrow_mut(); @@ -242,16 +241,12 @@ impl HardlinkConnectionPool { } fn open(&self) -> Option { - Connection::open_with_flags( - &self.db_path, - rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .ok() + Connection::open_with_flags(&self.db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY).ok() } pub fn with_connection(&self, f: impl FnOnce(&Connection) -> R) -> Option { thread_local! { - static CONN: RefCell> = RefCell::new(None); + static CONN: RefCell> = const { RefCell::new(None) }; } CONN.with(|cell| { let mut borrow = cell.borrow_mut(); @@ -299,6 +294,7 @@ pub struct DupMap { // --------------------------------------------------------------------------- /// Build shared context from account/session info (pre-compute stage). +#[allow(clippy::too_many_arguments)] pub fn build_shared_context( attach_dir: PathBuf, media_dir: PathBuf, @@ -432,12 +428,7 @@ pub fn dedup(tasks: Vec) -> (Vec, DupMap) { unique.push(task); } - ( - unique, - DupMap { - duplicates, - }, - ) + (unique, DupMap { duplicates }) } /// Default rayon thread pool size: min(num_cpus, 4). @@ -469,7 +460,12 @@ pub fn resolve_parallel( batches.entry(task.kind()).or_default().push(task); } - let order = [TaskKind::Image, TaskKind::Voice, TaskKind::Video, TaskKind::File]; + let order = [ + TaskKind::Image, + TaskKind::Voice, + TaskKind::Video, + TaskKind::File, + ]; let mut all_results = Vec::new(); let mut all_errors = ErrorSummary::default(); @@ -531,7 +527,10 @@ pub fn resolve_parallel( fn resolve_one(task: &MediaTask, ctx: &SharedContext) -> ResolvedAsset { match task { MediaTask::Image { md5, msg_index } => resolve_image(md5, *msg_index, ctx), - MediaTask::Voice { server_id, msg_index } => resolve_voice(*server_id, *msg_index, ctx), + MediaTask::Voice { + server_id, + msg_index, + } => resolve_voice(*server_id, *msg_index, ctx), MediaTask::Video { md5, create_time, @@ -668,7 +667,8 @@ fn resolve_voice(server_id: i64, msg_index: usize, ctx: &SharedContext) -> Resol let blob = ctx.voice_pool.with_connections(|conns| { for conn in conns { - if let Ok(b) = wx_media::extract_voice_with_conn_hint(conn, &svr_id, chat_name_id_hint) { + if let Ok(b) = wx_media::extract_voice_with_conn_hint(conn, &svr_id, chat_name_id_hint) + { return Some(b); } } @@ -750,9 +750,9 @@ fn resolve_video( ctx: &SharedContext, ) -> ResolvedAsset { // Try hardlink DB first - let hardlink_result = ctx.hardlink_pool.with_connection(|conn| { - wx_media::query_hardlink_with_conn(conn, "video", md5) - }); + let hardlink_result = ctx + .hardlink_pool + .with_connection(|conn| wx_media::query_hardlink_with_conn(conn, "video", md5)); let entries = match hardlink_result { Some(Ok(e)) => Some(e), @@ -860,9 +860,9 @@ fn resolve_file( ctx: &SharedContext, ) -> ResolvedAsset { // Try hardlink DB first - let hardlink_result = ctx.hardlink_pool.with_connection(|conn| { - wx_media::query_hardlink_with_conn(conn, "file", md5) - }); + let hardlink_result = ctx + .hardlink_pool + .with_connection(|conn| wx_media::query_hardlink_with_conn(conn, "file", md5)); let entries = match hardlink_result { Some(Ok(e)) => Some(e), @@ -889,7 +889,7 @@ fn resolve_file( let filename = format!("{}_{}", md5, entry.file_name); if ctx.write_gate.claim(&filename) { let out_path = ctx.output_media_dir.join(&filename); - if let Err(e) = std::fs::copy(&source, &out_path) { + if let Err(e) = std::fs::copy(source, &out_path) { return ResolvedAsset { msg_index, asset: None, @@ -973,23 +973,19 @@ pub fn collect( let mut errors = ErrorSummary::default(); // Build index from results by msg_index - let mut by_index: HashMap, Vec, Option)> = - HashMap::new(); + let mut by_index: HashMap, Vec)> = HashMap::new(); for r in results { if let Some(e) = r.error { errors.errors.push(e); } - by_index.insert( - r.msg_index, - (r.asset, r.tags, None), - ); + by_index.insert(r.msg_index, (r.asset, r.tags)); } // Place canonical results — count tags always, copy asset only when present. // Matches old MediaBridge: SkippedVideo/SkippedFile stats counted unconditionally; // image stats (ThumbnailImage, WxgfTranscoded, WxgfFallback) also counted // because canonical always does the full resolve. - for (msg_idx, (asset, tags, _)) in &by_index { + for (msg_idx, (asset, tags)) in &by_index { apply_tags(&mut stats, tags); if let Some(a) = asset { media_map[*msg_idx].push(a.clone()); @@ -1003,8 +999,12 @@ pub fn collect( // This two-step approach matches old MediaBridge behavior where skipped/fallback // stats were counted regardless of dedup, but image stats only counted once. for (dup_msg_idx, canonical_msg_idx) in &dup_map.duplicates { - if let Some((asset, tags, _)) = by_index.get(canonical_msg_idx) { - let dup_tags: Vec = tags.iter().copied().filter(|t| t.counts_on_duplicate()).collect(); + if let Some((asset, tags)) = by_index.get(canonical_msg_idx) { + let dup_tags: Vec = tags + .iter() + .copied() + .filter(|t| t.counts_on_duplicate()) + .collect(); apply_tags(&mut stats, &dup_tags); if let Some(a) = asset { media_map[*dup_msg_idx].push(a.clone()); @@ -1416,7 +1416,10 @@ mod tests { // Second task: Voice assert!(matches!( &tasks[1], - MediaTask::Voice { server_id: 2, msg_index: 1 } + MediaTask::Voice { + server_id: 2, + msg_index: 1 + } )); } @@ -1446,7 +1449,11 @@ mod tests { let encrypted: Vec = wxgf.iter().map(|b| b ^ xor_key).collect(); let username_hash = format!("{:x}", wx_media::md5_hash(talker.as_bytes())); - let img_dir = root.join("attach").join(&username_hash).join("2026-03").join("Img"); + let img_dir = root + .join("attach") + .join(&username_hash) + .join("2026-03") + .join("Img"); std::fs::create_dir_all(&img_dir).unwrap(); std::fs::write(img_dir.join(format!("{md5}.dat")), &encrypted).unwrap(); @@ -1572,10 +1579,7 @@ mod tests { let silk = sample_silk(); create_voice_media_db( &media_dir.join("media_0.db"), - &[ - (55, 1000, 1, 101, &silk), - (55, 1001, 2, 102, &silk), - ], + &[(55, 1000, 1, 101, &silk), (55, 1001, 2, 102, &silk)], ); let ctx = Arc::new(build_shared_context( diff --git a/crates/wx-cli/src/cmd/key.rs b/crates/wx-cli/src/cmd/key.rs index 59681c5..1a5c29f 100644 --- a/crates/wx-cli/src/cmd/key.rs +++ b/crates/wx-cli/src/cmd/key.rs @@ -64,10 +64,7 @@ pub async fn cmd_key_extract(timeout_secs: u64) -> Result<(), Box Result<(), Box> { // Scan process memory. eprintln!("Scanning WeChat process memory..."); - let results = - wx_keychain::capture_key_mach(pid, &accounts, &wx_decrypt::MACOS_4_1_7_31)?; + let results = wx_keychain::capture_key_mach(pid, &accounts, &wx_decrypt::MACOS_4_1_7_31)?; // Count total pairs across all results let total_pairs: usize = results @@ -126,18 +122,15 @@ pub fn cmd_key_scan() -> Result<(), Box> { for r in &results { let matched = &r.matched_account; - let nickname = wx_keychain::resolve_nickname( - &matched.data_dir, - &r.key_material, - &matched.base_wxid, - ) - .unwrap_or_else(|e| { - eprintln!( - " Warning: nickname resolution failed for {}: {e}", - matched.account_id - ); - None - }); + let nickname = + wx_keychain::resolve_nickname(&matched.data_dir, &r.key_material, &matched.base_wxid) + .unwrap_or_else(|e| { + eprintln!( + " Warning: nickname resolution failed for {}: {e}", + matched.account_id + ); + None + }); let pairs = match &r.key_material { wx_decrypt::KeyMaterial::EncKeys(pairs) => pairs, @@ -173,10 +166,7 @@ pub fn cmd_key_scan() -> Result<(), Box> { } store.save_default()?; - eprintln!( - "Keys saved to {:?}", - wx_keychain::KeyStore::default_path()? - ); + eprintln!("Keys saved to {:?}", wx_keychain::KeyStore::default_path()?); Ok(()) } diff --git a/crates/wx-cli/src/cmd/paths.rs b/crates/wx-cli/src/cmd/paths.rs index bcbf2c0..b4b8ea8 100644 --- a/crates/wx-cli/src/cmd/paths.rs +++ b/crates/wx-cli/src/cmd/paths.rs @@ -39,7 +39,13 @@ fn print_paths_table(summary: &PathsSummary) { } else { "[missing]" }; - println!("{: { - match wx_db::native_fts::search_message_fts( - &conn, - keyword, - effective_limit, - offset, - ) { + match wx_db::native_fts::search_message_fts(&conn, keyword, effective_limit, offset) + { Ok(result) => { return native_fts_envelope( result, diff --git a/crates/wx-cli/src/cmd/serve/bridge.rs b/crates/wx-cli/src/cmd/serve/bridge.rs index 389e971..9de9fda 100644 --- a/crates/wx-cli/src/cmd/serve/bridge.rs +++ b/crates/wx-cli/src/cmd/serve/bridge.rs @@ -23,10 +23,7 @@ struct BridgeState { startup_watermark: i64, } -fn should_broadcast_talker( - visibility: &wx_context::VisibilityIndex, - talker: &str, -) -> bool { +fn should_broadcast_talker(visibility: &wx_context::VisibilityIndex, talker: &str) -> bool { !visibility.is_hidden_talker(talker) } @@ -407,25 +404,40 @@ mod tests { #[test] fn enrich_messages_filters_hidden_sender_in_group() { - let visibility = VisibilityIndex::build( - &["wxid_spam".to_string()], - &[], - &ContactResolver::empty(), - ); + let visibility = + VisibilityIndex::build(&["wxid_spam".to_string()], &[], &ContactResolver::empty()); let msgs = vec![ wx_db::Message { - sort_seq: 1, server_id: 1, msg_type: 1, sub_type: 0, - sender: "wxid_spam".to_string(), talker: "group@chatroom".to_string(), - create_time: 100, content: wx_db::MessageContent::Text("spam".into()), status: 0, + sort_seq: 1, + server_id: 1, + msg_type: 1, + sub_type: 0, + sender: "wxid_spam".to_string(), + talker: "group@chatroom".to_string(), + create_time: 100, + content: wx_db::MessageContent::Text("spam".into()), + status: 0, }, wx_db::Message { - sort_seq: 2, server_id: 2, msg_type: 1, sub_type: 0, - sender: "wxid_normal".to_string(), talker: "group@chatroom".to_string(), - create_time: 101, content: wx_db::MessageContent::Text("hello".into()), status: 0, + sort_seq: 2, + server_id: 2, + msg_type: 1, + sub_type: 0, + sender: "wxid_normal".to_string(), + talker: "group@chatroom".to_string(), + create_time: 101, + content: wx_db::MessageContent::Text("hello".into()), + status: 0, }, ]; - let result = enrich_messages(msgs, "wxid_me", &ContactResolver::empty(), "group@chatroom", &visibility); + let result = enrich_messages( + msgs, + "wxid_me", + &ContactResolver::empty(), + "group@chatroom", + &visibility, + ); assert_eq!(result.len(), 1, "hidden sender message should be filtered"); assert_eq!(result[0].message.sender, "wxid_normal"); } @@ -433,11 +445,8 @@ mod tests { #[test] fn session_sender_redaction_in_bridge() { use crate::schema::project_session_sender; - let visibility = VisibilityIndex::build( - &["wxid_spam".to_string()], - &[], - &ContactResolver::empty(), - ); + let visibility = + VisibilityIndex::build(&["wxid_spam".to_string()], &[], &ContactResolver::empty()); let ev = wx_monitor::SessionEvent { username: "group@chatroom".to_string(), sort_timestamp: 1, @@ -448,7 +457,8 @@ mod tests { last_msg_sender: Some("wxid_spam".to_string()), last_sender_display_name: Some("Spammer".to_string()), }; - let mut enriched = crate::schema::enrich_session_event(ev, "wxid_me", &ContactResolver::empty()); + let mut enriched = + crate::schema::enrich_session_event(ev, "wxid_me", &ContactResolver::empty()); project_session_sender(&mut enriched, &visibility); assert_eq!(enriched.session.summary, "[消息已隐藏]"); diff --git a/crates/wx-cli/src/cmd/serve/media.rs b/crates/wx-cli/src/cmd/serve/media.rs index ed2038a..9bded8e 100644 --- a/crates/wx-cli/src/cmd/serve/media.rs +++ b/crates/wx-cli/src/cmd/serve/media.rs @@ -8,13 +8,11 @@ use axum::http::HeaderValue; use axum::response::{IntoResponse, Response}; use tower::ServiceExt; use tower_http::services::ServeFile; -use wx_db::{ - open_readonly_connection, Message, MessageContent, MessageQuery, SortOrder, WechatDb, -}; +use wx_db::{open_readonly_connection, Message, MessageContent, MessageQuery, SortOrder, WechatDb}; -use crate::util::{format_month, sanitize_filename}; use super::error::ServeError; use super::state::{AppState, CachedVoicePayload}; +use crate::util::{format_month, sanitize_filename}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MediaFormat { @@ -147,8 +145,9 @@ async fn resolve_media( ))), MessageContent::Voice => { let db_paths = { - let mut cache_guard = state_for_cache.media_db_paths.lock() - .map_err(|e: std::sync::PoisonError<_>| ServeError::Internal(e.to_string()))?; + let mut cache_guard = state_for_cache.media_db_paths.lock().map_err( + |e: std::sync::PoisonError<_>| ServeError::Internal(e.to_string()), + )?; match cache_guard.as_ref() { Some(paths) => paths.clone(), None => { @@ -297,7 +296,7 @@ fn resolve_voice( let mut first_db_error: Option = None; for db_path in db_paths { - let conn = match open_readonly_connection(&db_path, raw_key.as_ref()) { + let conn = match open_readonly_connection(db_path, raw_key.as_ref()) { Ok(conn) => conn, Err(err) => { if first_db_error.is_none() { diff --git a/crates/wx-cli/src/cmd/serve/mod.rs b/crates/wx-cli/src/cmd/serve/mod.rs index f3c67e8..542d5cb 100644 --- a/crates/wx-cli/src/cmd/serve/mod.rs +++ b/crates/wx-cli/src/cmd/serve/mod.rs @@ -280,8 +280,12 @@ pub async fn cmd_serve( hardlink_db_conn, raw_key: acct.raw_key, dat_decrypt, - voice_cache: Arc::new(std::sync::Mutex::new(LruCache::new(NonZeroUsize::new(256).unwrap()))), - image_xor_cache: Arc::new(std::sync::Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap()))), + voice_cache: Arc::new(std::sync::Mutex::new(LruCache::new( + NonZeroUsize::new(256).unwrap(), + ))), + image_xor_cache: Arc::new(std::sync::Mutex::new(LruCache::new( + NonZeroUsize::new(1024).unwrap(), + ))), name2id_cache: Arc::new(std::sync::Mutex::new(None)), media_db_paths: Arc::new(std::sync::Mutex::new(None)), }); diff --git a/crates/wx-cli/src/cmd/serve/refresh.rs b/crates/wx-cli/src/cmd/serve/refresh.rs index a4aadbd..92aab45 100644 --- a/crates/wx-cli/src/cmd/serve/refresh.rs +++ b/crates/wx-cli/src/cmd/serve/refresh.rs @@ -11,6 +11,8 @@ use wx_context::{ }; use wx_db::WechatDb; +type Name2IdCache = Arc>>>; + /// Signal sent to the refresh task. pub enum RefreshTrigger { Refresh, @@ -36,7 +38,7 @@ pub struct RefreshTask { /// Path to FTS DB for reopening. fts_path: Option, /// Cache of name2id mapping — cleared when FTS is reopened. - name2id_cache: Option>>>>, + name2id_cache: Option, /// Cache of media DB paths — cleared on every refresh. media_db_paths: Option>>>>, /// Cached hardlink.db connection — cleared on refresh so it is reopened lazily. @@ -79,7 +81,7 @@ impl RefreshTask { /// Set the caches that should be invalidated on refresh. pub fn with_caches( mut self, - name2id_cache: Option>>>>, + name2id_cache: Option, media_db_paths: Option>>>>, hardlink_db_conn: Option>>>, ) -> Self { diff --git a/crates/wx-cli/src/cmd/server/manager.rs b/crates/wx-cli/src/cmd/server/manager.rs index e584ddb..b11ccc0 100644 --- a/crates/wx-cli/src/cmd/server/manager.rs +++ b/crates/wx-cli/src/cmd/server/manager.rs @@ -20,7 +20,9 @@ use crate::OutputFormat; const START_TIMEOUT: Duration = Duration::from_secs(10); const STOP_TIMEOUT: Duration = Duration::from_secs(5); -fn resolve_app_paths(runtime_root: Option) -> Result> { +fn resolve_app_paths( + runtime_root: Option, +) -> Result> { match runtime_root { Some(root) => Ok(AppPaths::with_runtime_root(root)?), None => Ok(AppPaths::new()?), @@ -120,9 +122,8 @@ pub async fn cmd_server_stop(args: ServerStopArgs) -> Result<(), Box Result<(), Box> { let ap = resolve_app_paths(args.runtime_root.clone())?; - let config = load_launch_config(&ap)?.ok_or( - "no persisted server launch configuration found; run `wx-cli server run` first", - )?; + let config = load_launch_config(&ap)? + .ok_or("no persisted server launch configuration found; run `wx-cli server run` first")?; let stop_args = ServerStopArgs { runtime_root: args.runtime_root.clone(), @@ -286,9 +287,7 @@ fn spawn_worker( command.arg("--runtime-root").arg(root); } - command - .arg("--worker-id") - .arg(worker_id); + command.arg("--worker-id").arg(worker_id); if let Some(key) = &config.key { command.arg("--key").arg(key); diff --git a/crates/wx-cli/src/cmd/sessions.rs b/crates/wx-cli/src/cmd/sessions.rs index fc71b34..bf202ba 100644 --- a/crates/wx-cli/src/cmd/sessions.rs +++ b/crates/wx-cli/src/cmd/sessions.rs @@ -6,7 +6,9 @@ use super::contacts::build_visibility; use super::thin_client::{ThinClientCliArgs, ThinClientOptions}; use crate::output::JsonEnvelope; use crate::schema::{enrich_session, EnrichedSession}; -use crate::util::{effective_limit_all, open_db_core, print_cache_stats, print_detection_note, try_remote_or_local}; +use crate::util::{ + effective_limit_all, open_db_core, print_cache_stats, print_detection_note, try_remote_or_local, +}; use crate::visibility_projection::project_sessions_envelope_enriched; use crate::{OutputFormat, SortOrderArg}; diff --git a/crates/wx-cli/src/cmd/watch.rs b/crates/wx-cli/src/cmd/watch.rs index dbddea2..bcb8885 100644 --- a/crates/wx-cli/src/cmd/watch.rs +++ b/crates/wx-cli/src/cmd/watch.rs @@ -197,15 +197,20 @@ mod tests { #[test] fn watch_text_hidden_sender_shows_placeholder() { use crate::schema::project_session_sender; - let visibility = VisibilityIndex::build( - &["wxid_spam".to_string()], &[], &ContactResolver::empty(), - ); + let visibility = + VisibilityIndex::build(&["wxid_spam".to_string()], &[], &ContactResolver::empty()); let mut enriched = make_enriched_session("group@chatroom", "spam msg", Some("wxid_spam")); project_session_sender(&mut enriched, &visibility); let line = format_watch_line_from_enriched(&enriched, &ContactResolver::empty(), "wxid_me"); - assert!(line.contains("[消息已隐藏]"), "should show placeholder: {line}"); - assert!(!line.contains("wxid_spam"), "should not leak sender wxid: {line}"); + assert!( + line.contains("[消息已隐藏]"), + "should show placeholder: {line}" + ); + assert!( + !line.contains("wxid_spam"), + "should not leak sender wxid: {line}" + ); } #[test] @@ -281,6 +286,7 @@ fn should_emit_event( show_hidden || !visibility.is_hidden_talker(&event.username) } +#[allow(clippy::too_many_arguments)] pub async fn cmd_watch( key_hex: Option, data_dir: Option, diff --git a/crates/wx-cli/src/schema.rs b/crates/wx-cli/src/schema.rs index 7a06b05..ab7bbe5 100644 --- a/crates/wx-cli/src/schema.rs +++ b/crates/wx-cli/src/schema.rs @@ -540,7 +540,11 @@ mod tests { } } - fn make_enriched(sender: &str, talker: &str, content: wx_db::MessageContent) -> EnrichedMessage { + fn make_enriched( + sender: &str, + talker: &str, + content: wx_db::MessageContent, + ) -> EnrichedMessage { let msg = make_message(sender, talker, content); let snippet = format_content(&msg); EnrichedMessage { @@ -562,21 +566,33 @@ mod tests { #[test] fn project_message_item_non_group_does_not_filter() { let vis = vis_with_hidden_persons(&["wxid_spam"]); - let msg = make_enriched("wxid_spam", "wxid_spam", wx_db::MessageContent::Text("hi".into())); + let msg = make_enriched( + "wxid_spam", + "wxid_spam", + wx_db::MessageContent::Text("hi".into()), + ); assert!(project_message_item(msg, "wxid_spam", &vis).is_some()); } #[test] fn project_message_item_group_hidden_sender_filtered() { let vis = vis_with_hidden_persons(&["wxid_spam"]); - let msg = make_enriched("wxid_spam", "group@chatroom", wx_db::MessageContent::Text("spam".into())); + let msg = make_enriched( + "wxid_spam", + "group@chatroom", + wx_db::MessageContent::Text("spam".into()), + ); assert!(project_message_item(msg, "group@chatroom", &vis).is_none()); } #[test] fn project_message_item_group_visible_sender_kept() { let vis = vis_with_hidden_persons(&["wxid_spam"]); - let msg = make_enriched("wxid_normal", "group@chatroom", wx_db::MessageContent::Text("hi".into())); + let msg = make_enriched( + "wxid_normal", + "group@chatroom", + wx_db::MessageContent::Text("hi".into()), + ); assert!(project_message_item(msg, "group@chatroom", &vis).is_some()); } @@ -679,8 +695,16 @@ mod tests { fn project_message_items_show_hidden_bypasses() { let vis = vis_with_hidden_persons(&["wxid_spam"]); let items = vec![ - make_enriched("wxid_spam", "group@chatroom", wx_db::MessageContent::Text("spam".into())), - make_enriched("wxid_normal", "group@chatroom", wx_db::MessageContent::Text("hi".into())), + make_enriched( + "wxid_spam", + "group@chatroom", + wx_db::MessageContent::Text("spam".into()), + ), + make_enriched( + "wxid_normal", + "group@chatroom", + wx_db::MessageContent::Text("hi".into()), + ), ]; let result = project_message_items(items, "group@chatroom", &vis, true); assert_eq!(result.len(), 2); @@ -690,8 +714,16 @@ mod tests { fn project_message_items_filters_hidden_sender() { let vis = vis_with_hidden_persons(&["wxid_spam"]); let items = vec![ - make_enriched("wxid_spam", "group@chatroom", wx_db::MessageContent::Text("spam".into())), - make_enriched("wxid_normal", "group@chatroom", wx_db::MessageContent::Text("hi".into())), + make_enriched( + "wxid_spam", + "group@chatroom", + wx_db::MessageContent::Text("spam".into()), + ), + make_enriched( + "wxid_normal", + "group@chatroom", + wx_db::MessageContent::Text("hi".into()), + ), ]; let result = project_message_items(items, "group@chatroom", &vis, false); assert_eq!(result.len(), 1); @@ -760,6 +792,9 @@ mod tests { }; project_session_sender(&mut session, &vis); assert_eq!(session.session.summary, "normal message"); - assert_eq!(session.session.last_msg_sender.as_deref(), Some("wxid_normal")); + assert_eq!( + session.session.last_msg_sender.as_deref(), + Some("wxid_normal") + ); } } diff --git a/crates/wx-cli/src/util.rs b/crates/wx-cli/src/util.rs index 8c94627..54442b7 100644 --- a/crates/wx-cli/src/util.rs +++ b/crates/wx-cli/src/util.rs @@ -3,6 +3,12 @@ use std::path::PathBuf; use crate::cmd::thin_client::{ThinClient, ThinClientError, ThinClientOptions}; use wx_context::{AccountContext, DecryptRequest, DecryptStats, PersistentCache}; +type OpenDbAllResult = ( + wx_db::WechatDb, + Option, + Option, +); + /// Open a WechatDb: direct encrypted open if raw_key available, else decrypt+cache (core only). pub fn open_db_core( acct: &AccountContext, @@ -27,14 +33,7 @@ pub fn open_db_core( pub fn open_db_all( acct: &AccountContext, progress: impl Fn(wx_context::DecryptProgress) + Send + Sync, -) -> Result< - ( - wx_db::WechatDb, - Option, - Option, - ), - Box, -> { +) -> Result> { if acct.raw_key.is_some() { eprintln!("Direct encrypted open (SQLCipher)"); let db = wx_context::open_encrypted_db(acct)?; @@ -249,14 +248,8 @@ mod tests { #[test] fn effective_limit_all_true_returns_max() { - assert_eq!( - effective_limit_all(true, 0), - wx_db::MAX_QUERY_LIMIT - ); - assert_eq!( - effective_limit_all(true, 50), - wx_db::MAX_QUERY_LIMIT - ); + assert_eq!(effective_limit_all(true, 0), wx_db::MAX_QUERY_LIMIT); + assert_eq!(effective_limit_all(true, 50), wx_db::MAX_QUERY_LIMIT); } #[test] diff --git a/crates/wx-cli/tests/ignore-filters.rs b/crates/wx-cli/tests/ignore-filters.rs index fe1dfb0..d4752ed 100644 --- a/crates/wx-cli/tests/ignore-filters.rs +++ b/crates/wx-cli/tests/ignore-filters.rs @@ -95,9 +95,15 @@ fn ignore_tags_hide_matching_contact_at_both_talker_and_sender_level() { "json", ], ); - let items = group_messages["items"].as_array().expect("query items array"); + let items = group_messages["items"] + .as_array() + .expect("query items array"); // The only group message is from wxid_hidden_tagged; it should be filtered out - assert_eq!(items.len(), 0, "tagged contact's group messages should be sender-level filtered: {group_messages}"); + assert_eq!( + items.len(), + 0, + "tagged contact's group messages should be sender-level filtered: {group_messages}" + ); // Session should show placeholder for group where last sender is tagged let sessions = run_json( @@ -112,9 +118,7 @@ fn ignore_tags_hide_matching_contact_at_both_talker_and_sender_level() { "json", ], ); - let items = sessions["items"] - .as_array() - .expect("sessions items array"); + let items = sessions["items"].as_array().expect("sessions items array"); let group_session = items .iter() .find(|item| item["username"].as_str() == Some(TALKER_GROUP)); @@ -331,16 +335,8 @@ fn create_encrypted_contact_db(path: &Path, raw_key: &[u8; 32]) { ) .expect("insert bob"); - let tagged_extra = encode_extra_buffer_for_test( - None, - None, - None, - None, - None, - None, - None, - Some("1"), - ); + let tagged_extra = + encode_extra_buffer_for_test(None, None, None, None, None, None, None, Some("1")); conn.execute( "INSERT INTO contact (username, nick_name, extra_buffer) VALUES (?1, ?2, ?3)", params![TALKER_TAGGED, "Sensitive Person", tagged_extra], @@ -451,8 +447,11 @@ fn create_encrypted_message_db(path: &Path, raw_key: &[u8; 32]) { group = TABLE_GROUP, ), |conn| { - conn.execute("INSERT INTO Timestamp VALUES (?1)", params![1_700_000_000_i64]) - .expect("insert timestamp"); + conn.execute( + "INSERT INTO Timestamp VALUES (?1)", + params![1_700_000_000_i64], + ) + .expect("insert timestamp"); conn.execute( "INSERT INTO Name2Id VALUES (?1, ?2)", params![1_i64, TALKER_ALICE], @@ -729,8 +728,11 @@ fn create_sender_account(root: &Path) { group = TABLE_GROUP_SENDER, ), |conn| { - conn.execute("INSERT INTO Timestamp VALUES (?1)", params![1_700_000_000_i64]) - .expect("insert timestamp"); + conn.execute( + "INSERT INTO Timestamp VALUES (?1)", + params![1_700_000_000_i64], + ) + .expect("insert timestamp"); conn.execute( "INSERT INTO Name2Id VALUES (?1, ?2)", params![1_i64, TALKER_ALICE], @@ -754,8 +756,15 @@ fn create_sender_account(root: &Path) { table = TABLE_ALICE ), params![ - 100_i64, 3001_i64, 1_i64, 1_i64, 1_700_000_301_i64, - b"private hello" as &[u8], None::>, 0_i32, None::, + 100_i64, + 3001_i64, + 1_i64, + 1_i64, + 1_700_000_301_i64, + b"private hello" as &[u8], + None::>, + 0_i32, + None::, ], ) .expect("insert alice private message"); @@ -767,8 +776,15 @@ fn create_sender_account(root: &Path) { table = TABLE_GROUP_SENDER ), params![ - 200_i64, 4001_i64, 1_i64, 1_i64, 1_700_000_101_i64, - b"alice says hello in group" as &[u8], None::>, 0_i32, None::, + 200_i64, + 4001_i64, + 1_i64, + 1_i64, + 1_700_000_101_i64, + b"alice says hello in group" as &[u8], + None::>, + 0_i32, + None::, ], ) .expect("insert alice group message"); @@ -780,8 +796,15 @@ fn create_sender_account(root: &Path) { table = TABLE_GROUP_SENDER ), params![ - 210_i64, 4002_i64, 1_i64, 5_i64, 1_700_000_102_i64, - b"spam content" as &[u8], None::>, 0_i32, None::, + 210_i64, + 4002_i64, + 1_i64, + 5_i64, + 1_700_000_102_i64, + b"spam content" as &[u8], + None::>, + 0_i32, + None::, ], ) .expect("insert spam group message"); @@ -795,10 +818,15 @@ fn create_sender_account(root: &Path) { table = TABLE_GROUP_SENDER ), params![ - 220_i64, 4003_i64, + 220_i64, + 4003_i64, quote_local_type, - 1_i64, 1_700_000_103_i64, - quote_xml.as_bytes(), None::>, 0_i32, None::, + 1_i64, + 1_700_000_103_i64, + quote_xml.as_bytes(), + None::>, + 0_i32, + None::, ], ) .expect("insert quote message"); @@ -814,22 +842,38 @@ fn sender_hiding_filters_hidden_sender_messages_in_group() { let result = run_json( fixture.path(), &[ - "query", TALKER_GROUP, - "--data-dir", senders_dir.as_str(), - "--key", TEST_KEY_HEX, - "--format", "json", + "query", + TALKER_GROUP, + "--data-dir", + senders_dir.as_str(), + "--key", + TEST_KEY_HEX, + "--format", + "json", ], ); let items = result["items"].as_array().expect("query items array"); // spam message (server_id=4002) should be filtered out - let senders: Vec<&str> = items.iter().map(|i| i["sender"].as_str().unwrap()).collect(); - assert!(!senders.contains(&TALKER_SPAM), "hidden sender message should be filtered: {result}"); - assert!(senders.contains(&TALKER_ALICE), "visible sender should remain: {result}"); + let senders: Vec<&str> = items + .iter() + .map(|i| i["sender"].as_str().unwrap()) + .collect(); + assert!( + !senders.contains(&TALKER_SPAM), + "hidden sender message should be filtered: {result}" + ); + assert!( + senders.contains(&TALKER_ALICE), + "visible sender should remain: {result}" + ); // paging.total should NOT change (DB-level count) // paging.returned should reflect filtered items - assert_eq!(result["paging"]["returned"].as_u64().unwrap(), items.len() as u64); + assert_eq!( + result["paging"]["returned"].as_u64().unwrap(), + items.len() as u64 + ); } #[test] @@ -841,14 +885,22 @@ fn sender_hiding_does_not_affect_private_chat() { let result = run_json( fixture.path(), &[ - "query", TALKER_ALICE, - "--data-dir", senders_dir.as_str(), - "--key", TEST_KEY_HEX, - "--format", "json", + "query", + TALKER_ALICE, + "--data-dir", + senders_dir.as_str(), + "--key", + TEST_KEY_HEX, + "--format", + "json", ], ); let items = result["items"].as_array().expect("query items array"); - assert_eq!(items.len(), 1, "private chat should not be filtered: {result}"); + assert_eq!( + items.len(), + 1, + "private chat should not be filtered: {result}" + ); } #[test] @@ -859,27 +911,51 @@ fn sender_hiding_redacts_quote_referring_hidden_sender() { let result = run_json( fixture.path(), &[ - "query", TALKER_GROUP, - "--data-dir", senders_dir.as_str(), - "--key", TEST_KEY_HEX, - "--format", "json", + "query", + TALKER_GROUP, + "--data-dir", + senders_dir.as_str(), + "--key", + TEST_KEY_HEX, + "--format", + "json", ], ); let items = result["items"].as_array().expect("query items array"); // Find the quote message (server_id=4003) let quote = items.iter().find(|i| i["server_id"].as_i64() == Some(4003)); - assert!(quote.is_some(), "quote message should be present (alice's reply): items={:?}", items.iter().map(|i| i["server_id"].as_i64()).collect::>()); + assert!( + quote.is_some(), + "quote message should be present (alice's reply): items={:?}", + items + .iter() + .map(|i| i["server_id"].as_i64()) + .collect::>() + ); let quote = quote.unwrap(); // refer_sender and refer_content should be null (redacted) let q = "e["content"]["Quote"]; - assert!(q["refer_sender"].is_null(), "refer_sender should be redacted: {quote}"); - assert!(q["refer_content"].is_null(), "refer_content should be redacted: {quote}"); + assert!( + q["refer_sender"].is_null(), + "refer_sender should be redacted: {quote}" + ); + assert!( + q["refer_content"].is_null(), + "refer_content should be redacted: {quote}" + ); // reply_text should be preserved - assert!(q["reply_text"].as_str().unwrap().contains("my reply"), "reply_text should be preserved: {quote}"); + assert!( + q["reply_text"].as_str().unwrap().contains("my reply"), + "reply_text should be preserved: {quote}" + ); // raw_xml should be cleared - assert_eq!(q["raw_xml"].as_str(), Some(""), "raw_xml should be empty: {quote}"); + assert_eq!( + q["raw_xml"].as_str(), + Some(""), + "raw_xml should be empty: {quote}" + ); } #[test] @@ -890,19 +966,33 @@ fn sender_hiding_show_hidden_restores_all_messages_and_quotes() { let result = run_json( fixture.path(), &[ - "query", TALKER_GROUP, - "--data-dir", senders_dir.as_str(), - "--key", TEST_KEY_HEX, + "query", + TALKER_GROUP, + "--data-dir", + senders_dir.as_str(), + "--key", + TEST_KEY_HEX, "--show-hidden", - "--format", "json", + "--format", + "json", ], ); let items = result["items"].as_array().expect("query items array"); - assert_eq!(items.len(), 3, "show_hidden should restore all 3 messages: {result}"); + assert_eq!( + items.len(), + 3, + "show_hidden should restore all 3 messages: {result}" + ); // Quote should have refer_sender intact - let quote = items.iter().find(|i| i["server_id"].as_i64() == Some(4003)).unwrap(); - assert!(!quote["content"]["Quote"]["refer_sender"].is_null(), "refer_sender should be intact with show_hidden: {quote}"); + let quote = items + .iter() + .find(|i| i["server_id"].as_i64() == Some(4003)) + .unwrap(); + assert!( + !quote["content"]["Quote"]["refer_sender"].is_null(), + "refer_sender should be intact with show_hidden: {quote}" + ); } #[test] @@ -914,22 +1004,37 @@ fn sender_hiding_session_placeholder_when_last_sender_hidden() { fixture.path(), &[ "sessions", - "--data-dir", senders_dir.as_str(), - "--key", TEST_KEY_HEX, - "--format", "json", + "--data-dir", + senders_dir.as_str(), + "--key", + TEST_KEY_HEX, + "--format", + "json", ], ); let items = sessions["items"].as_array().expect("sessions items array"); - let group_session = items.iter().find(|i| i["username"].as_str() == Some(TALKER_GROUP)); - assert!(group_session.is_some(), "group session should be visible: {sessions}"); + let group_session = items + .iter() + .find(|i| i["username"].as_str() == Some(TALKER_GROUP)); + assert!( + group_session.is_some(), + "group session should be visible: {sessions}" + ); let group_session = group_session.unwrap(); // Summary should be placeholder, sender fields should be null - assert_eq!(group_session["summary"].as_str(), Some("[消息已隐藏]"), - "summary should be placeholder: {group_session}"); - assert!(group_session["last_msg_sender"].is_null(), - "last_msg_sender should be null: {group_session}"); - assert!(group_session["direction"].is_null(), - "direction should be null: {group_session}"); + assert_eq!( + group_session["summary"].as_str(), + Some("[消息已隐藏]"), + "summary should be placeholder: {group_session}" + ); + assert!( + group_session["last_msg_sender"].is_null(), + "last_msg_sender should be null: {group_session}" + ); + assert!( + group_session["direction"].is_null(), + "direction should be null: {group_session}" + ); } diff --git a/crates/wx-cli/tests/serve-media.rs b/crates/wx-cli/tests/serve-media.rs index c6dad3e..4c4696a 100644 --- a/crates/wx-cli/tests/serve-media.rs +++ b/crates/wx-cli/tests/serve-media.rs @@ -98,8 +98,10 @@ fn serve_media_visible_sender_in_group_with_hidden_persons_not_visibility_blocke let body = String::from_utf8_lossy(&response.body); // The media endpoint was reached (not blocked by visibility) but the asset // isn't on disk. Acceptable. - assert!(!body.contains("message not found"), - "visible sender should NOT get visibility 404: {body}"); + assert!( + !body.contains("message not found"), + "visible sender should NOT get visibility 404: {body}" + ); } // If 200, even better — means asset resolution succeeded } @@ -317,10 +319,7 @@ fn spawn_test_server_with_env(envs: &[(&str, &str)]) -> TestServer { spawn_test_server_with_setup(envs, &[]) } -fn spawn_test_server_with_setup( - envs: &[(&str, &str)], - hidden_contacts: &[&str], -) -> TestServer { +fn spawn_test_server_with_setup(envs: &[(&str, &str)], hidden_contacts: &[&str]) -> TestServer { let fixture = create_fixture(); if !hidden_contacts.is_empty() { write_settings(fixture.path(), hidden_contacts); @@ -864,8 +863,8 @@ fn create_encrypted_message_db(path: &Path, raw_key: &[u8; 32]) { params![ 900_i64, 7001_i64, - 3_i64, // msg_type=3 (image) - 3_i64, // real_sender_id=3 → HIDDEN_SENDER + 3_i64, // msg_type=3 (image) + 3_i64, // real_sender_id=3 → HIDDEN_SENDER 1_700_000_900_i64, Vec::::new(), group_image_info, @@ -885,8 +884,8 @@ fn create_encrypted_message_db(path: &Path, raw_key: &[u8; 32]) { params![ 910_i64, 7002_i64, - 3_i64, // msg_type=3 (image) - 1_i64, // real_sender_id=1 → TALKER (visible) + 3_i64, // msg_type=3 (image) + 1_i64, // real_sender_id=1 → TALKER (visible) 1_700_000_910_i64, Vec::::new(), group_visible_info, diff --git a/crates/wx-context/src/cache.rs b/crates/wx-context/src/cache.rs index 3de7dee..666b74c 100644 --- a/crates/wx-context/src/cache.rs +++ b/crates/wx-context/src/cache.rs @@ -773,10 +773,7 @@ mod tests { &tmp.path().join("cache").join("test.db"), ) .unwrap_err(); - assert!(matches!( - err, - wx_decrypt::DecryptError::NoMatchingEncKey - )); + assert!(matches!(err, wx_decrypt::DecryptError::NoMatchingEncKey)); } #[test] diff --git a/crates/wx-context/src/visibility.rs b/crates/wx-context/src/visibility.rs index 0d60af6..0df7757 100644 --- a/crates/wx-context/src/visibility.rs +++ b/crates/wx-context/src/visibility.rs @@ -77,8 +77,7 @@ impl VisibilityIndex { /// /// Hidden talkers OR hidden senders in visible groups cannot access media. pub fn allows_media_for_sender(&self, talker: &str, sender: &str) -> bool { - !self.hidden_persons.contains(talker) - && !self.is_hidden_sender_in_group(talker, sender) + !self.hidden_persons.contains(talker) && !self.is_hidden_sender_in_group(talker, sender) } } @@ -230,12 +229,9 @@ mod tests { #[test] fn allows_media_for_sender_covers_both_levels() { let idx = VisibilityIndex { - hidden_persons: vec![ - "hidden_group@chatroom".to_string(), - "wxid_spam".to_string(), - ] - .into_iter() - .collect(), + hidden_persons: vec!["hidden_group@chatroom".to_string(), "wxid_spam".to_string()] + .into_iter() + .collect(), }; // Hidden talker → no media assert!(!idx.allows_media_for_sender("hidden_group@chatroom", "wxid_anyone")); diff --git a/crates/wx-db/src/decode.rs b/crates/wx-db/src/decode.rs index 75ed4d7..af3f448 100644 --- a/crates/wx-db/src/decode.rs +++ b/crates/wx-db/src/decode.rs @@ -302,11 +302,8 @@ mod tests { #[test] fn non_group_returns_fallback_unchanged() { - let (sender, content) = parse_group_sender( - false, - "hello world".to_string(), - "fallback".to_string(), - ); + let (sender, content) = + parse_group_sender(false, "hello world".to_string(), "fallback".to_string()); assert_eq!(sender, "fallback"); assert_eq!(content, "hello world"); } @@ -335,22 +332,16 @@ mod tests { #[test] fn group_empty_content_after_separator() { - let (sender, content) = parse_group_sender( - true, - "wxid_abc:\n".to_string(), - "fallback".to_string(), - ); + let (sender, content) = + parse_group_sender(true, "wxid_abc:\n".to_string(), "fallback".to_string()); assert_eq!(sender, "wxid_abc"); assert_eq!(content, ""); } #[test] fn group_only_colon_before_newline() { - let (sender, content) = parse_group_sender( - true, - ":\nsome content".to_string(), - "fallback".to_string(), - ); + let (sender, content) = + parse_group_sender(true, ":\nsome content".to_string(), "fallback".to_string()); assert_eq!(sender, ""); assert_eq!(content, "some content"); } diff --git a/crates/wx-db/src/fts.rs b/crates/wx-db/src/fts.rs index 8e73c20..ab1b079 100644 --- a/crates/wx-db/src/fts.rs +++ b/crates/wx-db/src/fts.rs @@ -6,7 +6,9 @@ use rusqlite::types::ValueRef; use rusqlite::Connection; use serde::Serialize; -use crate::decode::{check_column_exists, decode_content, msg_table_name, parse_content, parse_group_sender}; +use crate::decode::{ + check_column_exists, decode_content, msg_table_name, parse_content, parse_group_sender, +}; use crate::error::DbError; use crate::model::{split_local_type, MessageContent}; use crate::open::WechatDb; @@ -414,8 +416,7 @@ impl WechatDb { )?; for shard in &self.shards { - let shard_conn = - WechatDb::open_shard_with_key(shard, self.sqlcipher_key.as_ref())?; + let shard_conn = WechatDb::open_shard_with_key(shard, self.sqlcipher_key.as_ref())?; // List Msg_* tables in this shard let mut table_stmt = shard_conn.prepare( @@ -574,7 +575,8 @@ impl WechatDb { let decoded_text = decode_content(&raw_content, wcdb_ct)?; // Group sender parsing - let (sender, content_text) = parse_group_sender(is_group, decoded_text, sender_from_name2id); + let (sender, content_text) = + parse_group_sender(is_group, decoded_text, sender_from_name2id); let (msg_type, sub_type) = split_local_type(local_type as i64); diff --git a/crates/wx-db/src/messages.rs b/crates/wx-db/src/messages.rs index 8844f47..8e7ce4f 100644 --- a/crates/wx-db/src/messages.rs +++ b/crates/wx-db/src/messages.rs @@ -323,9 +323,11 @@ impl WechatDb { msg_type_filter: Option, ) -> usize { let result = if let Some(mt) = msg_type_filter { - conn.query_row(sql, [start_time, end_time, mt as i64], |row: &rusqlite::Row<'_>| { - row.get::<_, i64>(0) - }) + conn.query_row( + sql, + [start_time, end_time, mt as i64], + |row: &rusqlite::Row<'_>| row.get::<_, i64>(0), + ) } else { conn.query_row(sql, [start_time, end_time], |row: &rusqlite::Row<'_>| { row.get::<_, i64>(0) diff --git a/crates/wx-db/src/xml_extract.rs b/crates/wx-db/src/xml_extract.rs index 65a4e23..e4d3c76 100644 --- a/crates/wx-db/src/xml_extract.rs +++ b/crates/wx-db/src/xml_extract.rs @@ -530,10 +530,7 @@ mod tests { #[test] fn extract_quote_fromusr_normal() { let xml = r#"replywxid_alicehi"#; - assert_eq!( - extract_quote_fromusr(xml), - Some("wxid_alice".to_string()) - ); + assert_eq!(extract_quote_fromusr(xml), Some("wxid_alice".to_string())); } #[test] @@ -552,19 +549,13 @@ mod tests { fn extract_quote_fromusr_prefers_chatusr_in_group() { // In group chats, is the chatroom ID, is the actual sender let xml = r#"replygroup@chatroomwxid_senderSenderhi"#; - assert_eq!( - extract_quote_fromusr(xml), - Some("wxid_sender".to_string()) - ); + assert_eq!(extract_quote_fromusr(xml), Some("wxid_sender".to_string())); } #[test] fn extract_quote_fromusr_falls_back_to_fromusr_without_chatusr() { // In private chats, only exists (no ) let xml = r#"replywxid_bobBobhi"#; - assert_eq!( - extract_quote_fromusr(xml), - Some("wxid_bob".to_string()) - ); + assert_eq!(extract_quote_fromusr(xml), Some("wxid_bob".to_string())); } } diff --git a/crates/wx-keychain/src/lib.rs b/crates/wx-keychain/src/lib.rs index 8090647..d8e0fb7 100644 --- a/crates/wx-keychain/src/lib.rs +++ b/crates/wx-keychain/src/lib.rs @@ -17,11 +17,11 @@ pub use process::config_dir; pub use process::detect_active_account; pub use process::{ ensure_supported_wechat_version, extract_base_wxid, find_account_dirs, find_account_dirs_under, - find_wechat_pid, is_xwechat_files_root, AccountDirInfo, ActiveAccount, - DetectionSource, SUPPORTED_VERSION, + find_wechat_pid, is_xwechat_files_root, AccountDirInfo, ActiveAccount, DetectionSource, + SUPPORTED_VERSION, }; -pub use wx_decrypt::read_db_salt; pub use store::{AccountKey, EncKeyEntry, KeyStore}; +pub use wx_decrypt::read_db_salt; use std::process::Command; diff --git a/crates/wx-keychain/src/lldb.rs b/crates/wx-keychain/src/lldb.rs index 39c6e12..9e3b4a3 100644 --- a/crates/wx-keychain/src/lldb.rs +++ b/crates/wx-keychain/src/lldb.rs @@ -46,7 +46,11 @@ pub async fn capture_key( // Pre-read salts from all accounts. Skip unreadable DBs. let account_salts: Vec<([u8; 16], &AccountDirInfo)> = accounts .iter() - .filter_map(|a| wx_decrypt::read_db_salt(&a.message_db_path).ok().map(|salt| (salt, a))) + .filter_map(|a| { + wx_decrypt::read_db_salt(&a.message_db_path) + .ok() + .map(|salt| (salt, a)) + }) .collect(); if account_salts.is_empty() { diff --git a/crates/wx-keychain/src/process.rs b/crates/wx-keychain/src/process.rs index 4ccda0c..ee3ed04 100644 --- a/crates/wx-keychain/src/process.rs +++ b/crates/wx-keychain/src/process.rs @@ -138,16 +138,16 @@ fn get_wechat_version() -> Result { /// Shared config directory resolved by `AppPaths`. pub fn config_dir() -> Result { - let ap = wx_paths::AppPaths::new() - .map_err(|e| KeychainError::Other(e.to_string()))?; + let ap = wx_paths::AppPaths::new().map_err(|e| KeychainError::Other(e.to_string()))?; Ok(ap.config_dir()) } /// Default xwechat_files base path. fn default_xwechat_files_base() -> Result { - let ap = wx_paths::AppPaths::new() - .map_err(|e| KeychainError::Other(e.to_string()))?; - Ok(ap.home().join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files")) + let ap = wx_paths::AppPaths::new().map_err(|e| KeychainError::Other(e.to_string()))?; + Ok(ap + .home() + .join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files")) } /// Detect account directories from the filesystem (without WeChat running). @@ -350,7 +350,6 @@ fn tiebreak_by_wal_mtime<'a>(accounts: &[&'a AccountDirInfo]) -> Option<&'a Acco best } - #[cfg(test)] mod tests { use super::*; diff --git a/crates/wx-keychain/src/store.rs b/crates/wx-keychain/src/store.rs index 93794a6..c945ce8 100644 --- a/crates/wx-keychain/src/store.rs +++ b/crates/wx-keychain/src/store.rs @@ -57,15 +57,13 @@ pub struct KeyStore { impl KeyStore { /// Default path: `/keys.toml` pub fn default_path() -> Result { - let ap = wx_paths::AppPaths::new() - .map_err(|e| KeychainError::Other(e.to_string()))?; + let ap = wx_paths::AppPaths::new().map_err(|e| KeychainError::Other(e.to_string()))?; Ok(ap.keys_file()) } /// Load from the default path, creating an empty store if the file doesn't exist. pub fn load_default() -> Result { - let ap = wx_paths::AppPaths::new() - .map_err(|e| KeychainError::Other(e.to_string()))?; + let ap = wx_paths::AppPaths::new().map_err(|e| KeychainError::Other(e.to_string()))?; ap.migrate_config() .map_err(|e| KeychainError::Other(format!("config migration failed: {}", e)))?; let path = ap.keys_file(); @@ -354,7 +352,6 @@ impl KeyStore { } } - #[cfg(test)] mod tests { use super::*; diff --git a/crates/wx-media/src/ffmpeg.rs b/crates/wx-media/src/ffmpeg.rs index f2ea33c..f7bbfbe 100644 --- a/crates/wx-media/src/ffmpeg.rs +++ b/crates/wx-media/src/ffmpeg.rs @@ -58,7 +58,11 @@ pub fn reset_ffmpeg_cache() { FFPROBE_CACHED.store(false, Ordering::Release); } -fn run_command_with_piped_input(bin: String, input: &[u8], args: &[&str]) -> Result { +fn run_command_with_piped_input( + bin: String, + input: &[u8], + args: &[&str], +) -> Result { let mut child = Command::new(&bin) .args(args) .stdin(Stdio::piped()) @@ -76,10 +80,12 @@ fn run_command_with_piped_input(bin: String, input: &[u8], args: &[&str]) -> Res }) }); - let output = child.wait_with_output().map_err(|e| MediaError::FfmpegFailed { - status: -1, - stderr: e.to_string(), - })?; + let output = child + .wait_with_output() + .map_err(|e| MediaError::FfmpegFailed { + status: -1, + stderr: e.to_string(), + })?; if let Some(writer) = writer { let _ = writer.join(); diff --git a/crates/wx-media/src/hardlink.rs b/crates/wx-media/src/hardlink.rs index 5aa3580..23ea69d 100644 --- a/crates/wx-media/src/hardlink.rs +++ b/crates/wx-media/src/hardlink.rs @@ -22,7 +22,7 @@ pub fn query_hardlink_with_conn( media_type: &str, key: &str, ) -> Result, MediaError> { - let table = resolve_table(&conn, media_type)?; + let table = resolve_table(conn, media_type)?; let query = format!( "SELECT f.md5, f.file_name, f.file_size, f.modify_time, diff --git a/crates/wx-media/src/key.rs b/crates/wx-media/src/key.rs index 241562d..0e313ff 100644 --- a/crates/wx-media/src/key.rs +++ b/crates/wx-media/src/key.rs @@ -37,9 +37,7 @@ fn extract_wxid_from_data_dir(data_dir: &Path) -> Result { Ok(data_dir.parent().map_or_else( || extract_wxid(dir_name), - |root| { - wx_keychain::process::extract_base_wxid_for_account_dir_under_root(root, dir_name) - }, + |root| wx_keychain::process::extract_base_wxid_for_account_dir_under_root(root, dir_name), )) } diff --git a/crates/wx-media/src/lib.rs b/crates/wx-media/src/lib.rs index 73d79a0..3348e2a 100644 --- a/crates/wx-media/src/lib.rs +++ b/crates/wx-media/src/lib.rs @@ -85,7 +85,9 @@ pub use audio_transcode::{transcode_silk_to_mp3, transcode_silk_to_ogg_opus}; pub use dat::{decrypt_dat, detect_dat_format, detect_image_type, detect_xor_key}; pub use error::MediaError; pub use fallback::{find_file_by_name, find_video_by_md5}; -pub use ffmpeg::{ffmpeg_available, ffprobe_available, reset_ffmpeg_cache, run_ffmpeg, run_ffprobe}; +pub use ffmpeg::{ + ffmpeg_available, ffprobe_available, reset_ffmpeg_cache, run_ffmpeg, run_ffprobe, +}; pub use hardlink::{query_hardlink, query_hardlink_with_conn}; pub use image_resolver::{resolve_image, resolve_image_by_md5}; pub use image_transcode::transcode_wxgf; @@ -97,7 +99,9 @@ pub use types::{ MediaLookupResult, TranscodeAudioResult, TranscodeImageResult, VoiceBlob, }; pub use video_decrypt::{decrypt_video, decrypt_video_with_keystream}; -pub use voice::{extract_voice, extract_voice_with_conn, extract_voice_with_conn_hint, find_media_dbs}; +pub use voice::{ + extract_voice, extract_voice_with_conn, extract_voice_with_conn_hint, find_media_dbs, +}; pub use wxgf::{parse_wxgf, WxgfContent}; /// Compute MD5 hash of bytes, returning the `md5::Digest` (displays as hex). diff --git a/crates/wx-media/tests/audio-transcode.rs b/crates/wx-media/tests/audio-transcode.rs index 651d392..b22b86f 100644 --- a/crates/wx-media/tests/audio-transcode.rs +++ b/crates/wx-media/tests/audio-transcode.rs @@ -12,8 +12,13 @@ fn sample_silk() -> Vec { #[cfg(feature = "audio")] fn long_sample_silk() -> Vec { - silk_rs::encode_silk(vec![0_u8; silent_pcm_frame().len() * 250], 24_000, 24_000, true) - .unwrap() + silk_rs::encode_silk( + vec![0_u8; silent_pcm_frame().len() * 250], + 24_000, + 24_000, + true, + ) + .unwrap() } #[cfg(feature = "audio")] diff --git a/crates/wx-media/tests/voice.rs b/crates/wx-media/tests/voice.rs index d0a637f..08983f9 100644 --- a/crates/wx-media/tests/voice.rs +++ b/crates/wx-media/tests/voice.rs @@ -184,10 +184,7 @@ fn voice_query_with_conn_returns_stable_not_found_errors() { fn voice_query_with_conn_hint_returns_chat_name_id_from_indexed_lookup() { let tmp = TempDir::new().unwrap(); let db_path = tmp.path().join("media.db"); - create_indexed_media_db( - &db_path, - &[(55, 1000, 1, "srv_hint_1", b"hinted_blob")], - ); + create_indexed_media_db(&db_path, &[(55, 1000, 1, "srv_hint_1", b"hinted_blob")]); let conn = rusqlite::Connection::open(&db_path).unwrap(); let blob = wx_media::extract_voice_with_conn_hint(&conn, "srv_hint_1", Some(55)).unwrap(); diff --git a/crates/wx-monitor/src/cache.rs b/crates/wx-monitor/src/cache.rs index 32dc9ae..69ac338 100644 --- a/crates/wx-monitor/src/cache.rs +++ b/crates/wx-monitor/src/cache.rs @@ -118,11 +118,7 @@ impl DecryptCache { wx_decrypt::dispatch_decrypt_db(src, dst, &self.key_material, self.params) } - fn do_decrypt_wal( - &self, - wal: &Path, - dst: &Path, - ) -> Result { + fn do_decrypt_wal(&self, wal: &Path, dst: &Path) -> Result { wx_decrypt::dispatch_decrypt_wal(wal, dst, &self.key_material, self.params) } } diff --git a/crates/wx-monitor/tests/integration.rs b/crates/wx-monitor/tests/integration.rs index 2e4f95e..80dddb0 100644 --- a/crates/wx-monitor/tests/integration.rs +++ b/crates/wx-monitor/tests/integration.rs @@ -217,10 +217,7 @@ async fn monitor_detects_session_change() { // Should be an Updated event for wxid_bob (the new session) assert_eq!(event.username, "wxid_bob"); - assert!(matches!( - event.kind, - wx_monitor::SessionEventKind::Updated - )); + assert!(matches!(event.kind, wx_monitor::SessionEventKind::Updated)); // Stop monitor and assert clean exit monitor.stop(); diff --git a/crates/wx-paths/src/lib.rs b/crates/wx-paths/src/lib.rs index 6ab85e7..450c6a2 100644 --- a/crates/wx-paths/src/lib.rs +++ b/crates/wx-paths/src/lib.rs @@ -211,7 +211,9 @@ impl AppPaths { /// `/lldb/wechat_lldb_output.txt` pub fn lldb_output_file() -> PathBuf { - Self::temp_root().join("lldb").join("wechat_lldb_output.txt") + Self::temp_root() + .join("lldb") + .join("wechat_lldb_output.txt") } /// `/nickname/_.db` @@ -242,13 +244,21 @@ impl AppPaths { /// Current platform identifier. pub fn platform() -> &'static str { #[cfg(target_os = "macos")] - { "macos" } + { + "macos" + } #[cfg(target_os = "linux")] - { "linux" } + { + "linux" + } #[cfg(target_os = "windows")] - { "windows" } + { + "windows" + } #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { "unknown" } + { + "unknown" + } } /// Build a summary of all paths. @@ -442,7 +452,10 @@ mod tests { let ap = AppPaths::new().unwrap(); let config = ap.config_dir(); assert!( - config.to_str().unwrap().contains("Application Support/wx-cli/config"), + config + .to_str() + .unwrap() + .contains("Application Support/wx-cli/config"), "macOS config should be under Application Support: {:?}", config ); @@ -464,7 +477,10 @@ mod tests { let ap = AppPaths::new().unwrap(); let state = ap.state_root(); assert!( - state.to_str().unwrap().contains("Application Support/wx-cli/state"), + state + .to_str() + .unwrap() + .contains("Application Support/wx-cli/state"), "macOS state should be under Application Support: {:?}", state ); diff --git a/crates/wx-paths/src/migration.rs b/crates/wx-paths/src/migration.rs index 2892a6e..9e669ce 100644 --- a/crates/wx-paths/src/migration.rs +++ b/crates/wx-paths/src/migration.rs @@ -120,8 +120,14 @@ mod tests { } // New files exist - assert_eq!(fs::read_to_string(new_config.join("keys.toml")).unwrap(), "test-keys"); - assert_eq!(fs::read_to_string(new_config.join("settings.toml")).unwrap(), "test-settings"); + assert_eq!( + fs::read_to_string(new_config.join("keys.toml")).unwrap(), + "test-keys" + ); + assert_eq!( + fs::read_to_string(new_config.join("settings.toml")).unwrap(), + "test-settings" + ); // Old files deleted assert!(!old_config.join("keys.toml").exists()); @@ -153,7 +159,10 @@ mod tests { } // New file unchanged - assert_eq!(fs::read_to_string(new_config.join("keys.toml")).unwrap(), "new-keys"); + assert_eq!( + fs::read_to_string(new_config.join("keys.toml")).unwrap(), + "new-keys" + ); // Old file still exists (wasn't migrated because dst exists) assert!(old_config.join("keys.toml").exists()); } diff --git a/crates/wx-paths/src/platform.rs b/crates/wx-paths/src/platform.rs index 72cdf91..268e911 100644 --- a/crates/wx-paths/src/platform.rs +++ b/crates/wx-paths/src/platform.rs @@ -26,9 +26,7 @@ impl PlatformBaseDirs { let config_root = dirs::config_dir() .ok_or(PathsError::NoConfig)? .join("wx-cli"); - let cache_root = dirs::cache_dir() - .ok_or(PathsError::NoCache)? - .join("wx-cli"); + let cache_root = dirs::cache_dir().ok_or(PathsError::NoCache)?.join("wx-cli"); let state_root = dirs::state_dir() .or_else(dirs::data_local_dir) .or_else(dirs::data_dir) @@ -48,9 +46,7 @@ impl PlatformBaseDirs { let config_root = dirs::config_dir() .ok_or(PathsError::NoConfig)? .join("wx-cli"); - let cache_root = dirs::cache_dir() - .ok_or(PathsError::NoCache)? - .join("wx-cli"); + let cache_root = dirs::cache_dir().ok_or(PathsError::NoCache)?.join("wx-cli"); let local_data = dirs::data_local_dir() .or_else(dirs::data_dir) .ok_or(PathsError::NoState)? @@ -71,9 +67,7 @@ impl PlatformBaseDirs { let config_root = dirs::config_dir() .ok_or(PathsError::NoConfig)? .join("wx-cli"); - let cache_root = dirs::cache_dir() - .ok_or(PathsError::NoCache)? - .join("wx-cli"); + let cache_root = dirs::cache_dir().ok_or(PathsError::NoCache)?.join("wx-cli"); let state_root = dirs::state_dir() .or_else(dirs::data_local_dir) .or_else(dirs::data_dir)