Skip to main content

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
35fn validate_app_name(app_name: &str) -> Result<&str> {
36    if app_name.trim().is_empty() {
37        return Err(Error::InvalidInput("app_name cannot be empty"));
38    }
39
40    if app_name.contains('\0') {
41        return Err(Error::InvalidInput("app_name cannot contain NUL bytes"));
42    }
43
44    Ok(app_name)
45}
46
47/// Returns the per-user roaming app-data directory for the given app name.
48///
49/// This function resolves the roaming app-data known folder via `SHGetKnownFolderPath`
50/// and appends `app_name`. It does not create the directory.
51///
52/// # Errors
53///
54/// Returns [`Error::InvalidInput`] if `app_name` is empty, whitespace only, or
55/// contains NUL bytes.
56/// Returns [`Error::WindowsApi`] if the Windows known-folder lookup fails.
57///
58/// # Examples
59///
60/// ```
61/// let path = win_desktop_utils::roaming_app_data("demo-app")?;
62/// assert!(path.ends_with("demo-app"));
63/// # Ok::<(), win_desktop_utils::Error>(())
64/// ```
65pub fn roaming_app_data(app_name: &str) -> Result<PathBuf> {
66    let app_name = validate_app_name(app_name)?;
67
68    let base = known_folder_path(
69        &FOLDERID_RoamingAppData,
70        "SHGetKnownFolderPath(RoamingAppData)",
71    )?;
72    Ok(base.join(app_name))
73}
74
75/// Returns the per-user local app-data directory for the given app name.
76///
77/// This function resolves the local app-data known folder via `SHGetKnownFolderPath`
78/// and appends `app_name`. It does not create the directory.
79///
80/// # Errors
81///
82/// Returns [`Error::InvalidInput`] if `app_name` is empty, whitespace only, or
83/// contains NUL bytes.
84/// Returns [`Error::WindowsApi`] if the Windows known-folder lookup fails.
85///
86/// # Examples
87///
88/// ```
89/// let path = win_desktop_utils::local_app_data("demo-app")?;
90/// assert!(path.ends_with("demo-app"));
91/// # Ok::<(), win_desktop_utils::Error>(())
92/// ```
93pub fn local_app_data(app_name: &str) -> Result<PathBuf> {
94    let app_name = validate_app_name(app_name)?;
95
96    let base = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")?;
97    Ok(base.join(app_name))
98}
99
100/// Returns the roaming app-data directory for the given app name and creates it if needed.
101///
102/// This is equivalent to calling [`roaming_app_data`] and then `create_dir_all` on the result.
103///
104/// # Errors
105///
106/// Propagates errors from [`roaming_app_data`] and directory creation.
107///
108/// # Examples
109///
110/// ```
111/// let path = win_desktop_utils::ensure_roaming_app_data("demo-app")?;
112/// assert!(path.ends_with("demo-app"));
113/// assert!(path.exists());
114/// # Ok::<(), win_desktop_utils::Error>(())
115/// ```
116pub fn ensure_roaming_app_data(app_name: &str) -> Result<PathBuf> {
117    let path = roaming_app_data(app_name)?;
118    fs::create_dir_all(&path)?;
119    Ok(path)
120}
121
122/// Returns the local app-data directory for the given app name and creates it if needed.
123///
124/// This is equivalent to calling [`local_app_data`] and then `create_dir_all` on the result.
125///
126/// # Errors
127///
128/// Propagates errors from [`local_app_data`] and directory creation.
129///
130/// # Examples
131///
132/// ```
133/// let path = win_desktop_utils::ensure_local_app_data("demo-app")?;
134/// assert!(path.ends_with("demo-app"));
135/// assert!(path.exists());
136/// # Ok::<(), win_desktop_utils::Error>(())
137/// ```
138pub fn ensure_local_app_data(app_name: &str) -> Result<PathBuf> {
139    let path = local_app_data(app_name)?;
140    fs::create_dir_all(&path)?;
141    Ok(path)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::{
147        known_folder_path, validate_app_name, FOLDERID_LocalAppData, FOLDERID_RoamingAppData,
148    };
149
150    #[test]
151    fn validate_app_name_rejects_empty_string() {
152        let result = validate_app_name("   ");
153        assert!(matches!(
154            result,
155            Err(crate::Error::InvalidInput("app_name cannot be empty"))
156        ));
157    }
158
159    #[test]
160    fn validate_app_name_rejects_nul_bytes() {
161        let result = validate_app_name("demo\0app");
162        assert!(matches!(
163            result,
164            Err(crate::Error::InvalidInput(
165                "app_name cannot contain NUL bytes"
166            ))
167        ));
168    }
169
170    #[test]
171    fn known_folder_roaming_app_data_exists() {
172        let path = known_folder_path(
173            &FOLDERID_RoamingAppData,
174            "SHGetKnownFolderPath(RoamingAppData)",
175        )
176        .unwrap();
177
178        assert!(path.exists());
179        assert!(path.is_dir());
180    }
181
182    #[test]
183    fn known_folder_local_app_data_exists() {
184        let path = known_folder_path(&FOLDERID_LocalAppData, "SHGetKnownFolderPath(LocalAppData)")
185            .unwrap();
186
187        assert!(path.exists());
188        assert!(path.is_dir());
189    }
190}