Skip to main content

std/os/darwin/fs/
dirs.rs

1use crate::env;
2use crate::fs::{HomeDirs, MediaDirs};
3use crate::io::{self, ErrorKind, const_error};
4use crate::path::PathBuf;
5
6/// Darwin-specific extensions to [`fs::HomeDirs`](HomeDirs).
7#[unstable(feature = "fs_home_dirs", issue = "162082")]
8pub impl(self) trait HomeDirsExt: Sized {
9    /// Load the standard user directory paths for the current user.
10    ///
11    /// On iOS, tvOS, watchOS, visionOS, and sandboxed macOS applications,
12    /// these directories are within the application's container. Outside
13    /// the sandbox, these are subdirectories of the `~/Library` directory on
14    /// macOS.
15    ///
16    /// The produced directory paths are not guaranteed to be the canonical
17    /// paths to the directories; they are allowed to be sandbox-redirected
18    /// paths as long as the directory is accessible there.
19    ///
20    /// The loaded common directories are:
21    ///
22    /// | `HomeDirs` | [`NSSearchPathDirectory`] |
23    /// | ---------- | ----------------------- |
24    /// | [`cache_home`] | [`NSCachesDirectory`] (`~/Library/Caches`) |
25    /// | [`config_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) |
26    /// | [`data_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) |
27    /// | [`state_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) |
28    ///
29    /// Note that the Application Support directory is used for the config,
30    /// data, and state directories. It is always possible for multiple user
31    /// directories to be configured to the same path, but this is the common
32    /// configuration on Apple platforms, making it even more important to not
33    /// assume files in different user directories cannot alias each other.
34    ///
35    /// # Errors
36    ///
37    /// Errors if the [user home](env::home_dir) cannot be determined.
38    //  Errors due to the underlying sysdir(3) API should never occur, as
39    //  - the user domain only has one directory for each search path;
40    //  - the user domain always returns subdirectory paths of `~`; and
41    //  - the username and OS defined path segments are always valid UTF-8.
42    ///
43    /// # Implementation-specific behavior
44    ///
45    /// Uses the `sysdir(3)` API from `libSystem` to discover the standard
46    /// user directories.
47    ///
48    /// This behavior may change in the future. One example change that we
49    /// explicitly reserve the right to make is to load additional common
50    /// directories not currently in this list.
51    ///
52    /// [`cache_home`]: HomeDirs::cache_home
53    /// [`config_home`]: HomeDirs::config_home
54    /// [`data_home`]: HomeDirs::data_home
55    /// [`state_home`]: HomeDirs::state_home
56    ///
57    /// [`NSSearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory?language=objc
58    /// [`NSCachesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory?language=objc
59    /// [`NSApplicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory?language=objc
60    #[unstable(feature = "fs_home_dirs", issue = "162082")]
61    fn sysdir() -> io::Result<Self>;
62}
63
64/// Darwin-specific extensions to [`fs::MediaDirs`](MediaDirs).
65#[unstable(feature = "fs_media_dirs", issue = "162083")]
66pub impl(self) trait MediaDirsExt: Sized {
67    /// Load the standard user directory paths for the current user.
68    ///
69    /// The produced directory paths are not guaranteed to be the canonical
70    /// paths to the directories; they are allowed to be sandbox-redirected
71    /// paths as long as the directory is accessible there.
72    ///
73    /// The loaded common directories are:
74    ///
75    /// | `MediaDirs` | [`NSSearchPathDirectory`] |
76    /// | ---------- | ----------------------- |
77    /// | [`desktop`] | [`NSDesktopDirectory`] (`~/Desktop`) |
78    /// | [`documents`] | [`NSDocumentDirectory`] (`~/Documents`) |
79    /// | [`downloads`] | [`NSDownloadsDirectory`] (`~/Downloads`) |
80    /// | [`music`] | [`NSMusicDirectory`] (`~/Music`) |
81    /// | [`pictures`] | [`NSPicturesDirectory`] (`~/Pictures`) |
82    /// | [`videos`] | [`NSMoviesDirectory`] (`~/Movies`) |
83    ///
84    /// # Errors
85    ///
86    /// Errors if the the [user home](env::home_dir) cannot be determined.
87    //  Errors due to the underlying sysdir(3) API should never occur, as
88    //  - the user domain only has one directory for each search path;
89    //  - the user domain always returns subdirectory paths of `~`;
90    //  - the username plus OS defined path segments cannot exceed PATH_MAX; and
91    //  - the username and OS defined path segments are always valid UTF-8.
92    ///
93    /// # Implementation-specific behavior
94    ///
95    /// Uses the `sysdir(3)` API from `libSystem` to discover the standard
96    /// user directories.
97    ///
98    /// This behavior may change in the future. One example change that we
99    /// explicitly reserve the right to make is to load additional common
100    /// directories not currently in this list.
101    ///
102    /// [`desktop`]: MediaDirs::desktop
103    /// [`documents`]: MediaDirs::documents
104    /// [`downloads`]: MediaDirs::downloads
105    /// [`music`]: MediaDirs::music
106    /// [`pictures`]: MediaDirs::pictures
107    /// [`videos`]: MediaDirs::videos
108    ///
109    /// [`NSSearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory?language=objc
110    /// [`NSDesktopDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/desktopdirectory?language=objc
111    /// [`NSDocumentDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/documentdirectory?language=objc
112    /// [`NSDownloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory?language=objc
113    /// [`NSMusicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory?language=objc
114    /// [`NSPicturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory?language=objc
115    /// [`NSMoviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory?language=objc
116    #[unstable(feature = "fs_media_dirs", issue = "162083")]
117    fn sysdir() -> io::Result<Self>;
118}
119
120fn user_home() -> io::Result<PathBuf> {
121    env::home_dir()
122        .filter(|p| p.is_absolute())
123        .ok_or(const_error!(ErrorKind::InvalidData, "home path not absolute"))
124}
125
126#[unstable(feature = "fs_home_dirs", issue = "162082")]
127#[cfg(target_vendor = "apple")]
128impl HomeDirsExt for HomeDirs {
129    fn sysdir() -> io::Result<Self> {
130        use libc::sysdir_search_path_directory_t::*;
131
132        let mut dirs = HomeDirs::empty();
133        let home = user_home()?;
134
135        let caches = sys::get_user_dir(&home, SYSDIR_DIRECTORY_CACHES)?;
136        let application_support = sys::get_user_dir(&home, SYSDIR_DIRECTORY_APPLICATION_SUPPORT)?;
137
138        dirs.cache = caches;
139        // Apple puts config/data/state all in Application Support
140        dirs.config = application_support.clone();
141        dirs.data = application_support.clone();
142        dirs.state = application_support;
143
144        Ok(dirs)
145    }
146}
147
148#[unstable(feature = "fs_media_dirs", issue = "162083")]
149#[cfg(target_vendor = "apple")]
150impl MediaDirsExt for MediaDirs {
151    fn sysdir() -> io::Result<Self> {
152        use libc::sysdir_search_path_directory_t::*;
153
154        let mut dirs = MediaDirs::empty();
155        let home = user_home()?;
156
157        let desktop = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DESKTOP)?;
158        let documents = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DOCUMENT)?;
159        let downloads = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DOWNLOADS)?;
160        let movies = sys::get_user_dir(&home, SYSDIR_DIRECTORY_MOVIES)?;
161        let music = sys::get_user_dir(&home, SYSDIR_DIRECTORY_MUSIC)?;
162        let pictures = sys::get_user_dir(&home, SYSDIR_DIRECTORY_PICTURES)?;
163
164        dirs.desktop = desktop;
165        dirs.documents = documents;
166        dirs.downloads = downloads;
167        dirs.music = music;
168        dirs.pictures = pictures;
169        dirs.videos = movies;
170
171        Ok(dirs)
172    }
173}
174
175/// Safer wrapper around the sysdir(3) API
176#[cfg(target_vendor = "apple")]
177mod sys {
178    use crate::ffi::{CStr, c_char};
179    use crate::io::{self, ErrorKind, const_error};
180    use crate::path::{Path, PathBuf};
181
182    /// Get the path for a system directory using `sysdir(3)`.
183    pub fn get_user_dir(
184        home: &Path,
185        kind: libc::sysdir_search_path_directory_t,
186    ) -> io::Result<Option<PathBuf>> {
187        use libc::sysdir_search_path_domain_mask_t::SYSDIR_DOMAIN_MASK_USER;
188
189        // SAFETY: SYSDIR_DOMAIN_MASK_USER < SYSDIR_DOMAIN_MASK_ALL
190        let mut iter = unsafe { Iter::new(home, kind, SYSDIR_DOMAIN_MASK_USER) };
191        let Some(path) = iter.next() else {
192            return Ok(None);
193        };
194        let path = path?;
195
196        if iter.next().is_some() {
197            // more than one path returned (shouldn't happen for SYSDIR_DOMAIN_MASK_USER)
198            return Err(const_error!(
199                ErrorKind::InvalidData,
200                "multiple paths returned for standard user directory",
201            ));
202        }
203
204        Ok(Some(path))
205    }
206
207    struct Iter<'a> {
208        home: &'a Path,
209        state: libc::sysdir_search_path_enumeration_state,
210    }
211
212    impl Drop for Iter<'_> {
213        fn drop(&mut self) {
214            for _ in self {}
215        }
216    }
217
218    impl<'a> Iter<'a> {
219        // SAFETY: `mask` must be <= `SYSDIR_DOMAIN_MASK_ALL`
220        pub unsafe fn new(
221            home: &'a Path,
222            kind: libc::sysdir_search_path_directory_t,
223            mask: libc::sysdir_search_path_domain_mask_t,
224        ) -> Self {
225            // SAFETY: forwarded to the caller
226            let state = unsafe { libc::sysdir_start_search_path_enumeration(kind, mask) };
227            Self { home, state }
228        }
229    }
230
231    impl Iterator for Iter<'_> {
232        type Item = io::Result<PathBuf>;
233
234        fn next(&mut self) -> Option<io::Result<PathBuf>> {
235            let mut buf = [0u8; libc::PATH_MAX as usize];
236            if self.state != 0 {
237                // SAFETY: `self.state` is nonzero and comes from prior sysdir_{start|get_next}_search_path_enumeration call
238                // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX` bytes to `path`
239                self.state = unsafe {
240                    libc::sysdir_get_next_search_path_enumeration(
241                        self.state,
242                        buf.as_mut_ptr() as *mut c_char,
243                    )
244                };
245            }
246
247            if self.state == 0 {
248                // exhausted
249                return None;
250            }
251
252            let Ok(path) = CStr::from_bytes_until_nul(&buf) else {
253                // should be impossible given home-relative paths, but be defensive
254                return Some(Err(const_error!(
255                    ErrorKind::InvalidData,
256                    "standard user directory path too long",
257                )));
258            };
259
260            let Ok(path) = path.to_str() else {
261                // should be impossible on a working system, but be defensive
262                return Some(Err(const_error!(
263                    ErrorKind::InvalidData,
264                    "standard user directory path not valid UTF-8",
265                )));
266            };
267
268            // expand `~` shorthand
269            Some(match path {
270                "~" => Ok(self.home.into()),
271                _ if path.starts_with("~/") => Ok(self.home.join(&path[2..])),
272                _ if path.starts_with("~") => Err(const_error!(
273                    ErrorKind::InvalidData,
274                    "standard user directory relative to different user",
275                )),
276                _ => {
277                    let path = PathBuf::from(path);
278                    if path.is_relative() {
279                        Err(const_error!(
280                            ErrorKind::InvalidData,
281                            "standard user directory path not absolute",
282                        ))
283                    } else {
284                        Ok(path)
285                    }
286                }
287            })
288        }
289    }
290}
291
292#[cfg(test)]
293#[cfg(target_vendor = "apple")]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn can_fetch_sysdir_paths() {
299        let dirs = HomeDirs::sysdir().unwrap();
300        assert!(dirs.cache_home().is_some());
301        assert!(dirs.config_home().is_some());
302        assert!(dirs.data_home().is_some());
303        assert!(dirs.state_home().is_some());
304
305        let dirs = MediaDirs::sysdir().unwrap();
306        assert!(dirs.desktop().is_some());
307        assert!(dirs.documents().is_some());
308        assert!(dirs.downloads().is_some());
309        assert!(dirs.music().is_some());
310        assert!(dirs.pictures().is_some());
311        assert!(dirs.videos().is_some());
312    }
313}