mirror of https://github.com/jackwener/wx-cli.git
Merge pull request #63 from Icy-Cat/feat/windows-mydocument-keyword
feat(windows): resolve MyDocument: token in Weixin data-root inipull/77/head
commit
f8550ae74d
|
|
@ -71,6 +71,8 @@ windows = { version = "0.58", features = [
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Com",
|
||||||
|
"Win32_UI_Shell",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
|
|
@ -320,9 +320,11 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().map(|e| e == "ini").unwrap_or(false) {
|
if path.extension().map(|e| e == "ini").unwrap_or(false) {
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||||
let data_root = content.trim().to_string();
|
let Some(data_root) = resolve_windows_data_root(content.trim()) else {
|
||||||
if PathBuf::from(&data_root).is_dir() {
|
continue;
|
||||||
let pattern = PathBuf::from(&data_root).join("xwechat_files");
|
};
|
||||||
|
if data_root.is_dir() {
|
||||||
|
let pattern = data_root.join("xwechat_files");
|
||||||
if let Ok(entries2) = std::fs::read_dir(&pattern) {
|
if let Ok(entries2) = std::fs::read_dir(&pattern) {
|
||||||
for entry2 in entries2.flatten() {
|
for entry2 in entries2.flatten() {
|
||||||
let storage = entry2.path().join("db_storage");
|
let storage = entry2.path().join("db_storage");
|
||||||
|
|
@ -340,6 +342,72 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
candidates.into_iter().next_back()
|
candidates.into_iter().next_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the data-root path that Weixin writes to its `*.ini` file under
|
||||||
|
/// `%APPDATA%\Tencent\xwechat\config\`.
|
||||||
|
///
|
||||||
|
/// Observed forms in the wild:
|
||||||
|
/// - A plain absolute path, e.g. `D:\WeChatFiles`.
|
||||||
|
/// - The literal token `MyDocument:` (sometimes with a trailing slash),
|
||||||
|
/// which is not a real filesystem path. Empirically this denotes
|
||||||
|
/// "the current user's Documents folder"; users who relocated
|
||||||
|
/// Documents to e.g. `D:\Documents` saw auto-detect fail silently
|
||||||
|
/// because `PathBuf::from("MyDocument:").is_dir()` is false.
|
||||||
|
///
|
||||||
|
/// We accept either form. For the `MyDocument:` token we resolve via
|
||||||
|
/// `SHGetKnownFolderPath(FOLDERID_Documents)`, which respects the standard
|
||||||
|
/// shell-folder redirect at
|
||||||
|
/// `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders\Personal`.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn resolve_windows_data_root(content: &str) -> Option<PathBuf> {
|
||||||
|
let trimmed = content.trim();
|
||||||
|
// Strip an optional trailing slash so `MyDocument:\` and `MyDocument:/` also match.
|
||||||
|
let stripped = trimmed
|
||||||
|
.strip_suffix(['\\', '/'])
|
||||||
|
.unwrap_or(trimmed);
|
||||||
|
if stripped.eq_ignore_ascii_case("MyDocument:") {
|
||||||
|
return known_documents_dir();
|
||||||
|
}
|
||||||
|
Some(PathBuf::from(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn known_documents_dir() -> Option<PathBuf> {
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
use windows::Win32::Foundation::HANDLE;
|
||||||
|
use windows::Win32::System::Com::CoTaskMemFree;
|
||||||
|
use windows::Win32::UI::Shell::{
|
||||||
|
FOLDERID_Documents, SHGetKnownFolderPath, KF_FLAG_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// SAFETY: standard Win32 known-folder API. SHGetKnownFolderPath either returns
|
||||||
|
// a heap-allocated PWSTR that the caller must free with CoTaskMemFree, or an
|
||||||
|
// error — in which case the out-pointer is not allocated. We free on every
|
||||||
|
// success path. Passing a null token (HANDLE::default()) means "the calling
|
||||||
|
// user", which is exactly what we want.
|
||||||
|
unsafe {
|
||||||
|
let pwstr =
|
||||||
|
SHGetKnownFolderPath(&FOLDERID_Documents, KF_FLAG_DEFAULT, HANDLE::default()).ok()?;
|
||||||
|
if pwstr.0.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Walk the NUL-terminated wide string to compute its length.
|
||||||
|
let mut len = 0usize;
|
||||||
|
while *pwstr.0.add(len) != 0 {
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
let slice = std::slice::from_raw_parts(pwstr.0, len);
|
||||||
|
let os_str = OsString::from_wide(slice);
|
||||||
|
CoTaskMemFree(Some(pwstr.0 as *const _));
|
||||||
|
let path = PathBuf::from(os_str);
|
||||||
|
if path.as_os_str().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
|
|
@ -351,6 +419,8 @@ mod tests {
|
||||||
config_path_in_dir, default_config_path, find_existing_config_path, home_config_path,
|
config_path_in_dir, default_config_path, find_existing_config_path, home_config_path,
|
||||||
resolve_cli_home,
|
resolve_cli_home,
|
||||||
};
|
};
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use super::{known_documents_dir, resolve_windows_data_root};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
@ -409,4 +479,24 @@ mod tests {
|
||||||
let path = default_config_path(Some(&cwd), Some(&exe), Some(&home));
|
let path = default_config_path(Some(&cwd), Some(&exe), Some(&home));
|
||||||
assert_eq!(path, cwd.join("config.json"));
|
assert_eq!(path, cwd.join("config.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn resolve_windows_data_root_passes_through_absolute_path() {
|
||||||
|
let p = resolve_windows_data_root("D:\\WeChatFiles").unwrap();
|
||||||
|
assert_eq!(p, PathBuf::from("D:\\WeChatFiles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn resolve_windows_data_root_recognises_mydocument_keyword() {
|
||||||
|
// Should match the keyword exactly (case-insensitive, with or without trailing slash)
|
||||||
|
// and resolve to a non-empty Documents path via SHGetKnownFolderPath.
|
||||||
|
let docs = known_documents_dir().expect("Documents known folder must resolve");
|
||||||
|
for keyword in ["MyDocument:", "mydocument:", "MyDocument:\\", "MyDocument:/"] {
|
||||||
|
let resolved = resolve_windows_data_root(keyword)
|
||||||
|
.unwrap_or_else(|| panic!("keyword {keyword:?} should resolve"));
|
||||||
|
assert_eq!(resolved, docs, "keyword {keyword:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue