diff --git a/library/std/src/env.rs b/library/std/src/env.rs index 1cbabf572367e..5f20956076c77 100644 --- a/library/std/src/env.rs +++ b/library/std/src/env.rs @@ -606,7 +606,6 @@ impl Error for JoinPathsError { /// For example, [XDG Base Directories] on Unix or the `LOCALAPPDATA` and `APPDATA` environment variables on Windows. /// /// [XDG Base Directories]: https://specifications.freedesktop.org/basedir-spec/latest/ -// feature(xdg_basedir): This should link to std::os::unix::xdg once it's stabilized /// /// # Unix /// diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 750ddb91c482b..a6eabf76ca345 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -48,6 +48,13 @@ use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner, fs as fs_imp}; use crate::time::SystemTime; use crate::{error, fmt}; +pub(crate) mod dirs; + +#[unstable(feature = "dir_discovery", issue = "157515")] +pub use self::dirs::HomeDirs; +#[unstable(feature = "media_dir_discovery", issue = "157515")] +pub use self::dirs::MediaDirs; + /// An object providing access to an open file on the filesystem. /// /// An instance of a `File` can be read and/or written depending on what options diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs new file mode 100644 index 0000000000000..b6fd5253af07b --- /dev/null +++ b/library/std/src/fs/dirs.rs @@ -0,0 +1,558 @@ +use crate::mem; +use crate::path::{Path, PathBuf}; +use crate::sys::fs::{ExtraHomeDirs, ExtraMediaDirs}; + +/// Common user directory paths used for user-specific application files. +/// +/// It is not required that the user directories are accessible by the current +/// user, nor that there is a directory at that path. A robust application +/// should handle the case where user directories are incorrectly configured. +/// +/// Even when configured correctly, multiple paths may be to the same location. +/// As such, you should not assume that a file written relative to one directory +/// will not conflict with the same relative path in a different home directory. +/// +/// # Platform-specific behavior +/// +/// As the filesystem conventions for discovering directories varies between +/// operating systems, constructors for `HomeDirs` that use the host platform's +/// filesystem conventions are provided as extension traits under the `std::os` +/// module. +#[unstable(feature = "dir_discovery", issue = "157515")] +#[derive(Debug, Clone)] +pub struct HomeDirs { + pub(crate) cache: Option, + pub(crate) config: Option, + pub(crate) data: Option, + pub(crate) state: Option, + #[cfg_attr(not(unix), expect(dead_code, reason = "no extra home dirs"))] + pub(crate) extra: ExtraHomeDirs, +} + +/// Common user directory paths used for user-specific media files. +/// +/// It is not required that the media directories are accessible by the current +/// user, nor that there is a directory at that path. A robust application +/// should handle the case where media directories are incorrectly configured. +/// +/// Even when configured correctly, multiple paths may be to the same location. +/// As such, you should not assume that a file written relative to one directory +/// will not conflict with the same relative path in a different media directory. +/// +/// # Platform-specific behavior +/// +/// As the filesystem conventions for discovering directories varies between +/// operating systems, constructors for `MediaDirs` that use the host platform's +/// filesystem conventions are provided as extension traits under the `std::os` +/// module. +#[unstable(feature = "media_dir_discovery", issue = "157515")] +#[derive(Debug, Clone)] +pub struct MediaDirs { + pub(crate) desktop: Option, + pub(crate) documents: Option, + pub(crate) downloads: Option, + pub(crate) music: Option, + pub(crate) pictures: Option, + pub(crate) videos: Option, + #[cfg_attr(not(unix), expect(dead_code, reason = "no extra media dirs"))] + pub(crate) extra: ExtraMediaDirs, +} + +impl HomeDirs { + /// Create a known user directory set with no known directories. + /// + /// This is useful with the builder `set_*` methods to create a `HomeDirs` + /// with exactly the directories you want, without any other defaults. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn empty() -> Self { + Self { cache: None, config: None, data: None, state: None, extra: Default::default() } + } + + /// A base directory relative to which user-specific non-essential cache + /// data files should be stored. + /// + /// Files in this directory may be automatically purged any time they are + /// not currently open. + /// + /// This is the same directory for all applications. As such, applications + /// should use a subdirectory for application-specific cache files. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `${XDG_CACHE_HOME:-$HOME/.cache}` | + /// | [Darwin] (macOS) | [`NSCachesDirectory`] (`$HOME/Library/Caches`) | + /// | [Windows] | [`{FOLDERID_LocalAppData}`] (`%LOCALAPPDATA%`) | + /// + /// Other paths can be configured via [`set_cache_home`](Self::set_cache_home). + /// + /// [XDG]: crate::os::unix::fs::HomeDirsExt + /// [Darwin]: crate::os::darwin::fs::HomeDirsExt + /// [Windows]: crate::os::windows::fs::HomeDirsExt + /// [`NSCachesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory?language=objc + /// [`{FOLDERID_LocalAppData}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_localappdata + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn cache_home(&self) -> Option<&Path> { + self.cache.as_deref() + } + + /// A base directory relative to which user-specific configuration files + /// should be stored. + /// + /// This is the same directory for all applications. As such, applications + /// should use a subdirectory for application-specific configuration files. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `${XDG_CONFIG_HOME:-$HOME/.config}` | + /// | [Darwin] (macOS) | [`NSApplicationSupportDirectory`] (`$HOME/Library/Application Support`) | + /// | [Windows] | [`{FOLDERID_RoamingAppData}`] (`%APPDATA%`) | + /// + /// Other paths can be configured via [`set_config_home`](Self::set_config_home). + /// + /// [XDG]: crate::os::unix::fs::HomeDirsExt + /// [Darwin]: crate::os::darwin::fs::HomeDirsExt + /// [Windows]: crate::os::windows::fs::HomeDirsExt + /// [`NSApplicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory?language=objc + /// [`{FOLDERID_RoamingAppData}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_roamingappdata + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn config_home(&self) -> Option<&Path> { + self.config.as_deref() + } + + /// A base directory relative to which user-specific data files should be + /// stored. + /// + /// This is the same directory for all applications. As such, applications + /// should use a subdirectory for application-specific data files. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `${XDG_DATA_HOME:-$HOME/.local/share}` | + /// | [Darwin] (macOS) | [`NSApplicationSupportDirectory`] (`$HOME/Library/Application Support`) | + /// | [Windows] | [`{FOLDERID_RoamingAppData}`] (`%APPDATA%`) | + /// + /// Other paths can be configured via [`set_data_home`](Self::set_data_home). + /// + /// [XDG]: crate::os::unix::fs::HomeDirsExt + /// [Darwin]: crate::os::darwin::fs::HomeDirsExt + /// [Windows]: crate::os::windows::fs::HomeDirsExt + /// [`NSApplicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory?language=objc + /// [`{FOLDERID_RoamingAppData}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_roamingappdata + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn data_home(&self) -> Option<&Path> { + self.data.as_deref() + } + + /// A base directory relative to which user-specific state files should be + /// stored. + /// + /// "State" files are data that should persist between application restarts, + /// but which is not important nor portable enough to the user to be stored + /// in the [data](Self::data_home) directory. Common examples include history + /// (such as logs, recently used files, etc) and any current state of the + /// application that should be reused (such as view, layout, open files, + /// undo history, etc). + /// + /// This is the same directory for all applications. As such, applications + /// should use a subdirectory for application-specific state files. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `${XDG_STATE_HOME:-$HOME/.local/state}` | + /// | [Darwin] (macOS) | [`NSApplicationSupportDirectory`] (`$HOME/Library/Application Support`) | + /// | [Windows] | [`{FOLDERID_LocalAppData}`] (`%LOCALAPPDATA%`) | + /// + /// Other paths can be configured via [`set_state_home`](Self::set_state_home). + /// + /// [XDG]: crate::os::unix::fs::HomeDirsExt + /// [Darwin]: crate::os::darwin::fs::HomeDirsExt + /// [Windows]: crate::os::windows::fs::HomeDirsExt + /// [`NSApplicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory?language=objc + /// [`{FOLDERID_LocalAppData}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_localappdata + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn state_home(&self) -> Option<&Path> { + self.state.as_deref() + } +} + +impl MediaDirs { + /// Create a known user directory set with no known directories. + /// + /// This is useful with the builder `set_*` methods to create a `MediaDirs` + /// with exactly the directories you want, without any other defaults. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn empty() -> Self { + Self { + desktop: None, + documents: None, + downloads: None, + music: None, + pictures: None, + videos: None, + extra: Default::default(), + } + } + + /// The OS-privileged user "Desktop" directory, often the `Desktop` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_DESKTOP_DIR` (`$HOME/Desktop`) | + /// | [Darwin] (macOS) | [`NSDesktopDirectory`] (`$HOME/Desktop`) | + /// | [Windows] | [`{FOLDERID_Desktop}`] (`%USERPROFILE%\Desktop`) | + /// + /// Other paths can be configured via [`set_desktop`](Self::set_desktop). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSDesktopDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/desktopdirectory?language=objc + /// [`{FOLDERID_Desktop}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_desktop + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn desktop(&self) -> Option<&Path> { + self.desktop.as_deref() + } + + /// The OS-privileged user "Documents" directory, often the `Documents` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_DOCUMENTS_DIR` (`$HOME/Documents`) | + /// | [Darwin] (macOS) | [`NSDocumentDirectory`] (`$HOME/Documents`) | + /// | [Windows] | [`{FOLDERID_Documents}`] (`%USERPROFILE%\Documents`) | + /// + /// Other paths can be configured via [`set_documents`](Self::set_documents). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSDocumentDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/documentdirectory?language=objc + /// [`{FOLDERID_Documents}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_documents + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn documents(&self) -> Option<&Path> { + self.documents.as_deref() + } + + /// The OS-privileged user "Downloads" directory, often the `Downloads` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_DOWNLOAD_DIR` (`$HOME/Downloads`) | + /// | [Darwin] (macOS) | [`NSDownloadsDirectory`] (`$HOME/Downloads`) | + /// | [Windows] | [`{FOLDERID_Downloads}`] (`%USERPROFILE%\Downloads`) | + /// + /// Other paths can be configured via [`set_downloads`](Self::set_downloads). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSDownloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory?language=objc + /// [`{FOLDERID_Downloads}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_downloads + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn downloads(&self) -> Option<&Path> { + self.downloads.as_deref() + } + + /// The OS-privileged user "Music" directory, often the `Music` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_MUSIC_DIR` (`$HOME/Music`) | + /// | [Darwin] (macOS) | [`NSMusicDirectory`] (`$HOME/Music`) | + /// | [Windows] | [`{FOLDERID_Music}`] (`%USERPROFILE%\Music`) | + /// + /// Other paths can be configured via [`set_music`](Self::set_music). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSMusicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory?language=objc + /// [`{FOLDERID_Music}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_music + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn music(&self) -> Option<&Path> { + self.music.as_deref() + } + + /// The OS-privileged user "Pictures" directory, often the `Pictures` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_PICTURES_DIR` (`$HOME/Pictures`) | + /// | [Darwin] (macOS) | [`NSPicturesDirectory`] (`$HOME/Pictures`) | + /// | [Windows] | [`{FOLDERID_Pictures}`] (`%USERPROFILE%\Pictures`) | + /// + /// Other paths can be configured via [`set_pictures`](Self::set_pictures). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSPicturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory?language=objc + /// [`{FOLDERID_Pictures}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_pictures + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn pictures(&self) -> Option<&Path> { + self.pictures.as_deref() + } + + /// The OS-privileged user "Videos" directory, often the `Videos` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + /// + /// # Platform-specific behavior + /// + /// When constructed using platform-specific conventions, the value is: + /// + /// | OS | Path | + /// | -- | ---- | + /// | [XDG] (Linux) | `$XDG_VIDEOS_DIR` (`$HOME/Videos`) | + /// | [Darwin] (macOS) | [`NSMoviesDirectory`] (`$HOME/Movies`) | + /// | [Windows] | [`{FOLDERID_Videos}`] (`%USERPROFILE%\Videos`) | + /// + /// Other paths can be configured via [`set_videos`](Self::set_videos). + /// + /// [XDG]: crate::os::unix::fs::MediaDirsExt + /// [Darwin]: crate::os::darwin::fs::MediaDirsExt + /// [Windows]: crate::os::windows::fs::MediaDirsExt + /// [`NSMoviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory?language=objc + /// [`{FOLDERID_Videos}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn videos(&self) -> Option<&Path> { + self.videos.as_deref() + } +} + +impl HomeDirs { + /// Take the contents of this directory set, leaving it empty. + /// + /// This is useful with the builder `set_*` methods to build `HomeDirs` + /// without needing an intermediate mutable binding. + /// + /// # Examples + /// + /// ``` + /// #![feature(dir_discovery)] + /// use std::fs::HomeDirs; + /// + /// # /* + /// let base_dir = /* ... */; + /// # */ let base_dir = std::path::PathBuf::from("/"); + /// let dirs = HomeDirs::empty() + /// .set_config_home(base_dir.join("config")) + /// .set_data_home(base_dir.join("data")) + /// .set_state_home(base_dir.join("state")) + /// .set_cache_home(base_dir.join("cache")) + /// .take(); + /// ``` + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn take(&mut self) -> Self { + mem::replace(self, Self::empty()) + } + + /// Set the path for [Self::cache_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn set_cache_home(&mut self, path: PathBuf) -> &mut Self { + self.cache = Some(path); + self + } + + /// Set the path for [Self::config_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn set_config_home(&mut self, path: PathBuf) -> &mut Self { + self.config = Some(path); + self + } + + /// Set the path for [Self::data_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn set_data_home(&mut self, path: PathBuf) -> &mut Self { + self.data = Some(path); + self + } + + /// Set the path for [Self::state_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn set_state_home(&mut self, path: PathBuf) -> &mut Self { + self.state = Some(path); + self + } +} + +impl MediaDirs { + /// Take the contents of this directory set, leaving it empty. + /// + /// This is useful with the builder `set_*` methods to build `MediaDirs` + /// without needing an intermediate mutable binding. + /// + /// # Examples + /// + /// ``` + /// #![feature(media_dir_discovery)] + /// use std::fs::MediaDirs; + /// + /// # /* + /// let base_dir = /* ... */; + /// # */ let base_dir = std::path::PathBuf::from("/"); + /// let dirs = MediaDirs::empty() + /// .set_desktop(base_dir.join("desktop")) + /// .set_documents(base_dir.join("documents")) + /// .set_downloads(base_dir.join("downloads")) + /// .set_music(base_dir.join("music")) + /// .set_pictures(base_dir.join("pictures")) + /// .set_videos(base_dir.join("videos")) + /// .take(); + /// ``` + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn take(&mut self) -> Self { + mem::replace(self, Self::empty()) + } + + /// Set the path for [Self::desktop]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_desktop(&mut self, path: PathBuf) -> &mut Self { + self.desktop = Some(path); + self + } + + /// Set the path for [Self::documents]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_documents(&mut self, path: PathBuf) -> &mut Self { + self.documents = Some(path); + self + } + + /// Set the path for [Self::downloads]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_downloads(&mut self, path: PathBuf) -> &mut Self { + self.downloads = Some(path); + self + } + + /// Set the path for [Self::music]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_music(&mut self, path: PathBuf) -> &mut Self { + self.music = Some(path); + self + } + + /// Set the path for [Self::pictures]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_pictures(&mut self, path: PathBuf) -> &mut Self { + self.pictures = Some(path); + self + } + + /// Set the path for [Self::videos]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_videos(&mut self, path: PathBuf) -> &mut Self { + self.videos = Some(path); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_home_dirs_field_hookup_matches() { + let mut dirs = HomeDirs::empty(); + + assert_eq!(dirs.config_home(), None); + assert_eq!(dirs.data_home(), None); + assert_eq!(dirs.state_home(), None); + assert_eq!(dirs.cache_home(), None); + + dirs.set_config_home("/config".into()); + dirs.set_data_home("/data".into()); + dirs.set_state_home("/state".into()); + dirs.set_cache_home("/cache".into()); + + assert_eq!(dirs.config_home(), Some("/config".as_ref())); + assert_eq!(dirs.data_home(), Some("/data".as_ref())); + assert_eq!(dirs.state_home(), Some("/state".as_ref())); + assert_eq!(dirs.cache_home(), Some("/cache".as_ref())); + } + + #[test] + fn test_media_dirs_field_hookup_matches() { + let mut dirs = MediaDirs::empty(); + + assert_eq!(dirs.desktop(), None); + assert_eq!(dirs.documents(), None); + assert_eq!(dirs.downloads(), None); + assert_eq!(dirs.music(), None); + assert_eq!(dirs.pictures(), None); + assert_eq!(dirs.videos(), None); + + dirs.set_desktop("/desktop".into()); + dirs.set_documents("/documents".into()); + dirs.set_downloads("/downloads".into()); + dirs.set_music("/music".into()); + dirs.set_pictures("/pictures".into()); + dirs.set_videos("/videos".into()); + + assert_eq!(dirs.desktop(), Some("/desktop".as_ref())); + assert_eq!(dirs.documents(), Some("/documents".as_ref())); + assert_eq!(dirs.downloads(), Some("/downloads".as_ref())); + assert_eq!(dirs.music(), Some("/music".as_ref())); + assert_eq!(dirs.pictures(), Some("/pictures".as_ref())); + assert_eq!(dirs.videos(), Some("/videos".as_ref())); + } +} diff --git a/library/std/src/os/darwin/fs.rs b/library/std/src/os/darwin/fs.rs index fc2869d6b13f6..a7a76ed80b7e1 100644 --- a/library/std/src/os/darwin/fs.rs +++ b/library/std/src/os/darwin/fs.rs @@ -7,6 +7,13 @@ use crate::fs::{self, Metadata}; use crate::sys::{AsInner, AsInnerMut, IntoInner}; use crate::time::SystemTime; +mod dirs; + +#[unstable(feature = "dir_discovery", issue = "157515")] +pub use self::dirs::HomeDirsExt; +#[unstable(feature = "media_dir_discovery", issue = "157515")] +pub use self::dirs::MediaDirsExt; + /// OS-specific extensions to [`fs::Metadata`]. /// /// [`fs::Metadata`]: crate::fs::Metadata diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs new file mode 100644 index 0000000000000..7ffd2e220176f --- /dev/null +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -0,0 +1,320 @@ +use crate::env; +use crate::fs::{HomeDirs, MediaDirs}; +use crate::io::{self, ErrorKind, const_error}; +use crate::path::PathBuf; + +trait Sealed {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} + +/// Darwin-specific extensions to [`fs::HomeDirs`](HomeDirs). +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait HomeDirsExt: Sized + Sealed { + /// Load the standard user directory paths for the current user. + /// + /// On iOS, tvOS, watchOS, visionOS, and sandboxed macOS applications, + /// these directories are within the application's sandbox bundle. Outside + /// the sandbox, these are subdirectories of the `~/Library` directory on + /// macOS. + /// + /// The produced directory paths are not guaranteed to be the canonical + /// paths to the directories; they are allowed to be sandbox-redirected + /// paths as long as the directory is accessible there. + /// + /// The loaded common directories are: + /// + /// | `HomeDirs` | [`NSSearchPathDirectory`] | + /// | ---------- | ----------------------- | + /// | [`cache_home`] | [`NSCachesDirectory`] (`~/Library/Caches`) | + /// | [`config_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`data_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`state_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// + /// Note that the Application Support directory is used for the config, + /// data, and state directories. It is always possible for multiple user + /// directories to be configured to the same path, but this is the common + /// configuration on Apple platforms, making it even more important to not + /// assume files in different user directories cannot alias each other. + /// + /// # Errors + /// + /// Errors if the the [user home](env::home_dir) cannot be determined. + // Errors due to the underlying sysdir(3) API should never occur, as + // - the user domain only has one directory for each search path; + // - the user domain always returns subdirectory paths of `~`; + // - the username plus OS defined path segments cannot exceed PATH_MAX; and + // - the username and OS defined path segments are always valid UTF-8. + /// + /// # Implementation-specific behavior + /// + /// Uses the `sysdir(3)` API from `libSystem` to discover the standard + /// user directories. + /// + /// This behavior may change in the future. One example change that we + /// explicitly reserve the right to make is to load additional common + /// directories not currently in this list. + /// + /// [`cache_home`]: HomeDirs::cache_home + /// [`config_home`]: HomeDirs::config_home + /// [`data_home`]: HomeDirs::data_home + /// [`state_home`]: HomeDirs::state_home + /// + /// [`NSSearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory?language=objc + /// [`NSCachesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory?language=objc + /// [`NSApplicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory?language=objc + #[unstable(feature = "dir_discovery", issue = "157515")] + fn sysdir() -> io::Result; +} + +/// Darwin-specific extensions to [`fs::MediaDirs`](MediaDirs). +#[unstable(feature = "media_dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait MediaDirsExt: Sized + Sealed { + /// Load the standard user directory paths for the current user. + /// + /// The produced directory paths are not guaranteed to be the canonical + /// paths to the directories; they are allowed to be sandbox-redirected + /// paths as long as the directory is accessible there. + /// + /// The loaded common directories are: + /// + /// | `MediaDirs` | [`NSSearchPathDirectory`] | + /// | ---------- | ----------------------- | + /// | [`desktop`] | [`NSDesktopDirectory`] (`~/Desktop`) | + /// | [`documents`] | [`NSDocumentDirectory`] (`~/Documents`) | + /// | [`downloads`] | [`NSDownloadsDirectory`] | + /// | [`music`] | [`NSMusicDirectory`] (`~/Music`) | + /// | [`pictures`] | [`NSPicturesDirectory`] (`~/Pictures`) | + /// | [`videos`] | [`NSMoviesDirectory`] (`~/Movies`) | + /// + /// # Errors + /// + /// Errors if the the [user home](env::home_dir) cannot be determined. + // Errors due to the underlying sysdir(3) API should never occur, as + // - the user domain only has one directory for each search path; + // - the user domain always returns subdirectory paths of `~`; + // - the username plus OS defined path segments cannot exceed PATH_MAX; and + // - the username and OS defined path segments are always valid UTF-8. + /// + /// # Implementation-specific behavior + /// + /// Uses the `sysdir(3)` API from `libSystem` to discover the standard + /// user directories. + /// + /// This behavior may change in the future. One example change that we + /// explicitly reserve the right to make is to load additional common + /// directories not currently in this list. + /// + /// [`desktop`]: MediaDirs::desktop + /// [`documents`]: MediaDirs::documents + /// [`downloads`]: MediaDirs::downloads + /// [`music`]: MediaDirs::music + /// [`pictures`]: MediaDirs::pictures + /// [`videos`]: MediaDirs::videos + /// + /// [`NSSearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory?language=objc + /// [`NSDesktopDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/desktopdirectory?language=objc + /// [`NSDocumentDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/documentdirectory?language=objc + /// [`NSDownloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory?language=objc + /// [`NSMusicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory?language=objc + /// [`NSPicturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory?language=objc + /// [`NSSharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory?language=objc + /// [`NSMoviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory?language=objc + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn sysdir() -> io::Result; +} + +fn user_home() -> io::Result { + env::home_dir() + .filter(|p| !p.is_empty()) + .ok_or(const_error!(ErrorKind::NotFound, "no home directory")) +} + +#[unstable(feature = "dir_discovery", issue = "157515")] +#[cfg(target_vendor = "apple")] +impl HomeDirsExt for HomeDirs { + fn sysdir() -> io::Result { + use libc::sysdir_search_path_directory_t::*; + + let mut dirs = HomeDirs::empty(); + let home = user_home()?; + + let caches = sys::get_user_dir(&home, SYSDIR_DIRECTORY_CACHES)?; + let application_support = sys::get_user_dir(&home, SYSDIR_DIRECTORY_APPLICATION_SUPPORT)?; + + dirs.cache = caches; + // Apple puts config/data/state all in Application Support + dirs.config = application_support.clone(); + dirs.data = application_support.clone(); + dirs.state = application_support; + + Ok(dirs) + } +} + +#[unstable(feature = "media_dir_discovery", issue = "157515")] +#[cfg(target_vendor = "apple")] +impl MediaDirsExt for MediaDirs { + fn sysdir() -> io::Result { + use libc::sysdir_search_path_directory_t::*; + + let mut dirs = MediaDirs::empty(); + let home = user_home()?; + + let desktop = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DESKTOP)?; + let documents = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DOCUMENT)?; + let downloads = sys::get_user_dir(&home, SYSDIR_DIRECTORY_DOWNLOADS)?; + let movies = sys::get_user_dir(&home, SYSDIR_DIRECTORY_MOVIES)?; + let music = sys::get_user_dir(&home, SYSDIR_DIRECTORY_MUSIC)?; + let pictures = sys::get_user_dir(&home, SYSDIR_DIRECTORY_PICTURES)?; + + dirs.desktop = desktop; + dirs.documents = documents; + dirs.downloads = downloads; + dirs.music = music; + dirs.pictures = pictures; + dirs.videos = movies; + + Ok(dirs) + } +} + +/// Safer wrapper around the sysdir(3) API +#[cfg(target_vendor = "apple")] +mod sys { + use crate::ffi::{CStr, c_char}; + use crate::io::{self, ErrorKind, const_error}; + use crate::path::{Path, PathBuf}; + + /// Get the path for a system directory using `sysdir(3)`. + pub fn get_user_dir( + home: &Path, + kind: libc::sysdir_search_path_directory_t, + ) -> io::Result> { + use libc::sysdir_search_path_domain_mask_t::SYSDIR_DOMAIN_MASK_USER; + + // SAFETY: SYSDIR_DOMAIN_MASK_USER < SYSDIR_DOMAIN_MASK_ALL + let mut iter = unsafe { Iter::new(home, kind, SYSDIR_DOMAIN_MASK_USER) }; + let Some(path) = iter.next() else { + return Ok(None); + }; + + if iter.next().is_some() { + // more than one path returned (shouldn't happen for SYSDIR_DOMAIN_MASK_USER) + return Err(const_error!( + ErrorKind::InvalidData, + "multiple paths returned for standard user directory", + )); + } + + Ok(Some(path?)) + } + + struct Iter<'a> { + home: &'a Path, + state: libc::sysdir_search_path_enumeration_state, + } + + impl Drop for Iter<'_> { + fn drop(&mut self) { + for _ in self {} + } + } + + impl<'a> Iter<'a> { + // SAFETY: `mask` must be <= `SYSDIR_DOMAIN_MASK_ALL` + pub unsafe fn new( + home: &'a Path, + kind: libc::sysdir_search_path_directory_t, + mask: libc::sysdir_search_path_domain_mask_t, + ) -> Self { + // SAFETY: forwarded to the caller + let state = unsafe { libc::sysdir_start_search_path_enumeration(kind, mask) }; + Self { home, state } + } + } + + impl Iterator for Iter<'_> { + type Item = io::Result; + + fn next(&mut self) -> Option> { + let mut buf = [0u8; libc::PATH_MAX as usize]; + if self.state != 0 { + // SAFETY: `self.state` is nonzero and comes from prior sysdir_{start|get_next}_search_path_enumeration call + // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX` bytes to `path` + self.state = unsafe { + libc::sysdir_get_next_search_path_enumeration( + self.state, + buf.as_mut_ptr() as *mut c_char, + ) + }; + } + + if self.state == 0 { + // exhausted + return None; + } + + let Ok(path) = CStr::from_bytes_until_nul(&buf) else { + // should be impossible given username length limits, but be defensive + return Some(Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path too long", + ))); + }; + + let Ok(path) = path.to_str() else { + // should be impossible on a working system, but be defensive + return Some(Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path not valid UTF-8", + ))); + }; + + // expand `~` shorthand + Some(match path { + "~" => Ok(self.home.into()), + _ if path.starts_with("~/") => Ok(self.home.join(&path[2..])), + _ if path.starts_with("~") => Err(const_error!( + ErrorKind::InvalidData, + "standard user directory relative to different user", + )), + _ => { + let path = PathBuf::from(path); + if path.is_relative() { + Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path not absolute", + )) + } else { + Ok(path) + } + } + }) + } + } +} + +#[cfg(test)] +#[cfg(target_vendor = "apple")] +mod tests { + use super::*; + + #[test] + fn can_fetch_sysdir_paths() { + let dirs = HomeDirs::sysdir().unwrap(); + assert!(dirs.cache_home().is_some()); + assert!(dirs.config_home().is_some()); + assert!(dirs.data_home().is_some()); + assert!(dirs.state_home().is_some()); + + let dirs = MediaDirs::sysdir().unwrap(); + assert!(dirs.desktop().is_some()); + assert!(dirs.documents().is_some()); + assert!(dirs.downloads().is_some()); + assert!(dirs.music().is_some()); + assert!(dirs.pictures().is_some()); + assert!(dirs.videos().is_some()); + } +} diff --git a/library/std/src/os/unix/fs.rs b/library/std/src/os/unix/fs.rs index c119912c3b022..cf0176bbbe759 100644 --- a/library/std/src/os/unix/fs.rs +++ b/library/std/src/os/unix/fs.rs @@ -21,6 +21,13 @@ use crate::{io, sys}; #[cfg(test)] mod tests; +mod dirs; + +#[unstable(feature = "dir_discovery", issue = "157515")] +pub use self::dirs::HomeDirsExt; +#[unstable(feature = "media_dir_discovery", issue = "157515")] +pub use self::dirs::MediaDirsExt; + /// Unix-specific extensions to [`fs::File`]. #[stable(feature = "file_offset", since = "1.15.0")] pub trait FileExt { diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs new file mode 100644 index 0000000000000..57da2f98ea8f4 --- /dev/null +++ b/library/std/src/os/unix/fs/dirs.rs @@ -0,0 +1,415 @@ +use crate::borrow::Cow; +use crate::env::{self, JoinPathsError, SplitPaths, join_paths, split_paths, var_os}; +use crate::ffi::{OsStr, OsString}; +use crate::fs::{self, HomeDirs, MediaDirs}; +use crate::io::{self, ErrorKind, const_error}; +use crate::os::unix::ffi::{OsStrExt, OsStringExt}; +use crate::path::{Path, PathBuf}; + +trait Sealed {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} + +/// XDG-specific extensions to [`fs::HomeDirs`](HomeDirs). +/// +/// The XDG conventions are defined by the Freedesktop.org project in the +/// [XDG Base Directory Specification][xdg-basedir]. These conventions have +/// been largely adopted by Linux distributions. +/// +/// The XDG conventions are written to be usable on any Unix-like filesystem, +/// thus this extension being provided in `os::unix` rather than `os::linux`. +/// However, while some tooling does use XDG conventions on macOS, note that +/// macOS has its own separate conventions for user directories. Consider +/// carefully what conventions your users will expect your application to +/// follow along with any legacy path compatibility you might need to support. +/// +/// [xdg-basedir]: https://specifications.freedesktop.org/basedir/ +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait HomeDirsExt: Sized + Sealed { + /// Load the user directory paths according to the + /// [XDG Base Directory Specification][xdg-basedir]. + /// + /// Each base directory path is set to the value of its corresponding + /// `XDG_*` environment variable (if it is set and non-empty), else to + /// the default value defined by the specification. + /// + /// | Field | Environment Variable | Default Value | + /// | ----- | -------------------- | ------------- | + /// | [`cache_home`] | `XDG_CACHE_HOME` | `$HOME/.cache` | + /// | [`config_home`] | `XDG_CONFIG_HOME` | `$HOME/.config` | + /// | [`data_home`] | `XDG_DATA_HOME` | `$HOME/.local/share` | + /// | [`state_home`] | `XDG_STATE_HOME` | `$HOME/.local/state` | + /// | [`runtime_home`] | `XDG_RUNTIME_DIR` | (see method docs) | + /// | [`config_dirs`] | `XDG_CONFIG_DIRS` | `/etc/xdg` | + /// | [`data_dirs`] | `XDG_DATA_DIRS` | `/usr/local/share/`, `/usr/share/` | + /// + /// Note that `$HOME` here means [`env::home_dir`], which uses + /// `$HOME` if set and non-empty, but falls back to the system password + /// database if it isn't set. A correctly configured XDG system will have + /// `$HOME` set, but this fallback matches that of the shell. + /// + /// # Errors + /// + /// Errors if the user's home directory cannot be determined. + /// + /// [xdg-basedir]: https://specifications.freedesktop.org/basedir/ + /// [`cache_home`]: HomeDirs::cache_home + /// [`config_home`]: HomeDirs::config_home + /// [`data_home`]: HomeDirs::data_home + /// [`state_home`]: HomeDirs::state_home + /// [`runtime_home`]: HomeDirsExt::runtime_home + /// [`config_dirs`]: HomeDirsExt::config_dirs + /// [`data_dirs`]: HomeDirsExt::data_dirs + #[unstable(feature = "dir_discovery", issue = "157515")] + fn xdg() -> io::Result; + + /// A base directory relative to which user-specific runtime files + /// (such as sockets, named pipes, etc) should be stored. + /// + /// Files in this directory may be subjected to periodic clean-up. + /// Larger files should not be placed here, since it might reside in + /// runtime memory and cannot necessarily be swapped out to disk. + /// + /// This path does not have a default if not set. If it isn't set, + /// applications should fall back to a replacement directory with + /// similar capabilities and print a warning message. + #[unstable(feature = "dir_discovery", issue = "157515")] + fn runtime_home(&self) -> Option<&Path>; + + /// A preference-ordered list of base directories to search for config + /// files *in addition to* [`config_home`]. + /// + /// The order of directories denotes their importance; the first directory + /// is the most important. Information defined relative to the more + /// important base directory takes precedent. [`config_home`] is not + /// necessarily present in this list, and is considered more important + /// than any base directory in this list. + /// + /// [`config_home`]: HomeDirs::config_home + #[unstable(feature = "dir_search_discovery", issue = "157515")] + fn config_dirs(&self) -> Option>; + + /// A preference-ordered list of base directories to search for data + /// files *in addition to* [`data_home`]. + /// + /// The order of directories denotes their importance; the first directory + /// is the most important. Information defined relative to the more + /// important base directory takes precedent. [`data_home`] is not + /// necessarily present in this list, and is considered more important + /// than any base directory in this list. + /// + /// [`data_home`]: HomeDirs::data_home + #[unstable(feature = "dir_search_discovery", issue = "157515")] + fn data_dirs(&self) -> Option>; + + /// Set the paths for [Self::runtime_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self; + + /// Set the paths for [Self::config_dirs]. + #[unstable(feature = "dir_search_discovery", issue = "157515")] + fn set_config_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError>; + + /// Set the paths for [Self::data_dirs]. + #[unstable(feature = "dir_search_discovery", issue = "157515")] + fn set_data_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError>; +} + +/// XDG-specific extensions to [`fs::MediaDirs`](MediaDirs). +/// +/// The XDG conventions are defined by the Freedesktop.org project in the +/// [xdg-user-dirs]. This configuration is generally present on desktop Linux +/// distributions, although adoption is less widespread than the base directory +/// specification. +/// +/// The XDG conventions are written to be usable on any Unix-like filesystem, +/// thus this extension being provided in `os::unix` rather than `os::linux`. +/// However, while some tooling does use XDG conventions on macOS, note that +/// macOS has its own separate conventions for user directories. Consider +/// carefully what conventions your users will expect your application to +/// follow along with any legacy path compatibility you might need to support. +/// +/// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ +#[unstable(feature = "media_dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait MediaDirsExt: Sized + Sealed { + /// Load the user directory paths according to the [xdg-user-dirs] tool. + /// + /// This directly reads and parses the `$XDG_CONFIG_HOME/user-dirs.dirs` + /// file as defined and maintained by the [xdg-user-dirs] tool. + /// + /// # Errors + /// + /// Errors if the user's home directory cannot be determined or if the + /// `$XDG_CONFIG_HOME/user-dirs.dirs` file cannot be read. + /// + /// # Implementation-specific behavior + /// + /// Only the format maintained by xdg-user-dirs-update is supported. Any + /// configuration that does not match the expected format will result in + /// loading an unspecified path or `None` for that directory. To be more + /// specific: + /// + /// - Any line not in the format of `XDG_{NAME}_DIR={path}` where `{NAME}` + /// is one of `DESKTOP`, `DOWNLOAD`, `TEMPLATES`, `PUBLICSHARE`, + /// `DOCUMENTS`, `MUSIC`, `PICTURES`, or `VIDEOS` is ignored. + /// - `{path}` must be a `"`-quoted shell-escaped path. + /// - `{path}` may only start with `/` or `$HOME/`. A home-relative path is + /// returned relative to [`env::home_dir`]; shell expansion is + /// not performed. + /// - A directory set to just `$HOME` without a subdirectory is treated as + /// unsetting the directory, and results in a `None` value for that path. + /// - When shell expansion syntax other than a leading `$HOME` is present, + /// no shell invocations will be done, but the produced directory path is + /// otherwise unspecified. + /// + /// This behavior may change in the future. One example change that we + /// explicitly reserve the right to make is to load paths in a more + /// permissive manner, such as supporting more shell expansion syntax + /// that xdg-user-dirs officially forbids putting in `user-dirs.dirs` + /// but has de-facto support when `source`ing the file in a shell. + /// + /// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn xdg() -> io::Result; + + /// The OS-privileged user "Templates" directory, often the `Templates` + /// folder in the user's home directory. + /// + /// As a media directory, this should typically be used as a default path + /// for file selection dialogs, not for automatically accessed file paths. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn templates(&self) -> Option<&Path>; + + /// Set the paths for [Self::templates]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn set_templates(&mut self, path: PathBuf) -> &mut Self; +} + +#[unstable(feature = "dir_discovery", issue = "157515")] +impl HomeDirsExt for HomeDirs { + fn xdg() -> io::Result { + let mut dirs = HomeDirs::empty(); + let user_home = user_home()?; + + dirs.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); + dirs.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); + dirs.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); + dirs.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); + dirs.extra.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); + + dirs.extra.config_path = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); + dirs.extra.data_path = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); + + Ok(dirs) + } + + fn runtime_home(&self) -> Option<&Path> { + self.extra.runtime.as_deref() + } + + fn config_dirs(&self) -> Option> { + self.extra.config_path.as_ref().map(|s| split_paths(s)) + } + + fn data_dirs(&self) -> Option> { + self.extra.data_path.as_ref().map(|s| split_paths(s)) + } + + fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self { + self.extra.runtime = Some(path); + self + } + + fn set_config_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError> { + self.extra.config_path = Some(join_paths(paths)?); + Ok(self) + } + + fn set_data_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError> { + self.extra.data_path = Some(join_paths(paths)?); + Ok(self) + } +} + +#[unstable(feature = "media_dir_discovery", issue = "157515")] +impl MediaDirsExt for MediaDirs { + fn xdg() -> io::Result { + let mut dirs = MediaDirs::empty(); + let user_home = user_home()?; + let config_home = xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config")); + + let spec = match fs::read(config_home.join("user-dirs.dirs")) { + Ok(spec) => spec, + Err(e) if e.kind() == ErrorKind::NotFound => { + return Err(const_error!( + ErrorKind::NotFound, + "missing `$XDG_CONFIG_HOME/user-dirs.dirs`", + )); + } + Err(e) => return Err(e), + }; + + for line in spec.split(|&b| b == b'\n') { + // trim leading whitespace + let trimmed = line.trim_ascii_start(); + // skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with(&[b'#']) { + continue; + } + + // only variable assignment lines are allowed; split on `=` + let mut split = trimmed.splitn(2, |&b| b == b'='); + // extract assignment parts; ignore lines not in this format + let Some(var) = split.next() else { continue }; + let Some(val) = split.next() else { continue }; + debug_assert_eq!(split.next(), None); + + // ensure the path is absolute or $HOME-relative + if !val.starts_with(b"\"/") && !val.starts_with(b"\"$HOME/") { + continue; + } + + // the path value is quoted; unquote it + let Some(path) = xdg_unquote(val) else { continue }; + + // expand the $HOME prefix if present + let path = if path.starts_with(b"$HOME/") { + user_home.join(OsStr::from_bytes(&path[6..])) + } else { + PathBuf::from(OsString::from_vec(path.into_owned())) + }; + + // load the known user directories + match var { + b"XDG_DESKTOP_DIR" => dirs.desktop = Some(path), + b"XDG_DOCUMENTS_DIR" => dirs.documents = Some(path), + b"XDG_DOWNLOAD_DIR" => dirs.downloads = Some(path), + b"XDG_MUSIC_DIR" => dirs.music = Some(path), + b"XDG_PICTURES_DIR" => dirs.pictures = Some(path), + b"XDG_VIDEOS_DIR" => dirs.videos = Some(path), + b"XDG_TEMPLATES_DIR" => dirs.extra.templates = Some(path), + _ => { + // ignore unknown variable assignment + } + } + } + + Ok(dirs) + } + + fn templates(&self) -> Option<&Path> { + self.extra.templates.as_deref() + } + + fn set_templates(&mut self, path: PathBuf) -> &mut Self { + self.extra.templates = Some(path); + self + } +} + +fn user_home() -> io::Result { + env::home_dir() + .filter(|p| !p.is_empty()) + .ok_or(const_error!(ErrorKind::NotFound, "no home directory")) +} + +fn xdg_dir(env: &str, fallback: impl FnOnce() -> PathBuf) -> PathBuf { + var_os(env).filter(|s| !s.is_empty()).map(PathBuf::from).unwrap_or_else(fallback) +} + +fn xdg_env(env: &str, fallback: &str) -> OsString { + var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| OsString::from(fallback)) +} + +fn xdg_unquote(bytes: &[u8]) -> Option> { + let [b'"', bytes @ .., b'"'] = bytes else { return None }; + + if !bytes.contains(&b'\\') { + return Some(Cow::Borrowed(bytes)); + } + + let mut rest = bytes; + let mut s = Vec::with_capacity(rest.len()); + loop { + let i = rest + .iter() + .position(|&b| matches!(b, b'"' | b'\\' | b'$' | b'`')) + .unwrap_or(rest.len()); + s.extend_from_slice(&rest[..i]); + match &rest[i..] { + [] => break, + [b'\\', c @ (b'"' | b'\\' | b'$' | b'`'), tail @ ..] => { + s.push(*c); + rest = tail; + } + [b'\\', b'\n', tail @ ..] => { + // line continuation + rest = tail; + } + [b'\\', tail @ ..] => { + s.push(b'\\'); + rest = tail; + } + [b'"' | b'$' | b'`', ..] => { + // unsupported shell syntax + return None; + } + _ => unreachable!(), + } + } + + Some(Cow::Owned(s)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_fetch_xdg_base_dirs() { + let dirs = HomeDirs::xdg().unwrap(); + + assert!(dirs.cache_home().is_some()); + assert!(dirs.config_home().is_some()); + assert!(dirs.data_home().is_some()); + assert!(dirs.state_home().is_some()); + // dirs.runtime() may not exist + assert!(dirs.config_dirs().is_some()); + assert!(dirs.data_dirs().is_some()); + } + + #[test] + #[cfg_attr(target_vendor = "apple", ignore = "Apple OSes don't use xdg-user-dirs")] + fn can_fetch_xdg_media_dirs() { + let dirs = match MediaDirs::xdg() { + Ok(dirs) => dirs, + Err(e) if e.kind() == ErrorKind::NotFound => { + // xdg-user-dirs not initialized on this system, skip the test + return; + } + Err(e) => panic!("failed to fetch xdg user dirs: {e:?}"), + }; + + assert!(dirs.desktop().is_some()); + assert!(dirs.documents().is_some()); + assert!(dirs.downloads().is_some()); + assert!(dirs.music().is_some()); + assert!(dirs.pictures().is_some()); + assert!(dirs.videos().is_some()); + assert!(dirs.templates().is_some()); + } +} diff --git a/library/std/src/os/unix/mod.rs b/library/std/src/os/unix/mod.rs index e3f15a00a0944..78c957270c451 100644 --- a/library/std/src/os/unix/mod.rs +++ b/library/std/src/os/unix/mod.rs @@ -94,7 +94,6 @@ pub mod net; pub mod process; pub mod raw; pub mod thread; -pub mod xdg; /// A prelude for conveniently writing platform-specific code. /// diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs deleted file mode 100644 index b51b8ac916c24..0000000000000 --- a/library/std/src/os/unix/xdg.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! XDG (X Desktop Group) related functionality for Unix platforms. -//! -//! The [XDG Base Directory Specification][basedir] defines a set of base -//! directories, relative to which user-specific files should be looked for. The -//! functions in this module provide those directory paths as configured by -//! the environment. -//! -//! Note that the use of these functions is not enforced by the system, and as -//! such, not all programs will necessarily respect all details of the XDG path -//! environment. This is a set of guidelines, and each program is ultimately -//! responsible for defining where and how it both reads and writes files. -//! -//! Use of XDG paths can be generally considered the conventional expectation -//! on Linux-based systems. Other Unix-based systems may or may not play well -//! with the XDG conventions. -//! -//! Directories returned by this module are not guaranteed to exist yet. If the -//! directory does not exist, an application should attempt to create it with -//! [permissions mode][super::fs::PermissionsExt::from_mode] `0o700`. -//! -//! [basedir]: https://specifications.freedesktop.org/basedir/latest/ -#![unstable(feature = "xdg_basedir", issue = "157515")] - -use crate::env::{home_dir, split_paths, var_os}; -use crate::ffi::{OsStr, OsString}; -use crate::path::PathBuf; - -fn xdg_home_dir() -> PathBuf { - // Note: home_dir can return `Some("")` in some cases. We assume that in - // this case the expected behavior is for `$HOME/path` to become `/path`, - // i.e. the home directory is effectively `/`. - match home_dir() { - None => panic!("an XDG environment should have a home directory"), - Some(home) if home.is_empty() => PathBuf::from("/"), - Some(home) => home, - } -} - -fn xdg_dir(env: &str, fallback_home_subdir: &str) -> PathBuf { - var_os(env) - .filter(|s| !s.is_empty()) - .map(PathBuf::from) - .unwrap_or_else(|| xdg_home_dir().join(fallback_home_subdir)) -} - -/// A base directory relative to which user-specific data files should be written. -/// -/// An application `appid` would typically be expected to write its data files -/// to `{data_home_dir}/{appid}/**/*`. -pub fn data_home_dir() -> PathBuf { - xdg_dir("XDG_DATA_HOME", ".local/share") -} - -/// A base directory relative to which user-specific configuration files should be written. -/// -/// An application `appid` would typically be expected to write its configuration -/// files to `{config_home_dir}/{appid}/**/*`. -pub fn config_home_dir() -> PathBuf { - xdg_dir("XDG_CONFIG_HOME", ".config") -} - -/// A base directory relative to which user-specific state data should be written. -/// -/// An application `appid` would typically be expected to write its state data to -/// `{state_home_dir}/{appid}/**/*`. -/// -/// Common kinds of state data include actions history (such as logs, history, -/// recently used files, etc.) and state of the application that can be reused -/// after application restart (such as view, layout, open files, undo history, -/// etc.). -pub fn state_home_dir() -> PathBuf { - xdg_dir("XDG_STATE_HOME", ".local/state") -} - -/// A base directory relative to which user-specific non-essential (cached) data should be written. -/// -/// An application `appid` would typically be expected to write its cache data to -/// `{cache_home_dir}/{appid}/**/*`. -pub fn cache_home_dir() -> PathBuf { - xdg_dir("XDG_CACHE_HOME", ".cache") -} - -/// An iterator that produces directory paths from XDG environment configuration. -/// -/// The iterator element type is [`PathBuf`]. -/// -/// This structure is created by [`xdg::data_dirs`] and [`xdg::config_dirs`]. -/// See the documentation of those functions for more. -/// -/// [`xdg::data_dirs`]: data_dirs -/// [`xdg::config_dirs`]: config_dirs -#[derive(Debug, Clone)] -pub struct XdgDirsIter { - list: OsString, - off: usize, -} - -impl XdgDirsIter { - fn new(env: &str, default: &str) -> Self { - let dirs = var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| default.into()); - Self { list: dirs, off: 0 } - } - - fn remaining(&self) -> Option<&OsStr> { - self.list.as_encoded_bytes().get(self.off..).map(|bytes| { - // SAFETY: `self.off` is the index after a path separator (or the - // start of the string), so is a valid OsStr boundary. - unsafe { OsStr::from_encoded_bytes_unchecked(bytes) } - }) - } -} - -impl Iterator for XdgDirsIter { - type Item = PathBuf; - - fn next(&mut self) -> Option { - let rest = self.remaining()?; - let next = split_paths(rest).next()?; - let len = next.as_os_str().len(); - self.off += len + 1; // Offset after this path and the separator after it. - Some(next) - } - - fn size_hint(&self) -> (usize, Option) { - let Some(dirs) = self.remaining() else { return (0, Some(0)) }; - split_paths(dirs).size_hint() - } -} - -/// A set of preference ordered directories relative to which data files should be searched. -/// -/// If an application defines a data file to be at `$XDG_DATA_DIRS/appid/file.name`, this means that: -/// -/// - The initial data file should be installed to `{system_data_dir}/appid/file.name`. -/// - A user-specific version of the data file may be created at -/// {[data_home_dir][]()}/appid/file.name. -/// - Lookups for the data file should search for `./appid/file.name` relative to -/// `data_home_dir` and each directory in `data_dirs`, giving preference to -/// files found relative to an earlier directory in the search order. -/// -/// An application may choose to handle a file being located under multiple base -/// directories however it sees fit, so long as it respects the search order. -/// For example, it could say that only the first file found is used, or that -/// data within the files is merged in some way. -pub fn data_dirs() -> XdgDirsIter { - // NB: the spec uses trailing slashes only for this default, for some reason - XdgDirsIter::new("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/") -} - -/// A set of preference ordered directories relative to which configuration files should be searched. -/// -/// If an application defines a configuration file to be at `$XDG_CONFIG_DIRS/appid/file.name`, this means that: -/// -/// - The initial configuration file should be installed to `{system_config_dir}/xdg/appid/file.name`. -/// - A user-specific version of the configuration file may be created at -/// {[config_home_dir][]()}/appid/file.name. -/// - Lookups for the configuration file should search for `./appid/file.name` -/// relative to `config_home_dir` and each directory in `config_dirs`, giving -/// preference to files found relative to an earlier directory in the search order. -/// -/// An application may choose to handle a file being located under multiple base -/// directories however it sees fit, so long as it respects the search order. -/// For example, it could say that only the first file found is used, or that -/// data within the files is merged in some way. -pub fn config_dirs() -> XdgDirsIter { - XdgDirsIter::new("XDG_CONFIG_DIRS", "/etc/xdg") -} diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index dfa9236a7e428..afae587bac5e8 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -11,6 +11,13 @@ use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner}; use crate::time::SystemTime; use crate::{io, sys}; +mod dirs; + +#[unstable(feature = "dir_discovery", issue = "157515")] +pub use self::dirs::HomeDirsExt; +#[unstable(feature = "media_dir_discovery", issue = "157515")] +pub use self::dirs::MediaDirsExt; + /// Windows-specific extensions to [`fs::File`]. #[stable(feature = "file_offset", since = "1.15.0")] pub trait FileExt { diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs new file mode 100644 index 0000000000000..b1d611e84ebb4 --- /dev/null +++ b/library/std/src/os/windows/fs/dirs.rs @@ -0,0 +1,243 @@ +use crate::fs::{HomeDirs, MediaDirs}; +use crate::io; + +trait Sealed {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} + +/// Windows-specific extensions to [`fs::HomeDirs`](HomeDirs). +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait HomeDirsExt: Sized + Sealed { + /// Load the known user folder paths using the [Known Folders] API. + /// + /// The loaded known folders are: + /// + /// | `HomeDirs` | [`KNOWNFOLDERID`] | + /// | ---------- | ----------------- | + /// | [`cache_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) | + /// | [`config_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) | + /// | [`data_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) | + /// | [`state_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) | + /// + /// Note that caches/state are both put in LocalAppData, and config/data + /// in RoamingAppData. It is always possible for multiple user directories + /// to be configured to the same path, but this is the common configuration + /// on Windows platforms, making it even more important to not assume files + /// in different user directories cannot alias each other. + /// + /// # Errors + /// + /// Errors if the underlying system discovery API returns an error. + /// The lack of a configured path is not considered an error and results + /// in a `None` value for that path in the returned `HomeDirs`. + /// + /// # Implementation-specific behavior + /// + /// Calls [`SHGetKnownFolderPath`] for the current user once for each known + /// folder. Does not create the folder if missing. + /// + /// This behavior may change in the future. One example change that we + /// explicitly reserve the right to make is to load additional common + /// directories not currently in this list. + /// + /// [Known Folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders + /// [`SHGetKnownFolderPath`]: https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + /// [`KNOWNFOLDERID`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid + /// [`FOLDERID_LocalAppData`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_localappdata + /// [`FOLDERID_RoamingAppData`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_roamingappdata + /// [`cache_home`]: HomeDirs::cache_home + /// [`config_home`]: HomeDirs::config_home + /// [`data_home`]: HomeDirs::data_home + /// [`state_home`]: HomeDirs::state_home + #[unstable(feature = "dir_discovery", issue = "157515")] + fn known_folders() -> io::Result; +} + +/// Windows-specific extensions to [`fs::MediaDirs`](MediaDirs). +#[unstable(feature = "media_dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait MediaDirsExt: Sized + Sealed { + /// Load the known user folder paths using the [Known Folders] API. + /// + /// The loaded known folders are: + /// + /// | `MediaDirs` | [`KNOWNFOLDERID`] | + /// | ---------- | ----------------- | + /// | [`desktop`] | [`FOLDERID_Desktop`] (`%USERPROFILE%\Desktop`) | + /// | [`documents`] | [`FOLDERID_Documents`] (`%USERPROFILE%\Documents`) | + /// | [`downloads`] | [`FOLDERID_Downloads`] (`%USERPROFILE%\Downloads`) | + /// | [`music`] | [`FOLDERID_Music`] (`%USERPROFILE%\Music`) | + /// | [`pictures`] | [`FOLDERID_Pictures`] (`%USERPROFILE%\Pictures`) | + /// | [`videos`] | [`FOLDERID_Videos`] (`%USERPROFILE%\Videos`) | + /// + /// # Errors + /// + /// Errors if the underlying system discovery API returns an error. + /// The lack of a configured path is not considered an error and results + /// in a `None` value for that path in the returned `MediaDirs`. + /// + /// # Implementation-specific behavior + /// + /// Calls [`SHGetKnownFolderPath`] for the current user once for each known + /// folder. Does not create the folder if missing. + /// + /// This behavior may change in the future. One example change that we + /// explicitly reserve the right to make is to load additional common + /// directories not currently in this list. + /// + /// [Known Folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders + /// [`SHGetKnownFolderPath`]: https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + /// [`KNOWNFOLDERID`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid + /// [`FOLDERID_Desktop`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_desktop + /// [`FOLDERID_Documents`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_documents + /// [`FOLDERID_Downloads`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_downloads + /// [`FOLDERID_Music`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_music + /// [`FOLDERID_Pictures`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_pictures + /// [`FOLDERID_Videos`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos + /// [`desktop`]: MediaDirs::desktop + /// [`documents`]: MediaDirs::documents + /// [`downloads`]: MediaDirs::downloads + /// [`music`]: MediaDirs::music + /// [`pictures`]: MediaDirs::pictures + /// [`videos`]: MediaDirs::videos + #[unstable(feature = "dir_discovery", issue = "157515")] + fn known_folders() -> io::Result; +} + +#[cfg(windows)] +#[unstable(feature = "dir_discovery", issue = "157515")] +impl HomeDirsExt for HomeDirs { + fn known_folders() -> io::Result { + use crate::sys::c; + + let local_app_data = sys::get_known_folder_path(&c::FOLDERID_LocalAppData)?; + let roaming_app_data = sys::get_known_folder_path(&c::FOLDERID_RoamingAppData)?; + + // AppData/Local -- system-local, doesn't make sense to sync to another + // AppData/Roaming -- data that makes sense to sync across machines + + let mut dirs = HomeDirs::empty(); + + dirs.cache = local_app_data.clone(); + dirs.config = roaming_app_data.clone(); + dirs.data = roaming_app_data; + dirs.state = local_app_data; + + Ok(dirs) + } +} + +#[cfg(windows)] +#[unstable(feature = "dir_discovery", issue = "157515")] +impl MediaDirsExt for MediaDirs { + fn known_folders() -> io::Result { + use crate::sys::c; + + let desktop = sys::get_known_folder_path(&c::FOLDERID_Desktop)?; + let documents = sys::get_known_folder_path(&c::FOLDERID_Documents)?; + let downloads = sys::get_known_folder_path(&c::FOLDERID_Downloads)?; + let music = sys::get_known_folder_path(&c::FOLDERID_Music)?; + let pictures = sys::get_known_folder_path(&c::FOLDERID_Pictures)?; + let videos = sys::get_known_folder_path(&c::FOLDERID_Videos)?; + + // AppData/Local -- system-local, doesn't make sense to sync to another + // AppData/Roaming -- data that makes sense to sync across machines + + let mut dirs = MediaDirs::empty(); + + dirs.desktop = desktop; + dirs.documents = documents; + dirs.downloads = downloads; + dirs.music = music; + dirs.pictures = pictures; + dirs.videos = videos; + + Ok(dirs) + } +} + +#[cfg(windows)] +mod sys { + use crate::io::{self, ErrorKind, const_error}; + use crate::path::PathBuf; + use crate::sys::{c, os2path}; + use crate::{ptr, slice}; + + /// Retrieve a known folder path from the Windows API. + pub fn get_known_folder_path(id: &c::GUID) -> io::Result> { + // Get the known folder path. hToken = NULL requests the current user + // scope, and we set KF_FLAG_DONT_VERIFY because it's a bit faster and + // we don't guarantee that the directories at the paths exist. + let mut pszPath = ptr::null_mut(); + // SAFETY: rfid/ppszPath are valid pointers, flags are appropriate, and + // a NULL hToken is supported by SHGetKnownFolderPath. + let hr = unsafe { + c::SHGetKnownFolderPath( + /* rfid */ id, + /* dwFlags */ c::KF_FLAG_DONT_VERIFY as _, + /* hToken */ ptr::null_mut(), + /* ppszPath */ &mut pszPath, + ) + }; + + let result = match hr { + c::S_OK => { + // SAFETY: pszPath was populated by a successful call to SHGetKnownFolderPath + // and is valid up to and including its nul terminator + let len = unsafe { c::lstrlenW(pszPath) }; + // SAFETY: *pszPath is valid up to and including its nul terminator + Ok(Some(os2path(unsafe { slice::from_raw_parts(pszPath, len as usize) }))) + } + c::E_FAIL => { + // This known folder id exists but does not have a path + if cfg!(debug_assertions) { + unreachable!("should not call get_known_folder_path on a virtual folder"); + } else { + Err(const_error!( + ErrorKind::InvalidInput, + "virtual known folders do not have paths" + )) + } + } + c::E_INVALIDARG => { + // This known folder id is not present on the system + Ok(None) + } + _ => { + // Miscellaneous error + Err(io::Error::from_raw_os_error(hr)) + } + }; + + // SAFETY: The caller is responsible for freeing the path returned by + // SHGetKnownFolderPath by calling CoTaskMemFree, whether it + // succeeds or not. + unsafe { c::CoTaskMemFree(pszPath.cast()) }; + + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_fetch_known_folder_paths() { + let dirs = HomeDirs::known_folders().unwrap(); + assert!(dirs.user_home().is_some()); + assert!(dirs.cache_home().is_some()); + assert!(dirs.config_home().is_some()); + assert!(dirs.data_home().is_some()); + + let dirs = MediaDirs::known_folders().unwrap(); + assert!(dirs.state_home().is_some()); + assert!(dirs.desktop().is_some()); + assert!(dirs.documents().is_some()); + assert!(dirs.downloads().is_some()); + assert!(dirs.music().is_some()); + assert!(dirs.pictures().is_some()); + assert!(dirs.videos().is_some()); + } +} diff --git a/library/std/src/sys/fs/common.rs b/library/std/src/sys/fs/common.rs index 0df91d56b8a19..6f7da4f0b2215 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -84,3 +84,6 @@ impl fmt::Debug for Dir { f.debug_struct("Dir").field("path", &self.path).finish() } } + +pub type ExtraHomeDirs = (); +pub type ExtraMediaDirs = (); diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index 0c297c5766b82..03341116d3e84 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -10,7 +10,7 @@ cfg_select! { mod unix; use unix as imp; #[cfg(not(target_os = "wasi"))] - pub use unix::{chown, fchown, lchown, mkfifo}; + pub use unix::{chown, fchown, lchown, mkfifo, ExtraHomeDirs, ExtraMediaDirs}; #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] pub use unix::chroot; #[cfg(not(target_os = "wasi"))] @@ -63,6 +63,9 @@ pub use imp::{ ReadDir, }; +#[cfg(not(target_family = "unix"))] +pub use self::common::{ExtraHomeDirs, ExtraMediaDirs}; + pub fn read_dir(path: &Path) -> io::Result { // FIXME: use with_native_path on all platforms imp::readdir(path) diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 3152a22534f6c..5c24794035467 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1966,6 +1966,20 @@ impl fmt::Debug for Mode { } } +#[cfg(not(target_os = "wasi"))] +#[derive(Debug, Default, Clone)] +pub struct ExtraHomeDirs { + pub runtime: Option, + pub config_path: Option, + pub data_path: Option, +} + +#[cfg(not(target_os = "wasi"))] +#[derive(Debug, Default, Clone)] +pub struct ExtraMediaDirs { + pub templates: Option, +} + pub fn readdir(path: &Path) -> io::Result { let ptr = run_path_with_cstr(path, &|p| unsafe { Ok(libc::opendir(p.as_ptr())) })?; if ptr.is_null() { diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index ac1603020312f..761ad275aebd6 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -33,6 +33,7 @@ CONSOLE_MODE CONSOLE_READCONSOLE_CONTROL CONTEXT CopyFileExW +CoTaskMemFree CP_UTF8 CREATE_ALWAYS CREATE_BREAKAWAY_FROM_JOB @@ -256,6 +257,8 @@ DUPLICATE_CLOSE_SOURCE DUPLICATE_HANDLE_OPTIONS DUPLICATE_SAME_ACCESS DuplicateHandle +E_FAIL +E_INVALIDARG E_NOTIMPL ENABLE_AUTO_POSITION ENABLE_ECHO_INPUT @@ -2136,6 +2139,15 @@ FlsFree FlsGetValue FlsSetValue FlushFileBuffers +FOLDERID_Desktop +FOLDERID_Documents +FOLDERID_Downloads +FOLDERID_LocalAppData +FOLDERID_Music +FOLDERID_Pictures +FOLDERID_Public +FOLDERID_RoamingAppData +FOLDERID_Videos FORMAT_MESSAGE_ALLOCATE_BUFFER FORMAT_MESSAGE_ARGUMENT_ARRAY FORMAT_MESSAGE_FROM_HMODULE @@ -2267,6 +2279,7 @@ IPV6_MREQ IPV6_MULTICAST_LOOP IPV6_V6ONLY IsThreadAFiber +KF_FLAG_DONT_VERIFY LINGER listen LocalFree @@ -2364,6 +2377,7 @@ ReleaseSRWLockShared RemoveDirectoryW RtlGenRandom RtlNtStatusToDosError +S_OK SD_BOTH SD_RECEIVE SD_SEND @@ -2392,6 +2406,7 @@ SetLastError setsockopt SetThreadStackGuarantee SetWaitableTimer +SHGetKnownFolderPath shutdown Sleep SleepConditionVariableSRW diff --git a/library/std/src/sys/pal/windows/c/windows_sys.rs b/library/std/src/sys/pal/windows/c/windows_sys.rs index 2348f890d8a1d..5cbe36b95e1b6 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -7,6 +7,7 @@ windows_link::link!("kernel32.dll" "system" fn AcquireSRWLockShared(srwlock : *m windows_link::link!("kernel32.dll" "system" fn AddVectoredExceptionHandler(first : u32, handler : PVECTORED_EXCEPTION_HANDLER) -> *mut core::ffi::c_void); windows_link::link!("kernel32.dll" "system" fn CancelIo(hfile : HANDLE) -> BOOL); windows_link::link!("kernel32.dll" "system" fn CloseHandle(hobject : HANDLE) -> BOOL); +windows_link::link!("combase.dll" "system" fn CoTaskMemFree(pv : *const core::ffi::c_void)); windows_link::link!("kernel32.dll" "system" fn CompareStringOrdinal(lpstring1 : PCWSTR, cchcount1 : i32, lpstring2 : PCWSTR, cchcount2 : i32, bignorecase : BOOL) -> COMPARESTRING_RESULT); windows_link::link!("kernel32.dll" "system" fn CopyFileExW(lpexistingfilename : PCWSTR, lpnewfilename : PCWSTR, lpprogressroutine : LPPROGRESS_ROUTINE, lpdata : *const core::ffi::c_void, pbcancel : *mut BOOL, dwcopyflags : COPYFILE_FLAGS) -> BOOL); windows_link::link!("kernel32.dll" "system" fn CreateDirectoryW(lppathname : PCWSTR, lpsecurityattributes : *const SECURITY_ATTRIBUTES) -> BOOL); @@ -92,6 +93,7 @@ windows_link::link!("kernel32.dll" "system" fn ReleaseSRWLockShared(srwlock : *m windows_link::link!("kernel32.dll" "system" fn RemoveDirectoryW(lppathname : PCWSTR) -> BOOL); windows_link::link!("advapi32.dll" "system" "SystemFunction036" fn RtlGenRandom(randombuffer : *mut core::ffi::c_void, randombufferlength : u32) -> bool); windows_link::link!("ntdll.dll" "system" fn RtlNtStatusToDosError(status : NTSTATUS) -> u32); +windows_link::link!("shell32.dll" "system" fn SHGetKnownFolderPath(rfid : *const GUID, dwflags : u32, htoken : HANDLE, ppszpath : *mut PWSTR) -> HRESULT); windows_link::link!("kernel32.dll" "system" fn SetCurrentDirectoryW(lppathname : PCWSTR) -> BOOL); windows_link::link!("kernel32.dll" "system" fn SetEnvironmentVariableW(lpname : PCWSTR, lpvalue : PCWSTR) -> BOOL); windows_link::link!("kernel32.dll" "system" fn SetFileAttributesW(lpfilename : PCWSTR, dwfileattributes : FILE_FLAGS_AND_ATTRIBUTES) -> BOOL); @@ -2387,6 +2389,8 @@ impl Default for EXCEPTION_RECORD { } pub const EXCEPTION_STACK_OVERFLOW: NTSTATUS = 0xC00000FD_u32 as _; pub const EXTENDED_STARTUPINFO_PRESENT: PROCESS_CREATION_FLAGS = 524288u32; +pub const E_FAIL: HRESULT = 0x80004005_u32 as _; +pub const E_INVALIDARG: HRESULT = 0x80070057_u32 as _; pub const E_NOTIMPL: HRESULT = 0x80004001_u32 as _; pub const ExceptionCollidedUnwind: EXCEPTION_DISPOSITION = 3i32; pub const ExceptionContinueExecution: EXCEPTION_DISPOSITION = 0i32; @@ -2674,6 +2678,15 @@ impl Default for FLOATING_SAVE_AREA { } } pub const FLS_OUT_OF_INDEXES: u32 = 4294967295u32; +pub const FOLDERID_Desktop: GUID = GUID::from_u128(0xb4bfcc3a_db2c_424c_b029_7fe99a87c641); +pub const FOLDERID_Documents: GUID = GUID::from_u128(0xfdd39ad0_238f_46af_adb4_6c85480369c7); +pub const FOLDERID_Downloads: GUID = GUID::from_u128(0x374de290_123f_4565_9164_39c4925e467b); +pub const FOLDERID_LocalAppData: GUID = GUID::from_u128(0xf1b32785_6fba_4fcf_9d55_7b8e7f157091); +pub const FOLDERID_Music: GUID = GUID::from_u128(0x4bd8d571_6d19_48d3_be97_422220080e43); +pub const FOLDERID_Pictures: GUID = GUID::from_u128(0x33e28130_4e1e_4676_835a_98395c3bc3bb); +pub const FOLDERID_Public: GUID = GUID::from_u128(0xdfdf76a2_c82a_4d63_906a_5644ac457385); +pub const FOLDERID_RoamingAppData: GUID = GUID::from_u128(0x3eb685db_65f9_4cf6_a03a_e3ef65729f3d); +pub const FOLDERID_Videos: GUID = GUID::from_u128(0x18989b1d_99b5_455b_841c_ab7c74e4ddfc); pub const FORMAT_MESSAGE_ALLOCATE_BUFFER: FORMAT_MESSAGE_OPTIONS = 256u32; pub const FORMAT_MESSAGE_ARGUMENT_ARRAY: FORMAT_MESSAGE_OPTIONS = 8192u32; pub const FORMAT_MESSAGE_FROM_HMODULE: FORMAT_MESSAGE_OPTIONS = 2048u32; @@ -2913,6 +2926,8 @@ impl Default for IP_MREQ { pub const IP_MULTICAST_LOOP: i32 = 11i32; pub const IP_MULTICAST_TTL: i32 = 10i32; pub const IP_TTL: i32 = 4i32; +pub const KF_FLAG_DONT_VERIFY: KNOWN_FOLDER_FLAG = 16384i32; +pub type KNOWN_FOLDER_FLAG = i32; #[repr(C)] #[derive(Clone, Copy, Default)] pub struct LINGER { @@ -3330,6 +3345,7 @@ pub struct SYSTEM_INFO_0_0 { pub wProcessorArchitecture: PROCESSOR_ARCHITECTURE, pub wReserved: u16, } +pub const S_OK: HRESULT = 0x0_u32 as _; pub const TCP_NODELAY: i32 = 1i32; pub const THREAD_CREATE_RUN_IMMEDIATELY: THREAD_CREATION_FLAGS = 0u32; pub const THREAD_CREATE_SUSPENDED: THREAD_CREATION_FLAGS = 4u32;