etcetera/base_strategy/
windows.rs

1use std::path::{Path, PathBuf};
2
3use crate::HomeDirError;
4
5/// This strategy follows Windows’ conventions. It seems that all Windows GUI apps, and some command-line ones follow this pattern. The specification is available [here](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
6///
7/// This initial example removes all the relevant environment variables to show the strategy’s use of the:
8/// - (on Windows) SHGetKnownFolderPath API.
9/// - (on non-Windows) Windows default directories.
10///
11/// ```
12/// use etcetera::base_strategy::BaseStrategy;
13/// use etcetera::base_strategy::Windows;
14/// use std::path::Path;
15///
16/// // Remove the environment variables that the strategy reads from.
17/// unsafe {
18/// std::env::remove_var("USERPROFILE");
19/// std::env::remove_var("APPDATA");
20/// std::env::remove_var("LOCALAPPDATA");
21/// }
22///
23/// let base_strategy = Windows::new().unwrap();
24///
25/// let home_dir = etcetera::home_dir().unwrap();
26///
27/// assert_eq!(
28///     base_strategy.home_dir(),
29///     &home_dir
30/// );
31/// assert_eq!(
32///     base_strategy.config_dir().strip_prefix(&home_dir),
33///     Ok(Path::new("AppData/Roaming/"))
34/// );
35/// assert_eq!(
36///     base_strategy.data_dir().strip_prefix(&home_dir),
37///     Ok(Path::new("AppData/Roaming/"))
38/// );
39/// assert_eq!(
40///     base_strategy.cache_dir().strip_prefix(&home_dir),
41///     Ok(Path::new("AppData/Local/"))
42/// );
43/// assert_eq!(
44///     base_strategy.state_dir(),
45///     None
46/// );
47/// assert_eq!(
48///     base_strategy.runtime_dir(),
49///     None
50/// );
51/// ```
52///
53/// This next example gives the environment variables values:
54///
55/// ```
56/// use etcetera::base_strategy::BaseStrategy;
57/// use etcetera::base_strategy::Windows;
58/// use std::path::Path;
59///
60/// let home_path = if cfg!(windows) {
61///     "C:\\foo\\".to_string()
62/// } else {
63///     etcetera::home_dir().unwrap().to_string_lossy().to_string()
64/// };
65/// let data_path = if cfg!(windows) {
66///     "C:\\bar\\"
67/// } else {
68///     "/bar/"
69/// };
70/// let cache_path = if cfg!(windows) {
71///     "C:\\baz\\"
72/// } else {
73///     "/baz/"
74/// };
75///
76/// unsafe {
77/// std::env::set_var("USERPROFILE", &home_path);
78/// std::env::set_var("APPDATA", data_path);
79/// std::env::set_var("LOCALAPPDATA", cache_path);
80/// }
81///
82/// let base_strategy = Windows::new().unwrap();
83///
84/// assert_eq!(
85///     base_strategy.home_dir(),
86///     Path::new(&home_path)
87/// );
88/// assert_eq!(
89///     base_strategy.config_dir(),
90///     Path::new(data_path)
91/// );
92/// assert_eq!(
93///     base_strategy.data_dir(),
94///     Path::new(data_path)
95/// );
96/// assert_eq!(
97///     base_strategy.cache_dir(),
98///     Path::new(cache_path)
99/// );
100/// assert_eq!(
101///     base_strategy.state_dir(),
102///     None
103/// );
104/// assert_eq!(
105///     base_strategy.runtime_dir(),
106///     None
107/// );
108/// ```
109
110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
111pub struct Windows {
112    home_dir: PathBuf,
113}
114
115impl Windows {
116    /// Create a new Windows BaseStrategy
117    pub fn new() -> Result<Self, HomeDirError> {
118        Ok(Self {
119            home_dir: crate::home_dir()?,
120        })
121    }
122
123    fn dir_inner(env: &'static str) -> Option<PathBuf> {
124        std::env::var_os(env)
125            .filter(|s| !s.is_empty())
126            .map(PathBuf::from)
127            .or_else(|| Self::dir_crt(env))
128    }
129
130    // Ref: https://github.com/rust-lang/cargo/blob/home-0.5.11/crates/home/src/windows.rs
131    // We should keep this code in sync with the above.
132    #[cfg(all(windows, not(target_vendor = "uwp")))]
133    fn dir_crt(env: &'static str) -> Option<PathBuf> {
134        use std::ffi::OsString;
135        use std::os::windows::ffi::OsStringExt;
136        use std::ptr;
137        use std::slice;
138
139        use windows_sys::Win32::Foundation::S_OK;
140        use windows_sys::Win32::System::Com::CoTaskMemFree;
141        use windows_sys::Win32::UI::Shell::{
142            FOLDERID_LocalAppData, FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY,
143            SHGetKnownFolderPath,
144        };
145
146        unsafe extern "C" {
147            fn wcslen(buf: *const u16) -> usize;
148        }
149
150        let folder_id = match env {
151            "APPDATA" => FOLDERID_RoamingAppData,
152            "LOCALAPPDATA" => FOLDERID_LocalAppData,
153            _ => return None,
154        };
155
156        unsafe {
157            let mut path = ptr::null_mut();
158            match SHGetKnownFolderPath(
159                &folder_id,
160                KF_FLAG_DONT_VERIFY as u32,
161                std::ptr::null_mut(),
162                &mut path,
163            ) {
164                S_OK => {
165                    let path_slice = slice::from_raw_parts(path, wcslen(path));
166                    let s = OsString::from_wide(path_slice);
167                    CoTaskMemFree(path.cast());
168                    Some(PathBuf::from(s))
169                }
170                _ => {
171                    // Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
172                    CoTaskMemFree(path.cast());
173                    None
174                }
175            }
176        }
177    }
178
179    #[cfg(not(all(windows, not(target_vendor = "uwp"))))]
180    fn dir_crt(_env: &'static str) -> Option<PathBuf> {
181        None
182    }
183}
184
185impl super::BaseStrategy for Windows {
186    fn home_dir(&self) -> &Path {
187        &self.home_dir
188    }
189
190    fn config_dir(&self) -> PathBuf {
191        self.data_dir()
192    }
193
194    fn data_dir(&self) -> PathBuf {
195        Self::dir_inner("APPDATA").unwrap_or_else(|| self.home_dir.join("AppData").join("Roaming"))
196    }
197
198    fn cache_dir(&self) -> PathBuf {
199        Self::dir_inner("LOCALAPPDATA")
200            .unwrap_or_else(|| self.home_dir.join("AppData").join("Local"))
201    }
202
203    fn state_dir(&self) -> Option<PathBuf> {
204        None
205    }
206
207    fn runtime_dir(&self) -> Option<PathBuf> {
208        None
209    }
210}