win_desktop_utils/paths.rs
1//! Helpers for resolving and creating per-user application data directories.
2
3use std::ffi::OsString;
4use std::fs;
5use std::os::windows::ffi::OsStringExt;
6use std::path::PathBuf;
7
8use crate::error::{Error, Result};
9use windows::core::GUID;
10use windows::Win32::System::Com::CoTaskMemFree;
11use windows::Win32::UI::Shell::{
12 FOLDERID_LocalAppData, FOLDERID_RoamingAppData, SHGetKnownFolderPath, KNOWN_FOLDER_FLAG,
13};
14
15fn known_folder_path(folder_id: &GUID, context: &'static str) -> Result<PathBuf> {
16 let raw = unsafe { SHGetKnownFolderPath(folder_id as *const _, KNOWN_FOLDER_FLAG(0), None) }
17 .map_err(|err| Error::WindowsApi {
18 context,
19 code: err.code().0,
20 })?;
21
22 if raw.is_null() {
23 return Err(Error::WindowsApi { context, code: 0 });
24 }
25
26 let path = unsafe { PathBuf::from(OsString::from_wide(raw.as_wide())) };
27
28 unsafe {
29 CoTaskMemFree(Some(raw.0.cast()));
30 }
31
32 Ok(path)
33}
34
35/// Returns the per-user roaming app-data directory for the given app name.
36///
37/// This function resolves the roaming app-data known folder via `SHGetKnownFolderPath`
38/// and appends `app_name`. It does not create the directory.
39///
40/// # Errors
41///
42/// Returns [`Error::InvalidInput`] if `app_name` is empty or whitespace only.
43/// Returns [`Error::WindowsApi`] if the Windows known-folder lookup fails.
44///
45/// # Examples
46///
47/// ```
48/// let path = win_desktop_utils::roaming_app_data("demo-app")?;
49/// assert!(path.ends_with("demo-app"));
50/// # Ok::<(), win_desktop_utils::Error>(())
51/// ```
52pub fn roaming_app_data(app_name: &str) -> Result<PathBuf> {
53 if app_name.trim().is_empty() {
54 return Err(Error::InvalidInput("app_name cannot be empty"));
55 }
56
57 let base = known_folder_path(
58 &FOLDERID_RoamingAppData,
59 "SHGetKnownFolderPath(RoamingAppData)",
60 )?;
61 Ok(base.join(app_name))
62}
63
64/// Returns the per-user local app-data directory for the given app name.
65///
66/// This function resolves the local app-data known folder via `SHGetKnownFolderPath`
67/// and appends `app_name`. It does not create the directory.
68///
69/// # Errors
70///
71/// Returns [`Error::InvalidInput`] if `app_name` is empty or whitespace only.
72/// Returns [`Error::WindowsApi`] if the Windows known-folder lookup fails.
73///
74/// # Examples
75///
76/// ```
77/// let path = win_desktop_utils::local_app_data("demo-app")?;
78/// assert!(path.ends_with("demo-app"));
79/// # Ok::<(), win_desktop_utils::Error>(())
80/// ```
81pub fn local_app_data(app_name: &str) -> Result<PathBuf> {
82 if app_name.trim().is_empty() {
83 return Err(Error::InvalidInput("app_name cannot be empty"));
84 }
85
86 let base = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")?;
87 Ok(base.join(app_name))
88}
89
90/// Returns the roaming app-data directory for the given app name and creates it if needed.
91///
92/// This is equivalent to calling [`roaming_app_data`] and then `create_dir_all` on the result.
93///
94/// # Errors
95///
96/// Propagates errors from [`roaming_app_data`] and directory creation.
97///
98/// # Examples
99///
100/// ```
101/// let path = win_desktop_utils::ensure_roaming_app_data("demo-app")?;
102/// assert!(path.ends_with("demo-app"));
103/// assert!(path.exists());
104/// # Ok::<(), win_desktop_utils::Error>(())
105/// ```
106pub fn ensure_roaming_app_data(app_name: &str) -> Result<PathBuf> {
107 let path = roaming_app_data(app_name)?;
108 fs::create_dir_all(&path)?;
109 Ok(path)
110}
111
112/// Returns the local app-data directory for the given app name and creates it if needed.
113///
114/// This is equivalent to calling [`local_app_data`] and then `create_dir_all` on the result.
115///
116/// # Errors
117///
118/// Propagates errors from [`local_app_data`] and directory creation.
119///
120/// # Examples
121///
122/// ```
123/// let path = win_desktop_utils::ensure_local_app_data("demo-app")?;
124/// assert!(path.ends_with("demo-app"));
125/// assert!(path.exists());
126/// # Ok::<(), win_desktop_utils::Error>(())
127/// ```
128pub fn ensure_local_app_data(app_name: &str) -> Result<PathBuf> {
129 let path = local_app_data(app_name)?;
130 fs::create_dir_all(&path)?;
131 Ok(path)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::{known_folder_path, FOLDERID_LocalAppData, FOLDERID_RoamingAppData};
137
138 #[test]
139 fn known_folder_roaming_app_data_exists() {
140 let path = known_folder_path(
141 &FOLDERID_RoamingAppData,
142 "SHGetKnownFolderPath(RoamingAppData)",
143 )
144 .unwrap();
145
146 assert!(path.exists());
147 assert!(path.is_dir());
148 }
149
150 #[test]
151 fn known_folder_local_app_data_exists() {
152 let path = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")
153 .unwrap();
154
155 assert!(path.exists());
156 assert!(path.is_dir());
157 }
158}