chore: Apache-2.0 license, Windows support, install.ps1

This commit is contained in:
jackwener
2026-04-16 22:30:45 +08:00
parent 6d40c7f737
commit 6cdc806642
27 changed files with 1687 additions and 866 deletions
+150 -1
View File
@@ -246,7 +246,7 @@ fn scan_region(
/// 在缓冲区中搜索 x'<96个十六进制字符>' 模式
///
/// 格式:x'<64hex(key)><32hex(salt)>'(总计 99 字节)
fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
pub(crate) fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
let total = HEX_PATTERN_LEN + 3; // x' + 96 hex + '
if buf.len() < total {
return;
@@ -291,3 +291,152 @@ fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
i += total;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 构造一条合法的 x'<key><salt>' 模式字节串
fn make_pattern(key: &[u8; 64], salt: &[u8; 32]) -> Vec<u8> {
let mut v = vec![b'x', b'\''];
v.extend_from_slice(key);
v.extend_from_slice(salt);
v.push(b'\'');
v
}
#[test]
fn test_is_hex_char_valid() {
for c in b'0'..=b'9' { assert!(is_hex_char(c), "digit {}", c as char); }
for c in b'a'..=b'f' { assert!(is_hex_char(c), "lower {}", c as char); }
for c in b'A'..=b'F' { assert!(is_hex_char(c), "upper {}", c as char); }
}
#[test]
fn test_is_hex_char_invalid() {
for c in [b'g', b'G', b'x', b'\'', b' ', b'\0', b'z', b'Z'] {
assert!(!is_hex_char(c), "expected non-hex: {}", c as char);
}
}
#[test]
fn test_search_pattern_basic() {
let key = [b'a'; 64];
let salt = [b'b'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, "a".repeat(64));
assert_eq!(results[0].1, "b".repeat(32));
}
#[test]
fn test_search_pattern_uppercase_lowercased() {
// 大写十六进制字符应被统一转为小写
let key = [b'A'; 64];
let salt = [b'B'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, "a".repeat(64));
assert_eq!(results[0].1, "b".repeat(32));
}
#[test]
fn test_search_pattern_not_all_hex() {
// 96 个十六进制字符中有一个非法字符 → 不匹配
let mut buf = vec![b'x', b'\''];
buf.extend_from_slice(&[b'a'; 95]);
buf.push(b'g'); // 'g' 不是合法十六进制字符
buf.push(b'\'');
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_wrong_closing_quote() {
// 结尾引号错误 → 不匹配
let mut buf = vec![b'x', b'\''];
buf.extend_from_slice(&[b'a'; 96]);
buf.push(b'"'); // 应为 b'\''
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_dedup() {
// 相同模式出现两次 → 只保留一条
let key = [b'1'; 64];
let salt = [b'2'; 32];
let pattern = make_pattern(&key, &salt);
let mut buf = pattern.clone();
buf.extend_from_slice(&pattern);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
}
#[test]
fn test_search_pattern_multiple_distinct() {
// 两个不同的合法模式 → 各自独立捕获
let key1 = [b'a'; 64]; let salt1 = [b'b'; 32];
let key2 = [b'c'; 64]; let salt2 = [b'd'; 32];
let mut buf = make_pattern(&key1, &salt1);
buf.extend_from_slice(&make_pattern(&key2, &salt2));
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 2);
let keys: Vec<&str> = results.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"a".repeat(64).as_str()));
assert!(keys.contains(&"c".repeat(64).as_str()));
}
#[test]
fn test_search_pattern_embedded_in_garbage() {
// 模式夹在垃圾字节中间,仍应找到
let mut buf = vec![0xFFu8; 50];
let key = [b'e'; 64];
let salt = [b'f'; 32];
buf.extend_from_slice(&make_pattern(&key, &salt));
buf.extend_from_slice(&[0x00u8; 50]);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
}
#[test]
fn test_search_pattern_too_short() {
// 缓冲区太小,无法容纳完整模式
let buf = [b'x', b'\'', b'a', b'b'];
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_empty_buf() {
let mut results = Vec::new();
search_pattern(&[], &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_real_hex_mix() {
// 合法的混合大小写十六进制(0-9, a-f, A-F)
let mut key = [b'0'; 64];
for (i, c) in b"0123456789abcdefABCDEF0123456789abcdef0123456789abcdef01234567".iter().enumerate() {
if i < 64 { key[i] = *c; }
}
let salt = [b'9'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
// 结果应全小写
assert!(results[0].0.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
}
+158
View File
@@ -82,3 +82,161 @@ mod hex {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
/// 创建一个进程唯一的临时目录(测试用),返回路径;测试结束后调用方负责删除
fn make_temp_dir(label: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
// 用 label + thread id 保证同进程内并发测试不冲突
p.push(format!("wx-cli-test-{}-{:?}", label, std::thread::current().id()));
fs::create_dir_all(&p).unwrap();
p
}
// ── read_db_salt ──────────────────────────────────────────────────────────
#[test]
fn test_read_db_salt_plaintext_sqlite() {
let dir = make_temp_dir("salt-plain");
let path = dir.join("plain.db");
// 明文 SQLite 头:前 15 字节是 "SQLite format 3"
let mut content = b"SQLite format 3\x00".to_vec();
content.extend_from_slice(&[0u8; 100]);
fs::write(&path, &content).unwrap();
assert!(read_db_salt(&path).is_none(), "明文 SQLite 应返回 None");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_read_db_salt_encrypted() {
let dir = make_temp_dir("salt-enc");
let path = dir.join("enc.db");
// 非 SQLite 头 → 视为加密数据库,取前 16 字节作为 salt
let header: [u8; 16] = [
0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
];
fs::write(&path, &header).unwrap();
let salt = read_db_salt(&path).expect("加密 DB 应返回 Some");
assert_eq!(salt, "deadbeef0102030405060708090a0b0c");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_read_db_salt_too_short() {
let dir = make_temp_dir("salt-short");
let path = dir.join("short.db");
fs::write(&path, b"tooshort").unwrap(); // < 16 bytes
assert!(read_db_salt(&path).is_none(), "文件太短应返回 None");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_read_db_salt_nonexistent() {
assert!(read_db_salt(Path::new("/nonexistent/surely/not/here.db")).is_none());
}
#[test]
fn test_read_db_salt_exactly_16_bytes() {
let dir = make_temp_dir("salt-16");
let path = dir.join("exact.db");
let header = [0xabu8; 16];
fs::write(&path, &header).unwrap();
let salt = read_db_salt(&path).unwrap();
// 0xab × 16 → "ab" × 16 = 32 chars
assert_eq!(salt, "ab".repeat(16));
fs::remove_dir_all(&dir).ok();
}
// ── collect_db_salts ──────────────────────────────────────────────────────
#[test]
fn test_collect_db_salts_empty_dir() {
let dir = make_temp_dir("collect-empty");
let salts = collect_db_salts(&dir);
assert!(salts.is_empty());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_collect_db_salts_skips_plaintext_sqlite() {
let dir = make_temp_dir("collect-plain");
let mut content = b"SQLite format 3\x00".to_vec();
content.extend_from_slice(&[0u8; 100]);
fs::write(dir.join("plain.db"), &content).unwrap();
assert!(collect_db_salts(&dir).is_empty(), "明文 SQLite 应被跳过");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_collect_db_salts_finds_encrypted() {
let dir = make_temp_dir("collect-enc");
let header = [0x11u8; 16];
fs::write(dir.join("msg.db"), &header).unwrap();
let salts = collect_db_salts(&dir);
assert_eq!(salts.len(), 1);
assert_eq!(salts[0].0, "11".repeat(16)); // 0x11 × 16 → "11" × 16
assert_eq!(salts[0].1, "msg.db");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_collect_db_salts_recursive() {
let dir = make_temp_dir("collect-rec");
let subdir = dir.join("sub");
fs::create_dir_all(&subdir).unwrap();
let header = [0xaau8; 16];
fs::write(dir.join("root.db"), &header).unwrap();
fs::write(subdir.join("nested.db"), &header).unwrap();
fs::write(dir.join("ignored.txt"), b"text file").unwrap();
let salts = collect_db_salts(&dir);
assert_eq!(salts.len(), 2, "应递归找到 2 个加密 .db");
let names: Vec<&str> = salts.iter().map(|(_, n)| n.as_str()).collect();
assert!(names.contains(&"root.db"));
assert!(names.contains(&"sub/nested.db"));
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_collect_db_salts_ignores_non_db_extensions() {
let dir = make_temp_dir("collect-ext");
let header = [0xbbu8; 16];
fs::write(dir.join("data.txt"), &header).unwrap();
fs::write(dir.join("data.json"), &header).unwrap();
fs::write(dir.join("data.sqlite"), &header).unwrap();
assert!(collect_db_salts(&dir).is_empty(), "非 .db 文件应被忽略");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_collect_db_salts_multiple_files_unique_salts() {
let dir = make_temp_dir("collect-multi");
fs::write(dir.join("a.db"), &[0x11u8; 16]).unwrap();
fs::write(dir.join("b.db"), &[0x22u8; 16]).unwrap();
fs::write(dir.join("c.db"), &[0x33u8; 16]).unwrap();
let salts = collect_db_salts(&dir);
assert_eq!(salts.len(), 3);
let salt_vals: std::collections::HashSet<&str> =
salts.iter().map(|(s, _)| s.as_str()).collect();
assert!(salt_vals.contains("11".repeat(16).as_str()));
assert!(salt_vals.contains("22".repeat(16).as_str()));
assert!(salt_vals.contains("33".repeat(16).as_str()));
fs::remove_dir_all(&dir).ok();
}
}