use std::ffi::OsString;
use std::fs;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use crate::error::{Error, Result};
use windows::core::GUID;
use windows::Win32::System::Com::CoTaskMemFree;
use windows::Win32::UI::Shell::{
FOLDERID_LocalAppData, FOLDERID_RoamingAppData, SHGetKnownFolderPath, KNOWN_FOLDER_FLAG,
};
fn known_folder_path(folder_id: &GUID, context: &'static str) -> Result<PathBuf> {
let raw = unsafe { SHGetKnownFolderPath(folder_id as *const _, KNOWN_FOLDER_FLAG(0), None) }
.map_err(|err| Error::WindowsApi {
context,
code: err.code().0,
})?;
if raw.is_null() {
return Err(Error::WindowsApi { context, code: 0 });
}
let path = unsafe { PathBuf::from(OsString::from_wide(raw.as_wide())) };
unsafe {
CoTaskMemFree(Some(raw.0.cast()));
}
Ok(path)
}
fn validate_app_name(app_name: &str) -> Result<&str> {
if app_name.trim().is_empty() {
return Err(Error::InvalidInput("app_name cannot be empty"));
}
if app_name.contains('\0') {
return Err(Error::InvalidInput("app_name cannot contain NUL bytes"));
}
Ok(app_name)
}
pub fn roaming_app_data(app_name: &str) -> Result<PathBuf> {
let app_name = validate_app_name(app_name)?;
let base = known_folder_path(
&FOLDERID_RoamingAppData,
"SHGetKnownFolderPath(RoamingAppData)",
)?;
Ok(base.join(app_name))
}
pub fn local_app_data(app_name: &str) -> Result<PathBuf> {
let app_name = validate_app_name(app_name)?;
let base = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")?;
Ok(base.join(app_name))
}
pub fn ensure_roaming_app_data(app_name: &str) -> Result<PathBuf> {
let path = roaming_app_data(app_name)?;
fs::create_dir_all(&path)?;
Ok(path)
}
pub fn ensure_local_app_data(app_name: &str) -> Result<PathBuf> {
let path = local_app_data(app_name)?;
fs::create_dir_all(&path)?;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::{
known_folder_path, validate_app_name, FOLDERID_LocalAppData, FOLDERID_RoamingAppData,
};
#[test]
fn validate_app_name_rejects_empty_string() {
let result = validate_app_name(" ");
assert!(matches!(
result,
Err(crate::Error::InvalidInput("app_name cannot be empty"))
));
}
#[test]
fn validate_app_name_rejects_nul_bytes() {
let result = validate_app_name("demo\0app");
assert!(matches!(
result,
Err(crate::Error::InvalidInput(
"app_name cannot contain NUL bytes"
))
));
}
#[test]
fn known_folder_roaming_app_data_exists() {
let path = known_folder_path(
&FOLDERID_RoamingAppData,
"SHGetKnownFolderPath(RoamingAppData)",
)
.unwrap();
assert!(path.exists());
assert!(path.is_dir());
}
#[test]
fn known_folder_local_app_data_exists() {
let path = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")
.unwrap();
assert!(path.exists());
assert!(path.is_dir());
}
}