use std::path::Path;
use tracing::warn;
use windows::Win32::UI::Shell::{
FOLDERID_CommonStartMenu, FOLDERID_Desktop, FOLDERID_RoamingAppData, FOLDERID_StartMenu,
SHGetKnownFolderPath, KF_FLAG_DEFAULT,
};
use zerolaunch_plugin_api::host::HostApiError;
use zerolaunch_plugin_api::services::path::{KnownPath, PathResolver};
#[cfg(not(feature = "portable"))]
const APP_DATA_DIR_NAME: &str = ".ZeroLaunch-rs";
pub struct WindowsPathResolver;
impl Default for WindowsPathResolver {
fn default() -> Self {
Self::new()
}
}
impl WindowsPathResolver {
pub fn new() -> Self {
Self
}
fn known_path_to_folder_id(path: KnownPath) -> Option<&'static windows_core::GUID> {
match path {
KnownPath::CommonStartMenu => Some(&FOLDERID_CommonStartMenu),
KnownPath::UserStartMenu => Some(&FOLDERID_StartMenu),
KnownPath::UserDesktop => Some(&FOLDERID_Desktop),
KnownPath::UserAppDataRoaming => Some(&FOLDERID_RoamingAppData),
KnownPath::AppDataDir
| KnownPath::AppLogDir
| KnownPath::AppIconCacheDir
| KnownPath::AppConfigDir
| KnownPath::AppCacheDir => None,
}
}
fn is_app_path(path: KnownPath) -> bool {
matches!(
path,
KnownPath::AppDataDir
| KnownPath::AppLogDir
| KnownPath::AppIconCacheDir
| KnownPath::AppConfigDir
| KnownPath::AppCacheDir
)
}
fn resolve_app_data_dir(&self) -> Result<String, HostApiError> {
let path_label = format!("{:?}", KnownPath::AppDataDir);
#[cfg(feature = "portable")]
{
let dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
dir.to_str()
.map(|s| s.to_string())
.ok_or_else(|| HostApiError::PathResolutionFailed {
path: path_label.to_string(),
reason: "便携模式路径转换失败".to_string(),
})
}
#[cfg(not(feature = "portable"))]
{
let home = dirs::home_dir().ok_or_else(|| {
warn!("无法获取用户 Home 目录");
HostApiError::PathResolutionFailed {
path: path_label.to_string(),
reason: "无法获取用户 Home 目录(dirs::home_dir() 返回 None)".to_string(),
}
})?;
home.join(APP_DATA_DIR_NAME)
.to_str()
.map(|s| s.to_string())
.ok_or_else(|| HostApiError::PathResolutionFailed {
path: path_label.to_string(),
reason: "应用数据目录路径转换失败".to_string(),
})
}
}
}
impl PathResolver for WindowsPathResolver {
fn resolve_path(&self, path: KnownPath) -> Result<String, HostApiError> {
let path_label = format!("{:?}", path);
if Self::is_app_path(path) {
return self.resolve_app_path_internal(path);
}
let folder_id =
Self::known_path_to_folder_id(path).expect("非应用路径应该有对应的 folder_id");
unsafe {
match SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None) {
Ok(pwstr) => match pwstr.to_string() {
Ok(s) => Ok(s),
Err(e) => {
warn!("路径转换失败 ({:?}): {}", path, e);
Err(HostApiError::PathResolutionFailed {
path: path_label,
reason: format!("路径字符串转换失败: {}", e),
})
}
},
Err(e) => {
warn!("获取已知路径失败 ({:?}): {}", path, e);
Err(HostApiError::PathResolutionFailed {
path: path_label,
reason: format!("SHGetKnownFolderPath 调用失败: {}", e),
})
}
}
}
}
}
impl WindowsPathResolver {
fn resolve_app_path_internal(&self, path: KnownPath) -> Result<String, HostApiError> {
match path {
KnownPath::AppDataDir => self.resolve_app_data_dir(),
KnownPath::AppLogDir => {
let app_data = self.resolve_app_data_dir()?;
Ok(Path::new(&app_data)
.join("logs")
.to_string_lossy()
.to_string())
}
KnownPath::AppIconCacheDir => {
let app_data = self.resolve_app_data_dir()?;
Ok(Path::new(&app_data)
.join("icons")
.to_string_lossy()
.to_string())
}
KnownPath::AppConfigDir => {
let app_data = self.resolve_app_data_dir()?;
Ok(Path::new(&app_data)
.join("config")
.to_string_lossy()
.to_string())
}
KnownPath::AppCacheDir => {
let app_data = self.resolve_app_data_dir()?;
Ok(Path::new(&app_data)
.join("plugin-cache")
.to_string_lossy()
.to_string())
}
_ => unreachable!("非应用路径不应进入此方法"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use zerolaunch_plugin_api::services::path::KnownPath;
#[test]
#[cfg(feature = "portable")]
fn portable_mode_resolves_app_data_to_exe_dir() {
let resolver = WindowsPathResolver::new();
let app_data = resolver
.resolve_path(KnownPath::AppDataDir)
.expect("便携模式解析应用数据目录应成功");
let exe_dir = std::env::current_exe()
.expect("测试进程应有可执行文件路径")
.parent()
.expect("可执行文件路径应有父目录")
.to_string_lossy()
.to_string();
assert_eq!(app_data, exe_dir);
let config_dir = resolver
.resolve_path(KnownPath::AppConfigDir)
.expect("便携模式解析配置目录应成功");
let expected_config = std::path::Path::new(&exe_dir)
.join("config")
.to_string_lossy()
.to_string();
assert_eq!(config_dir, expected_config);
}
#[test]
#[cfg(not(feature = "portable"))]
fn standard_mode_resolves_app_data_to_home() {
let resolver = WindowsPathResolver::new();
let app_data = resolver
.resolve_path(KnownPath::AppDataDir)
.expect("标准模式解析应用数据目录应成功");
let home = dirs::home_dir().expect("测试环境应有 Home 目录");
let expected = home.join(APP_DATA_DIR_NAME).to_string_lossy().to_string();
assert_eq!(app_data, expected);
}
}