win_desktop_utils/
paths.rs1use 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
47pub 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
75pub 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
100pub 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
122pub 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}