mirror of
https://github.com/pandorafuture/wx-cli.git
synced 2026-08-29 04:00:55 +00:00
chore: restore fmt and clippy CI checks
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -421,7 +421,10 @@ impl MediaBridge {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_image_bytes(decoded_data: Vec<u8>, decoded_ext: &str) -> (Vec<u8>, String, bool, bool) {
|
||||
pub fn export_image_bytes(
|
||||
decoded_data: Vec<u8>,
|
||||
decoded_ext: &str,
|
||||
) -> (Vec<u8>, String, bool, bool) {
|
||||
if decoded_ext != "wxgf" {
|
||||
return (decoded_data, decoded_ext.to_string(), false, false);
|
||||
}
|
||||
|
||||
@@ -197,10 +197,9 @@ impl VoiceConnectionPool {
|
||||
fn open_all(&self) -> Vec<Connection> {
|
||||
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<R>(&self, f: impl FnOnce(&[Connection]) -> R) -> R {
|
||||
thread_local! {
|
||||
static CONNS: RefCell<Option<(u64, Vec<Connection>)>> = RefCell::new(None);
|
||||
static CONNS: RefCell<Option<(u64, Vec<Connection>)>> = const { RefCell::new(None) };
|
||||
}
|
||||
CONNS.with(|cell| {
|
||||
let mut borrow = cell.borrow_mut();
|
||||
@@ -242,16 +241,12 @@ impl HardlinkConnectionPool {
|
||||
}
|
||||
|
||||
fn open(&self) -> Option<Connection> {
|
||||
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<R>(&self, f: impl FnOnce(&Connection) -> R) -> Option<R> {
|
||||
thread_local! {
|
||||
static CONN: RefCell<Option<(u64, Connection)>> = RefCell::new(None);
|
||||
static CONN: RefCell<Option<(u64, Connection)>> = 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<MediaTask>) -> (Vec<MediaTask>, 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<usize, (Option<MediaAsset>, Vec<TaskTag>, Option<ExportError>)> =
|
||||
HashMap::new();
|
||||
let mut by_index: HashMap<usize, (Option<MediaAsset>, Vec<TaskTag>)> = 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<TaskTag> = 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<TaskTag> = 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<u8> = 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(
|
||||
|
||||
@@ -64,10 +64,7 @@ pub async fn cmd_key_extract(timeout_secs: u64) -> Result<(), Box<dyn std::error
|
||||
Some(matched.base_wxid.clone()),
|
||||
);
|
||||
store.save_default()?;
|
||||
eprintln!(
|
||||
"Key saved to {:?}",
|
||||
wx_keychain::KeyStore::default_path()?
|
||||
);
|
||||
eprintln!("Key saved to {:?}", wx_keychain::KeyStore::default_path()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -102,8 +99,7 @@ pub fn cmd_key_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// 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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
store.save_default()?;
|
||||
eprintln!(
|
||||
"Keys saved to {:?}",
|
||||
wx_keychain::KeyStore::default_path()?
|
||||
);
|
||||
eprintln!("Keys saved to {:?}", wx_keychain::KeyStore::default_path()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,7 +39,13 @@ fn print_paths_table(summary: &PathsSummary) {
|
||||
} else {
|
||||
"[missing]"
|
||||
};
|
||||
println!("{:<width$} {:<60} {}", label, display, status, width = max_label);
|
||||
println!(
|
||||
"{:<width$} {:<60} {}",
|
||||
label,
|
||||
display,
|
||||
status,
|
||||
width = max_label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,9 +298,7 @@ fn load_local_query(
|
||||
// When limit pushdown was used (non-anchor, non-all), total_rows only reflects the
|
||||
// scanned window. Use a lightweight COUNT(*) query to get the actual DB-level total.
|
||||
if !has_anchor && !all {
|
||||
let mt_filter = msg_type
|
||||
.as_ref()
|
||||
.and_then(|s| wx_db::parse_msg_type(s));
|
||||
let mt_filter = msg_type.as_ref().and_then(|s| wx_db::parse_msg_type(s));
|
||||
let db_total = db.count_messages(
|
||||
&talker,
|
||||
since.unwrap_or(0),
|
||||
|
||||
@@ -5,7 +5,9 @@ use wx_context::{register_mm_fts_tokenizer, AccountContext, ContactResolver, Res
|
||||
use super::thin_client::{ThinClient, ThinClientCliArgs, ThinClientOptions};
|
||||
use crate::output::{JsonEnvelope, PagingMeta, StatsMeta};
|
||||
use crate::schema::{enrich_message_as_hit, enrich_native_fts_hit, SearchHit};
|
||||
use crate::util::{effective_limit_all, open_db_all, print_cache_stats, print_detection_note, try_remote_or_local};
|
||||
use crate::util::{
|
||||
effective_limit_all, open_db_all, print_cache_stats, print_detection_note, try_remote_or_local,
|
||||
};
|
||||
use crate::OutputFormat;
|
||||
|
||||
// Unused imports kept for Task 6 cleanup reference:
|
||||
@@ -67,12 +69,8 @@ fn load_local_search(
|
||||
Ok(conn)
|
||||
}) {
|
||||
Ok(conn) => {
|
||||
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,
|
||||
|
||||
@@ -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, "[消息已隐藏]");
|
||||
|
||||
@@ -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<String> = 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() {
|
||||
|
||||
@@ -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)),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ use wx_context::{
|
||||
};
|
||||
use wx_db::WechatDb;
|
||||
|
||||
type Name2IdCache = Arc<std::sync::Mutex<Option<HashMap<i64, String>>>>;
|
||||
|
||||
/// 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<PathBuf>,
|
||||
/// Cache of name2id mapping — cleared when FTS is reopened.
|
||||
name2id_cache: Option<Arc<std::sync::Mutex<Option<HashMap<i64, String>>>>>,
|
||||
name2id_cache: Option<Name2IdCache>,
|
||||
/// Cache of media DB paths — cleared on every refresh.
|
||||
media_db_paths: Option<Arc<std::sync::Mutex<Option<Vec<PathBuf>>>>>,
|
||||
/// 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<Arc<std::sync::Mutex<Option<HashMap<i64, String>>>>>,
|
||||
name2id_cache: Option<Name2IdCache>,
|
||||
media_db_paths: Option<Arc<std::sync::Mutex<Option<Vec<PathBuf>>>>>,
|
||||
hardlink_db_conn: Option<Arc<std::sync::Mutex<Option<Connection>>>>,
|
||||
) -> Self {
|
||||
|
||||
@@ -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<PathBuf>) -> Result<AppPaths, Box<dyn std::error::Error>> {
|
||||
fn resolve_app_paths(
|
||||
runtime_root: Option<PathBuf>,
|
||||
) -> Result<AppPaths, Box<dyn std::error::Error>> {
|
||||
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<dyn std::er
|
||||
|
||||
pub async fn cmd_server_restart(args: ServerRestartArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
data_dir: Option<PathBuf>,
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PersistentCache>,
|
||||
Option<DecryptStats>,
|
||||
);
|
||||
|
||||
/// 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<PersistentCache>,
|
||||
Option<DecryptStats>,
|
||||
),
|
||||
Box<dyn std::error::Error>,
|
||||
> {
|
||||
) -> Result<OpenDbAllResult, Box<dyn std::error::Error>> {
|
||||
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]
|
||||
|
||||
@@ -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::<Vec<u8>>, 0_i32, None::<i32>,
|
||||
100_i64,
|
||||
3001_i64,
|
||||
1_i64,
|
||||
1_i64,
|
||||
1_700_000_301_i64,
|
||||
b"private hello" as &[u8],
|
||||
None::<Vec<u8>>,
|
||||
0_i32,
|
||||
None::<i32>,
|
||||
],
|
||||
)
|
||||
.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::<Vec<u8>>, 0_i32, None::<i32>,
|
||||
200_i64,
|
||||
4001_i64,
|
||||
1_i64,
|
||||
1_i64,
|
||||
1_700_000_101_i64,
|
||||
b"alice says hello in group" as &[u8],
|
||||
None::<Vec<u8>>,
|
||||
0_i32,
|
||||
None::<i32>,
|
||||
],
|
||||
)
|
||||
.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::<Vec<u8>>, 0_i32, None::<i32>,
|
||||
210_i64,
|
||||
4002_i64,
|
||||
1_i64,
|
||||
5_i64,
|
||||
1_700_000_102_i64,
|
||||
b"spam content" as &[u8],
|
||||
None::<Vec<u8>>,
|
||||
0_i32,
|
||||
None::<i32>,
|
||||
],
|
||||
)
|
||||
.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::<Vec<u8>>, 0_i32, None::<i32>,
|
||||
1_i64,
|
||||
1_700_000_103_i64,
|
||||
quote_xml.as_bytes(),
|
||||
None::<Vec<u8>>,
|
||||
0_i32,
|
||||
None::<i32>,
|
||||
],
|
||||
)
|
||||
.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::<Vec<_>>());
|
||||
assert!(
|
||||
quote.is_some(),
|
||||
"quote message should be present (alice's reply): items={:?}",
|
||||
items
|
||||
.iter()
|
||||
.map(|i| i["server_id"].as_i64())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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::<u8>::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::<u8>::new(),
|
||||
group_visible_info,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -323,9 +323,11 @@ impl WechatDb {
|
||||
msg_type_filter: Option<u32>,
|
||||
) -> 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)
|
||||
|
||||
@@ -530,10 +530,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extract_quote_fromusr_normal() {
|
||||
let xml = r#"<msg><appmsg><title>reply</title><refermsg><fromusr>wxid_alice</fromusr><content>hi</content></refermsg></appmsg></msg>"#;
|
||||
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, <fromusr> is the chatroom ID, <chatusr> is the actual sender
|
||||
let xml = r#"<msg><appmsg><title>reply</title><refermsg><fromusr>group@chatroom</fromusr><chatusr>wxid_sender</chatusr><displayname>Sender</displayname><content>hi</content></refermsg></appmsg></msg>"#;
|
||||
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 <fromusr> exists (no <chatusr>)
|
||||
let xml = r#"<msg><appmsg><title>reply</title><refermsg><fromusr>wxid_bob</fromusr><displayname>Bob</displayname><content>hi</content></refermsg></appmsg></msg>"#;
|
||||
assert_eq!(
|
||||
extract_quote_fromusr(xml),
|
||||
Some("wxid_bob".to_string())
|
||||
);
|
||||
assert_eq!(extract_quote_fromusr(xml), Some("wxid_bob".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -138,16 +138,16 @@ fn get_wechat_version() -> Result<String, KeychainError> {
|
||||
|
||||
/// Shared config directory resolved by `AppPaths`.
|
||||
pub fn config_dir() -> Result<PathBuf, KeychainError> {
|
||||
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<PathBuf, KeychainError> {
|
||||
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::*;
|
||||
|
||||
@@ -57,15 +57,13 @@ pub struct KeyStore {
|
||||
impl KeyStore {
|
||||
/// Default path: `<config_dir>/keys.toml`
|
||||
pub fn default_path() -> Result<PathBuf, KeychainError> {
|
||||
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<Self, KeychainError> {
|
||||
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::*;
|
||||
|
||||
@@ -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<Output, MediaError> {
|
||||
fn run_command_with_piped_input(
|
||||
bin: String,
|
||||
input: &[u8],
|
||||
args: &[&str],
|
||||
) -> Result<Output, MediaError> {
|
||||
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();
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn query_hardlink_with_conn(
|
||||
media_type: &str,
|
||||
key: &str,
|
||||
) -> Result<Vec<HardlinkEntry>, 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,
|
||||
|
||||
@@ -37,9 +37,7 @@ fn extract_wxid_from_data_dir(data_dir: &Path) -> Result<String, MediaError> {
|
||||
|
||||
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),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -12,8 +12,13 @@ fn sample_silk() -> Vec<u8> {
|
||||
|
||||
#[cfg(feature = "audio")]
|
||||
fn long_sample_silk() -> Vec<u8> {
|
||||
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")]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<usize, wx_decrypt::DecryptError> {
|
||||
fn do_decrypt_wal(&self, wal: &Path, dst: &Path) -> Result<usize, wx_decrypt::DecryptError> {
|
||||
wx_decrypt::dispatch_decrypt_wal(wal, dst, &self.key_material, self.params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -211,7 +211,9 @@ impl AppPaths {
|
||||
|
||||
/// `<temp_root>/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")
|
||||
}
|
||||
|
||||
/// `<temp_root>/nickname/<pid>_<nanos>.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
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user