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}