mirror of
https://github.com/pandorafuture/wx-cli.git
synced 2026-08-29 04:00:55 +00:00
WeChat macOS database decryption and query tool, supporting key extraction, message querying, full-text search, conversation export, real-time monitoring, media handling, and HTTP server mode with REST API and SSE.
329 lines
9.5 KiB
Rust
329 lines
9.5 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use rusqlite::{params, Connection};
|
|
use tempfile::TempDir;
|
|
use wx_db::test_ddl;
|
|
use wx_db::{encode_extra_buffer_for_test, ContactQuery, WechatDb};
|
|
|
|
// ---- helpers ----
|
|
|
|
/// Create a fixture with contacts that have extra_buffer data, description, and labels.
|
|
fn create_fixture_extended() -> TempDir {
|
|
let dir = TempDir::new().unwrap();
|
|
let base = dir.path();
|
|
|
|
// contact/contact.db
|
|
let contact_dir = base.join("contact");
|
|
fs::create_dir_all(&contact_dir).unwrap();
|
|
create_contact_db_extended(&contact_dir.join("contact.db"));
|
|
|
|
// session/session.db (minimal)
|
|
let session_dir = base.join("session");
|
|
fs::create_dir_all(&session_dir).unwrap();
|
|
create_session_db(&session_dir.join("session.db"));
|
|
|
|
// message/message_0.db (minimal shard)
|
|
let msg_dir = base.join("message");
|
|
fs::create_dir_all(&msg_dir).unwrap();
|
|
create_message_shard(&msg_dir.join("message_0.db"), 1000);
|
|
|
|
dir
|
|
}
|
|
|
|
/// Create a fixture WITHOUT a contact_label table.
|
|
fn create_fixture_no_label_table() -> TempDir {
|
|
let dir = TempDir::new().unwrap();
|
|
let base = dir.path();
|
|
|
|
let contact_dir = base.join("contact");
|
|
fs::create_dir_all(&contact_dir).unwrap();
|
|
{
|
|
let conn = Connection::open(contact_dir.join("contact.db")).unwrap();
|
|
test_ddl::create_test_contact_table(&conn);
|
|
// No contact_label table at all
|
|
let blob =
|
|
encode_extra_buffer_for_test(Some(1), None, None, None, None, None, None, Some("5,6"));
|
|
conn.execute(
|
|
"INSERT INTO contact (username, alias, remark, nick_name, extra_buffer) \
|
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
params!["wxid_nolabel", "", "No Label", "", blob],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let session_dir = base.join("session");
|
|
fs::create_dir_all(&session_dir).unwrap();
|
|
create_session_db(&session_dir.join("session.db"));
|
|
|
|
let msg_dir = base.join("message");
|
|
fs::create_dir_all(&msg_dir).unwrap();
|
|
create_message_shard(&msg_dir.join("message_0.db"), 1000);
|
|
|
|
dir
|
|
}
|
|
|
|
fn create_contact_db_extended(path: &Path) {
|
|
let conn = Connection::open(path).unwrap();
|
|
test_ddl::create_test_contact_table(&conn);
|
|
test_ddl::create_test_contact_label_table(&conn);
|
|
|
|
// Label rows
|
|
conn.execute(
|
|
"INSERT INTO contact_label VALUES (?1, ?2, ?3)",
|
|
params!["5", "体育生", 0],
|
|
)
|
|
.unwrap();
|
|
conn.execute(
|
|
"INSERT INTO contact_label VALUES (?1, ?2, ?3)",
|
|
params!["6", "直男", 1],
|
|
)
|
|
.unwrap();
|
|
|
|
// Contact 1: all fields populated
|
|
let blob_full = encode_extra_buffer_for_test(
|
|
Some(1),
|
|
Some("成长本就是一个孤立无援的过程"),
|
|
Some("CN"),
|
|
Some("Beijing"),
|
|
Some("Haidian"),
|
|
Some(30),
|
|
Some("15891926830"),
|
|
Some("5,6"),
|
|
);
|
|
conn.execute(
|
|
"INSERT INTO contact (username, alias, remark, nick_name, description, extra_buffer) \
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
params![
|
|
"wxid_full",
|
|
"Dalong-SS",
|
|
"张三",
|
|
"大龙",
|
|
"QQ 442007516",
|
|
blob_full
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
// Contact 2: empty extra_buffer
|
|
conn.execute(
|
|
"INSERT INTO contact (username, alias, remark, nick_name) \
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
params!["wxid_empty", "empty_alias", "Empty Remark", "Empty Nick"],
|
|
)
|
|
.unwrap();
|
|
|
|
// Contact 3: partial region (only province)
|
|
let blob_partial =
|
|
encode_extra_buffer_for_test(None, None, None, Some("Guangdong"), None, None, None, None);
|
|
conn.execute(
|
|
"INSERT INTO contact (username, alias, remark, nick_name, extra_buffer) \
|
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
params![
|
|
"wxid_partial",
|
|
"",
|
|
"Partial Remark",
|
|
"Partial",
|
|
blob_partial
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
// Contact 4: phone only (for keyword search test)
|
|
let blob_phone = encode_extra_buffer_for_test(
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
Some("13912345678"),
|
|
None,
|
|
);
|
|
conn.execute(
|
|
"INSERT INTO contact (username, alias, remark, nick_name, extra_buffer) \
|
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
params!["wxid_phone", "", "Phone Guy", "PG", blob_phone],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn create_session_db(path: &Path) {
|
|
let conn = Connection::open(path).unwrap();
|
|
test_ddl::create_test_session_table_extended(&conn);
|
|
conn.execute(
|
|
"INSERT INTO SessionTable VALUES (?1, ?2, ?3, NULL, NULL, NULL)",
|
|
params!["wxid_full", 1000, "hello"],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn create_message_shard(path: &Path, timestamp: i64) {
|
|
let conn = Connection::open(path).unwrap();
|
|
conn.execute_batch("CREATE TABLE Timestamp (timestamp INTEGER);")
|
|
.unwrap();
|
|
conn.execute("INSERT INTO Timestamp VALUES (?1)", params![timestamp])
|
|
.unwrap();
|
|
}
|
|
|
|
// ---- tests ----
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_all_fields() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let full = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_full")
|
|
.unwrap();
|
|
|
|
assert_eq!(full.gender, Some(1));
|
|
assert_eq!(
|
|
full.signature.as_deref(),
|
|
Some("成长本就是一个孤立无援的过程")
|
|
);
|
|
assert_eq!(full.region.as_deref(), Some("CN · Beijing · Haidian"));
|
|
assert_eq!(full.source_scene, Some(30));
|
|
assert_eq!(full.phone.as_deref(), Some("15891926830"));
|
|
assert_eq!(full.memo.as_deref(), Some("QQ 442007516"));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_phone_extraction() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let phone_guy = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_phone")
|
|
.unwrap();
|
|
assert_eq!(phone_guy.phone.as_deref(), Some("13912345678"));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_labels_resolved() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let full = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_full")
|
|
.unwrap();
|
|
assert_eq!(full.labels.len(), 2);
|
|
assert!(full.labels.contains(&"体育生".to_string()));
|
|
assert!(full.labels.contains(&"直男".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_empty_blob() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let empty = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_empty")
|
|
.unwrap();
|
|
assert_eq!(empty.gender, None);
|
|
assert_eq!(empty.signature, None);
|
|
assert_eq!(empty.region, None);
|
|
assert_eq!(empty.source_scene, None);
|
|
assert_eq!(empty.phone, None);
|
|
assert!(empty.labels.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_no_label_table() {
|
|
let dir = create_fixture_no_label_table();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let c = &result.items[0];
|
|
assert_eq!(c.user_name, "wxid_nolabel");
|
|
// label_ids are "5,6" but no contact_label table → labels is empty
|
|
assert!(c.labels.is_empty());
|
|
// gender still decoded
|
|
assert_eq!(c.gender, Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_extra_buffer_partial_region() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let partial = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_partial")
|
|
.unwrap();
|
|
assert_eq!(partial.region.as_deref(), Some("Guangdong"));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_memo_from_description() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
let result = db.query_contacts(&ContactQuery::new()).unwrap();
|
|
|
|
let full = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_full")
|
|
.unwrap();
|
|
assert_eq!(full.memo.as_deref(), Some("QQ 442007516"));
|
|
|
|
// Contact with no description → memo is None
|
|
let empty = result
|
|
.items
|
|
.iter()
|
|
.find(|c| c.user_name == "wxid_empty")
|
|
.unwrap();
|
|
assert_eq!(empty.memo, None);
|
|
}
|
|
|
|
#[test]
|
|
fn contact_keyword_matches_description() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
|
|
let result = db
|
|
.query_contacts(&ContactQuery::new().keyword("442007"))
|
|
.unwrap();
|
|
assert_eq!(result.items.len(), 1);
|
|
assert_eq!(result.items[0].user_name, "wxid_full");
|
|
assert_eq!(result.stats.filtered_count, Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_keyword_matches_phone() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
|
|
let result = db
|
|
.query_contacts(&ContactQuery::new().keyword("13912345"))
|
|
.unwrap();
|
|
assert_eq!(result.items.len(), 1);
|
|
assert_eq!(result.items[0].user_name, "wxid_phone");
|
|
}
|
|
|
|
#[test]
|
|
fn contact_keyword_matches_label_name() {
|
|
let dir = create_fixture_extended();
|
|
let db = WechatDb::open(dir.path()).unwrap();
|
|
|
|
let result = db
|
|
.query_contacts(&ContactQuery::new().keyword("体育生"))
|
|
.unwrap();
|
|
assert_eq!(result.items.len(), 1);
|
|
assert_eq!(result.items[0].user_name, "wxid_full");
|
|
}
|