Skip to main content

std/os/windows/fs/
dirs.rs

1use crate::fs::{HomeDirs, MediaDirs};
2use crate::io::{ErrorKind, const_error};
3use crate::path::PathBuf;
4use crate::{env, io};
5
6/// Windows-specific extensions to [`fs::HomeDirs`](HomeDirs).
7#[unstable(feature = "fs_home_dirs", issue = "162082")]
8pub impl(self) trait HomeDirsExt: Sized {
9    /// Load the known user folder paths from environment variables.
10    ///
11    /// The loaded known folders are:
12    ///
13    /// | `HomeDirs` | Environment Variable |
14    /// | ---------- | -------------------- |
15    /// | [`cache_home`] | `%LOCALAPPDATA%` (`%USERPROFILE%\AppData\Local`) |
16    /// | [`config_home`] | `%APPDATA%` (`%USERPROFILE%\AppData\Roaming`) |
17    /// | [`data_home`] | `%APPDATA%` (`%USERPROFILE%\AppData\Roaming`) |
18    /// | [`state_home`] | `%LOCALAPPDATA%` (`%USERPROFILE%\AppData\Local`) |
19    ///
20    /// Note that caches/state are both put in `AppData\Local`, and config/data
21    /// in `AppData\Roaming`. It is always possible for multiple user directories
22    /// to be configured to the same path, but this is the common configuration
23    /// on Windows platforms, making it even more important to not assume files
24    /// in different user directories cannot alias each other.
25    ///
26    /// # Errors
27    ///
28    /// Errors if `%APPDATA%` or `%LOCALAPPDATA%` are not set to absolute paths.
29    ///
30    /// # Implementation-specific behavior
31    ///
32    /// Windows keeps these environment variables updated to contain the paths
33    /// to the configured folder path, but it is possible for the environment
34    /// variables to not match the underlying system, such as when the user or
35    /// a program modifies the environment directly, or if the configuration
36    /// changed after the environment block was copied from the system.
37    ///
38    /// Unlike [`known_folders`](Self::known_folders), this does not require
39    /// `Shell32.dll` and thus does not require the overhead of linking in
40    /// DLLs that may result in Windows considering the application as a
41    /// graphical application.
42    ///
43    /// This behavior may change in the future. One example change that we
44    /// explicitly reserve the right to make is to load additional common
45    /// directories not currently in this list.
46    ///
47    /// [`cache_home`]: HomeDirs::cache_home
48    /// [`config_home`]: HomeDirs::config_home
49    /// [`data_home`]: HomeDirs::data_home
50    /// [`state_home`]: HomeDirs::state_home
51    #[unstable(feature = "fs_home_dirs", issue = "162082")]
52    fn appdata_env() -> io::Result<Self>;
53
54    /// Load the known user folder paths using the [Known Folders] API.
55    ///
56    /// The loaded known folders are:
57    ///
58    /// | `HomeDirs` | [`KNOWNFOLDERID`] |
59    /// | ---------- | ----------------- |
60    /// | [`cache_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) |
61    /// | [`config_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) |
62    /// | [`data_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) |
63    /// | [`state_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) |
64    ///
65    /// Note that caches/state are both put in LocalAppData, and config/data
66    /// in RoamingAppData. It is always possible for multiple user directories
67    /// to be configured to the same path, but this is the common configuration
68    /// on Windows platforms, making it even more important to not assume files
69    /// in different user directories cannot alias each other.
70    ///
71    /// # Errors
72    ///
73    /// Errors if the underlying system discovery API returns an error. The
74    /// lack of a configured path is not considered an error and results in a
75    /// `None` value.
76    ///
77    /// # Implementation-specific behavior
78    ///
79    /// Calls [`SHGetKnownFolderPath`] for the current user once for each known
80    /// folder. Does not create the folder if missing.
81    ///
82    /// COM should be initialized on the thread that calls this function or
83    /// else it may return unexpected errors.
84    ///
85    /// This behavior may change in the future. One example change that we
86    /// explicitly reserve the right to make is to load additional common
87    /// directories not currently in this list.
88    ///
89    /// [Known Folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders
90    /// [`SHGetKnownFolderPath`]: https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
91    /// [`KNOWNFOLDERID`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
92    /// [`FOLDERID_LocalAppData`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_localappdata
93    /// [`FOLDERID_RoamingAppData`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_roamingappdata
94    /// [`cache_home`]: HomeDirs::cache_home
95    /// [`config_home`]: HomeDirs::config_home
96    /// [`data_home`]: HomeDirs::data_home
97    /// [`state_home`]: HomeDirs::state_home
98    #[unstable(feature = "fs_home_dirs", issue = "162082")]
99    fn known_folders() -> io::Result<Self>;
100}
101
102/// Windows-specific extensions to [`fs::MediaDirs`](MediaDirs).
103#[unstable(feature = "fs_media_dirs", issue = "162083")]
104pub impl(self) trait MediaDirsExt: Sized {
105    /// Load the known user folder paths using the [Known Folders] API.
106    ///
107    /// The loaded known folders are:
108    ///
109    /// | `MediaDirs` | [`KNOWNFOLDERID`] |
110    /// | ---------- | ----------------- |
111    /// | [`desktop`] | [`FOLDERID_Desktop`] (`%USERPROFILE%\Desktop`) |
112    /// | [`documents`] | [`FOLDERID_Documents`] (`%USERPROFILE%\Documents`) |
113    /// | [`downloads`] | [`FOLDERID_Downloads`] (`%USERPROFILE%\Downloads`) |
114    /// | [`music`] | [`FOLDERID_Music`] (`%USERPROFILE%\Music`) |
115    /// | [`pictures`] | [`FOLDERID_Pictures`] (`%USERPROFILE%\Pictures`) |
116    /// | [`videos`] | [`FOLDERID_Videos`] (`%USERPROFILE%\Videos`) |
117    ///
118    /// # Errors
119    ///
120    /// Errors if the underlying system discovery API returns an error. The
121    /// lack of a configured path is not considered an error and results in a
122    /// `None` value.
123    ///
124    /// # Implementation-specific behavior
125    ///
126    /// Calls [`SHGetKnownFolderPath`] for the current user once for each known
127    /// folder. Does not create the folder if missing.
128    ///
129    /// COM should be initialized on the thread that calls this function or
130    /// else it may return unexpected errors.
131    ///
132    /// This behavior may change in the future. One example change that we
133    /// explicitly reserve the right to make is to load additional common
134    /// directories not currently in this list.
135    ///
136    /// [Known Folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders
137    /// [`SHGetKnownFolderPath`]: https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
138    /// [`KNOWNFOLDERID`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
139    /// [`FOLDERID_Desktop`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_desktop
140    /// [`FOLDERID_Documents`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_documents
141    /// [`FOLDERID_Downloads`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_downloads
142    /// [`FOLDERID_Music`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_music
143    /// [`FOLDERID_Pictures`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_pictures
144    /// [`FOLDERID_Videos`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos
145    /// [`desktop`]: MediaDirs::desktop
146    /// [`documents`]: MediaDirs::documents
147    /// [`downloads`]: MediaDirs::downloads
148    /// [`music`]: MediaDirs::music
149    /// [`pictures`]: MediaDirs::pictures
150    /// [`videos`]: MediaDirs::videos
151    #[unstable(feature = "fs_media_dirs", issue = "162083")]
152    fn known_folders() -> io::Result<Self>;
153}
154
155#[cfg(windows)]
156#[unstable(feature = "fs_home_dirs", issue = "162082")]
157impl HomeDirsExt for HomeDirs {
158    fn appdata_env() -> io::Result<Self> {
159        let roaming_app_data = env::var_os("APPDATA")
160            .map(PathBuf::from)
161            .filter(|p| p.is_absolute())
162            .ok_or(const_error!(ErrorKind::InvalidData, "non-absolute %APPDATA%"))?;
163        let local_app_data = env::var_os("LOCALAPPDATA")
164            .map(PathBuf::from)
165            .filter(|p| p.is_absolute())
166            .ok_or(const_error!(ErrorKind::InvalidData, "non-absolute %LOCALAPPDATA%"))?;
167
168        // AppData/Local -- system-local, doesn't make sense to sync to another
169        // AppData/Roaming -- data that makes sense to sync across machines
170
171        let mut dirs = HomeDirs::empty();
172
173        dirs.cache = Some(local_app_data.clone());
174        dirs.config = Some(roaming_app_data.clone());
175        dirs.data = Some(roaming_app_data);
176        dirs.state = Some(local_app_data);
177
178        Ok(dirs)
179    }
180
181    fn known_folders() -> io::Result<Self> {
182        use crate::sys::c;
183
184        let local_app_data = sys::get_known_folder_path(&c::FOLDERID_LocalAppData)?;
185        let roaming_app_data = sys::get_known_folder_path(&c::FOLDERID_RoamingAppData)?;
186
187        // AppData/Local -- system-local, doesn't make sense to sync to another
188        // AppData/Roaming -- data that makes sense to sync across machines
189
190        let mut dirs = HomeDirs::empty();
191
192        dirs.cache = local_app_data.clone();
193        dirs.config = roaming_app_data.clone();
194        dirs.data = roaming_app_data;
195        dirs.state = local_app_data;
196
197        Ok(dirs)
198    }
199}
200
201#[cfg(windows)]
202#[unstable(feature = "fs_media_dirs", issue = "162083")]
203impl MediaDirsExt for MediaDirs {
204    fn known_folders() -> io::Result<Self> {
205        use crate::sys::c;
206
207        let desktop = sys::get_known_folder_path(&c::FOLDERID_Desktop)?;
208        let documents = sys::get_known_folder_path(&c::FOLDERID_Documents)?;
209        let downloads = sys::get_known_folder_path(&c::FOLDERID_Downloads)?;
210        let music = sys::get_known_folder_path(&c::FOLDERID_Music)?;
211        let pictures = sys::get_known_folder_path(&c::FOLDERID_Pictures)?;
212        let videos = sys::get_known_folder_path(&c::FOLDERID_Videos)?;
213
214        let mut dirs = MediaDirs::empty();
215
216        dirs.desktop = desktop;
217        dirs.documents = documents;
218        dirs.downloads = downloads;
219        dirs.music = music;
220        dirs.pictures = pictures;
221        dirs.videos = videos;
222
223        Ok(dirs)
224    }
225}
226
227#[cfg(windows)]
228mod sys {
229    use crate::io::{self, ErrorKind, const_error};
230    use crate::path::PathBuf;
231    use crate::sys::{c, os2path};
232    use crate::{ptr, slice};
233
234    /// Retrieve a known folder path from the Windows API.
235    pub fn get_known_folder_path(id: &c::GUID) -> io::Result<Option<PathBuf>> {
236        // Get the known folder path. hToken = NULL requests the current user
237        // scope, and we set KF_FLAG_DONT_VERIFY because it's a bit faster and
238        // we don't guarantee that the directories at the paths exist.
239        let mut pszPath = ptr::null_mut();
240        // SAFETY: rfid/ppszPath are valid pointers, flags are appropriate, and
241        //   a NULL hToken is supported by SHGetKnownFolderPath.
242        let hr = unsafe {
243            c::SHGetKnownFolderPath(
244                /* rfid */ id,
245                /* dwFlags */ c::KF_FLAG_DONT_VERIFY as _,
246                /* hToken */ ptr::null_mut(),
247                /* ppszPath */ &mut pszPath,
248            )
249        };
250
251        let result = match hr {
252            c::S_OK => {
253                // SAFETY: pszPath was populated by a successful call to SHGetKnownFolderPath
254                //   and is valid up to and including its nul terminator
255                let len = unsafe { c::lstrlenW(pszPath) };
256                // SAFETY: *pszPath is valid up to and including its nul terminator
257                Ok(Some(os2path(unsafe { slice::from_raw_parts(pszPath, len as usize) })))
258            }
259            c::E_FAIL => {
260                // This known folder id exists but does not have a path
261                if cfg!(debug_assertions) {
262                    unreachable!("should not call get_known_folder_path on a virtual folder");
263                } else {
264                    Err(const_error!(
265                        ErrorKind::InvalidInput,
266                        "virtual known folders do not have paths"
267                    ))
268                }
269            }
270            c::E_INVALIDARG => {
271                // This known folder id is not present on the system
272                Ok(None)
273            }
274            _ => {
275                // Miscellaneous error
276                Err(io::Error::from_raw_os_error(hr))
277            }
278        };
279
280        // SAFETY: The caller is responsible for freeing the path returned by
281        //   SHGetKnownFolderPath by calling CoTaskMemFree, whether it
282        //   succeeds or not.
283        unsafe { c::CoTaskMemFree(pszPath.cast()) };
284
285        result
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn can_fetch_known_folder_paths() {
295        let dirs = HomeDirs::known_folders().unwrap();
296        assert!(dirs.cache_home().is_some());
297        assert!(dirs.config_home().is_some());
298        assert!(dirs.data_home().is_some());
299        assert!(dirs.state_home().is_some());
300
301        let dirs = MediaDirs::known_folders().unwrap();
302        assert!(dirs.desktop().is_some());
303        assert!(dirs.documents().is_some());
304        assert!(dirs.downloads().is_some());
305        assert!(dirs.music().is_some());
306        assert!(dirs.pictures().is_some());
307        assert!(dirs.videos().is_some());
308    }
309}