From fcd04d6f276b839bc3ad0a4d264f73ede4c8b70d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 22 Jun 2026 16:54:41 -0400 Subject: [PATCH 01/43] Revert "Rollup merge of #157518 - CAD97:xdg_basedir, r=aapoalas" This reverts commit ac8006482d2fceed2530e529e6db42096420d4aa, reversing changes made to 8d6b38095ef12416976655207031eeb4337df71b. --- library/std/src/env.rs | 1 - library/std/src/os/unix/mod.rs | 1 - library/std/src/os/unix/xdg.rs | 167 --------------------------------- 3 files changed, 169 deletions(-) delete mode 100644 library/std/src/os/unix/xdg.rs 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/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") -} From 46652a01e55f2979fd27178b4e371464f204c0b0 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:38:14 -0400 Subject: [PATCH 02/43] add fs::UserDirs --- library/std/src/fs.rs | 5 + library/std/src/fs/dirs.rs | 349 +++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 library/std/src/fs/dirs.rs diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 750ddb91c482b..59f4529bb3370 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -48,6 +48,11 @@ 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::UserDirs; + /// 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..4d4fa3259d9cf --- /dev/null +++ b/library/std/src/fs/dirs.rs @@ -0,0 +1,349 @@ +use crate::ffi::OsString; +use crate::mem; +use crate::path::{Path, PathBuf}; + +/// A set of known user-specific directories. +/// +/// Most operating systems provide some way to discover the paths to some set +/// of "well-known" directories that it is recommended for applications to use +/// for files accessed at runtime that are not part of the application itself. +/// `UserDirs` provides an OS-agnostic way to manipulate these directories. +/// +/// A note of caution, however: multiple of these directory paths may be the +/// same as each other, so you should not assume that a file written relative +/// to the [config](Self::config) directory will not conflict with the same +/// relative path in the [data](Self::data) directory, for example. Aliasing +/// directories in this way is the common practice of some operating systems, +/// so it is important that a program still works correctly even if user paths +/// alias each other. +/// +/// # Platform-specific behavior +/// +/// As the filesystem conventions for user-specific directories varries between +/// operating systems, constructors for `UserDirs` that discover the host OS's +/// filesystem conventions are provided as extension traits under the `std::os` +/// module. +#[unstable(feature = "dir_discovery", issue = "157515")] +#[derive(Debug, Default, Clone)] +pub struct UserDirs { + pub(crate) home: UserHomeDirs, + pub(crate) media: UserMediaDirs, + #[allow(dead_code)] // only used for XDG + pub(crate) search: UserSearchDirs, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct UserHomeDirs { + pub(crate) cache: Option, + pub(crate) config: Option, + pub(crate) data: Option, + pub(crate) state: Option, + #[allow(dead_code)] // only used for XDG + pub(crate) runtime: Option, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct UserMediaDirs { + pub(crate) desktop: Option, + pub(crate) documents: Option, + pub(crate) downloads: Option, + pub(crate) music: Option, + pub(crate) pictures: Option, + pub(crate) public_share: Option, + pub(crate) videos: Option, + #[allow(dead_code)] // only used for XDG + pub(crate) templates: Option, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct UserSearchDirs { + #[allow(dead_code)] // only used for XDG + pub(crate) data: Option, + #[allow(dead_code)] // only used for XDG + pub(crate) config: Option, +} + +impl UserDirs { + /// Create a known user directory set with no known directories. + /// + /// This is useful with the builder `set_*` methods to create a `UserDirs` + /// with exactly the directories you want, without any other defaults. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn empty() -> Self { + Self::default() + } + + /// 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. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn config_home(&self) -> Option<&Path> { + self.home.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. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn data_home(&self) -> Option<&Path> { + self.home.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) 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. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn state_home(&self) -> Option<&Path> { + self.home.state.as_deref() + } + + /// 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. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn cache_home(&self) -> Option<&Path> { + self.home.cache.as_deref() + } + + /// 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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn desktop(&self) -> Option<&Path> { + self.media.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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn documents(&self) -> Option<&Path> { + self.media.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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn downloads(&self) -> Option<&Path> { + self.media.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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn music(&self) -> Option<&Path> { + self.media.music.as_deref() + } + + /// The OS-privileged user "Public Share" directory, often the `Public` + /// 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")] + pub fn public_share(&self) -> Option<&Path> { + self.media.public_share.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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn pictures(&self) -> Option<&Path> { + self.media.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. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn videos(&self) -> Option<&Path> { + self.media.videos.as_deref() + } +} + +impl UserDirs { + /// Take the contents of this directory set, leaving it empty. + /// + /// This is useful with the builder `set_*` methods to build `UserDirs` + /// without needing an intermediate mutable binding. + /// + /// # Examples + /// + /// ``` + /// #![feature(dir_discovery)] + /// use std::fs::UserDirs; + /// + /// # /* + /// let base_dir = /* ... */; + /// # */ let base_dir = std::path::PathBuf::from("/"); + /// let dirs = UserDirs::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::take(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.home.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.home.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.home.state = Some(path); + self + } + + /// 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.home.cache = Some(path); + self + } + + /// Set the path for [Self::desktop]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_desktop(&mut self, path: PathBuf) -> &mut Self { + self.media.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.media.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.media.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.media.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.media.pictures = Some(path); + self + } + + /// Set the path for [Self::public_share]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + pub fn set_public_share(&mut self, path: PathBuf) -> &mut Self { + self.media.public_share = 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.media.videos = Some(path); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_dirs_field_hookup_matches() { + let mut dirs = UserDirs::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); + + 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.public_share(), None); + assert_eq!(dirs.videos(), 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()); + + 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_public_share("/public_share".into()); + dirs.set_videos("/videos".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())); + + 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.public_share(), Some("/public_share".as_ref())); + assert_eq!(dirs.videos(), Some("/videos".as_ref())); + } +} From 9a2a784a09c17b1a10d49485a3c892c1069c3637 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:38:52 -0400 Subject: [PATCH 03/43] add unix fs::UserDirsExt --- library/std/Cargo.toml | 5 + library/std/src/os/unix/fs.rs | 5 + library/std/src/os/unix/fs/dirs.rs | 377 +++++++++++++++++++++++++++++ 3 files changed, 387 insertions(+) create mode 100644 library/std/src/os/unix/fs/dirs.rs diff --git a/library/std/Cargo.toml b/library/std/Cargo.toml index 4c433d4970cda..1ab706fdc83f4 100644 --- a/library/std/Cargo.toml +++ b/library/std/Cargo.toml @@ -62,6 +62,11 @@ path = "../windows_link" rand = { version = "0.9.0", default-features = false, features = ["alloc"] } rand_xorshift = "0.4.0" +# [target.'cfg(unix)'.dependencies] +# shlex = { version = "1.3.0", default-features = false, features = [ +# "rustc-dep-of-std", # allowlisted but missing rustc-dep-of-std feature +# ] } + [target.'cfg(any(all(target_family = "wasm", not(any(unix, target_os = "wasi"))), target_os = "xous", target_os = "vexos", all(target_vendor = "fortanix", target_env = "sgx")))'.dependencies] dlmalloc = { version = "0.2.10", features = ['rustc-dep-of-std'] } diff --git a/library/std/src/os/unix/fs.rs b/library/std/src/os/unix/fs.rs index c119912c3b022..8e8e94458cb27 100644 --- a/library/std/src/os/unix/fs.rs +++ b/library/std/src/os/unix/fs.rs @@ -21,6 +21,11 @@ use crate::{io, sys}; #[cfg(test)] mod tests; +mod dirs; + +#[unstable(feature = "dir_discovery", issue = "157515")] +pub use self::dirs::UserDirsExt; + /// 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..53ff76e72d3e8 --- /dev/null +++ b/library/std/src/os/unix/fs/dirs.rs @@ -0,0 +1,377 @@ +// use shlex::bytes::Shlex; + +use crate::env::{JoinPathsError, SplitPaths, home_dir, join_paths, split_paths, var_os}; +use crate::ffi::{OsStr, OsString}; +use crate::fs::{self, UserDirs}; +use crate::io::{self, ErrorKind, const_error}; +use crate::os::unix::ffi::{OsStrExt, OsStringExt}; +use crate::path::{Path, PathBuf}; + +trait Sealed {} +impl Sealed for UserDirs {} + +/// XDG-specific extensions to [`fs::UserDirs`](UserDirs). +/// +/// The XDG conventions are defined by the Freedesktop.org project in the +/// [XDG Base Directory Specification][xdg-basedir] and the [xdg-user-dirs] +/// tool. 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 convetions 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/ +/// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait UserDirsExt: Sized + Sealed { + /// Load the user directory paths according to the + /// [XDG Base Directory Specification][xdg-basedir]. + /// + /// Sets [`cache_home`], [`config_home`], [`data_home`], and + /// [`state_home`], as well as the XDG-specific [`runtime_home`], + /// [`config_dirs`], and [`data_dirs`]. Each of these are set to the value + /// of their 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`](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 common to both the shell + /// and the `xdg-user-dirs` tool. + /// + /// # Errors + /// + /// Errors if the user's home directory cannot be determined. + /// + /// [xdg-basedir]: https://specifications.freedesktop.org/basedir/ + /// [`cache_home`]: UserDirs::cache_home + /// [`config_home`]: UserDirs::config_home + /// [`data_home`]: UserDirs::data_home + /// [`state_home`]: UserDirs::state_home + /// [`runtime_home`]: UserDirsExt::runtime_home + /// [`config_dirs`]: UserDirsExt::config_dirs + /// [`data_dirs`]: UserDirsExt::data_dirs + #[unstable(feature = "dir_discovery", issue = "157515")] + fn xdg_base() -> io::Result; + + /// Load the user directory paths according to the xdg-user-dirs tool. + /// + /// In addition to the base directories set by [`xdg_base`], this also + /// reads the `$XDG_CONFIG_HOME/user-dirs.dirs` file as defined by the + /// [xdg-user-dirs] tool to set [`desktop`], [`documents`], [`downloads`], + /// [`music`], [`pictures`], [`public_share`], and [`videos`], as well as + /// the XDG-specific [`templates`]. + /// + /// `user-dirs.dirs` uses "a shell format, so it's easy to access from a + /// shell script," and the way the [xdg-user-dirs] tool suggests loading + /// the configuration is to `source` the file in a shell. Instead of using + /// the shell (and potentially executing arbitrary code), we directly read + /// and parse the file. + /// + /// Only lines in the `XDG_{NAME}_DIR={path}` format are processed; all + /// other lines are ignored. `{NAME}` is a known directory name defined by + /// the xdg-user-dirs tool, and `{path}` is a double-quoted shell-escaped + /// path; any line that does not match this format is silently ignored. + /// Additionally, we follow the documentation and only expand a leading + /// `$HOME/` prefix in the path; other shell expansions may work in other + /// tools, but will be unexpanded in the returned path here if present. + /// Furthermore, paths which are neither rooted nor relative to `$HOME` + /// are ignored, as they are not valid according to the specification. + /// + /// # Errors + /// + /// Errors if the user's home directory cannot be determined or if the + /// `$XDG_CONFIG_HOME/user-dirs.dirs` file cannot be read. + /// + /// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ + /// [`desktop`]: UserDirs::desktop + /// [`documents`]: UserDirs::documents + /// [`downloads`]: UserDirs::downloads + /// [`music`]: UserDirs::music + /// [`pictures`]: UserDirs::pictures + /// [`public_share`]: UserDirs::public_share + /// [`videos`]: UserDirs::videos + /// [`templates`]: UserDirsExt::templates + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn xdg_user() -> 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`](UserDirs::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. + #[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`](UserDirs::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. + #[unstable(feature = "dir_search_discovery", issue = "157515")] + fn data_dirs(&self) -> Option>; + + /// 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::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>; + + /// Set the paths for [Self::templates]. + #[unstable(feature = "media_dir_discovery", issue = "157515")] + fn set_templates(&mut self, path: PathBuf) -> &mut Self; +} + +impl UserDirs { + fn set_xdg_base(&mut self, user_home: &Path) { + self.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); + self.home.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); + self.home.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); + self.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); + self.home.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); + + self.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); + self.search.config = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); + } +} + +#[unstable(feature = "dir_discovery", issue = "157515")] +impl UserDirsExt for UserDirs { + fn xdg_base() -> io::Result { + let mut dirs = Self::empty(); + let user_home = user_home()?; + dirs.set_xdg_base(&user_home); + Ok(dirs) + } + + fn xdg_user() -> io::Result { + let mut dirs = Self::empty(); + let user_home = user_home()?; + + // load the base directories first + dirs.set_xdg_base(&user_home); + + let spec = match fs::read(dirs.config_home().unwrap().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); + // expand the only allowed expansion: a leading `$HOME/` prefix, still quoted + let buffer; + const HOME_RELATIVE_PREFIX: &[u8] = b"\"$HOME/"; + let expanded = if val.starts_with(HOME_RELATIVE_PREFIX) { + let joined = user_home.join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); + buffer = OsString::from_iter([OsStr::new("\""), joined.as_os_str()]); + buffer.as_bytes() + } else { + val + }; + + // the path value is shell-escaped, so unescape it + // FIXME: get shlex working as dep-of-std + // let mut lex = Shlex::new(expanded); + // let Some(path) = lex.next() else { continue }; + // let None = lex.next() else { continue }; + let path = &expanded[1..expanded.len() - 1]; // FIXME: placeholder quote strip + + // load the known user directories + match var { + b"XDG_DESKTOP_DIR" => dirs.media.desktop = Some(path_from_bytes(&path)), + b"XDG_DOCUMENTS_DIR" => dirs.media.documents = Some(path_from_bytes(&path)), + b"XDG_DOWNLOAD_DIR" => dirs.media.downloads = Some(path_from_bytes(&path)), + b"XDG_MUSIC_DIR" => dirs.media.music = Some(path_from_bytes(&path)), + b"XDG_PICTURES_DIR" => dirs.media.pictures = Some(path_from_bytes(&path)), + b"XDG_PUBLICSHARE_DIR" => dirs.media.public_share = Some(path_from_bytes(&path)), + b"XDG_VIDEOS_DIR" => dirs.media.videos = Some(path_from_bytes(&path)), + b"XDG_TEMPLATES_DIR" => dirs.media.templates = Some(path_from_bytes(&path)), + _ => { + // ignore unknown variable assignment, matching shell permissiveness + } + } + } + + Ok(dirs) + } + + fn runtime_home(&self) -> Option<&Path> { + self.home.runtime.as_deref() + } + + fn config_dirs(&self) -> Option> { + self.search.config.as_ref().map(|s| split_paths(s)) + } + + fn data_dirs(&self) -> Option> { + self.search.data.as_ref().map(|s| split_paths(s)) + } + + fn templates(&self) -> Option<&Path> { + self.media.templates.as_deref() + } + + fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self { + self.home.runtime = Some(path); + self + } + + fn set_config_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError> { + self.search.config = Some(join_paths(paths)?); + Ok(self) + } + + fn set_data_dirs( + &mut self, + paths: impl IntoIterator>, + ) -> Result<&mut Self, JoinPathsError> { + self.search.data = Some(join_paths(paths)?); + Ok(self) + } + + fn set_templates(&mut self, path: PathBuf) -> &mut Self { + self.media.templates = Some(path); + self + } +} + +fn user_home() -> Result { + 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 path_from_bytes(bytes: &[u8]) -> PathBuf { + PathBuf::from(OsString::from_vec(bytes.to_vec())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_fetch_xdg_base_dirs() { + let dirs = UserDirs::xdg_base().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()); + + assert!(dirs.desktop().is_none()); + assert!(dirs.documents().is_none()); + assert!(dirs.downloads().is_none()); + assert!(dirs.music().is_none()); + assert!(dirs.pictures().is_none()); + assert!(dirs.public_share().is_none()); + assert!(dirs.videos().is_none()); + assert!(dirs.templates().is_none()); + } + + #[test] + fn can_fetch_xdg_user_dirs() { + let dirs = UserDirs::xdg_user().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()); + + 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.public_share().is_some()); + assert!(dirs.videos().is_some()); + assert!(dirs.templates().is_some()); + } +} From 0870d94f8f7c78e16d1a275892661bf937aff8d7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:39:12 -0400 Subject: [PATCH 04/43] add windows fs::UserDirsExt --- library/std/src/os/windows/fs.rs | 5 + library/std/src/os/windows/fs/dirs.rs | 182 ++++++++++++++++++ .../std/src/sys/pal/windows/c/bindings.txt | 14 ++ .../std/src/sys/pal/windows/c/windows_sys.rs | 16 ++ 4 files changed, 217 insertions(+) create mode 100644 library/std/src/os/windows/fs/dirs.rs diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index dfa9236a7e428..a5f19628c148d 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -11,6 +11,11 @@ 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::UserDirsExt; + /// 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..3160d18278fc4 --- /dev/null +++ b/library/std/src/os/windows/fs/dirs.rs @@ -0,0 +1,182 @@ +use crate::fs::UserDirs; +use crate::io; +#[cfg(windows)] +use crate::{ + io::{ErrorKind, const_error}, + path::PathBuf, + ptr, slice, + sys::{c, os2path}, +}; + +trait Sealed {} +impl Sealed for UserDirs {} + +/// Windows-specific extensions to [`fs::UserDirs`](UserDirs). +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait UserDirsExt: Sized + Sealed { + /// Load the known user folder paths using the [Known Folders] API. + /// + /// The loaded known folders are: + /// + /// | `UserDirs` | [`KNOWNFOLDERID`] | + /// | ---------- | ----------------- | + /// | [`cache_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) | + /// | [`config_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) | + /// | [`data_home`] | [`FOLDERID_RoamingAppData`] (`%APPDATA%`) | + /// | [`state_home`] | [`FOLDERID_LocalAppData`] (`%LOCALAPPDATA%`) | + /// | [`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`) | + /// | [`public_share`] | [`FOLDERID_Public`] (`%PUBLIC%`)[^public] | + /// | [`videos`] | [`FOLDERID_Videos`] (`%USERPROFILE%\Videos`) | + /// + /// 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 Apple platforms, making it even more important to not + /// assume files in different user directories cannot alias each other. + /// + /// [^public]: The default public directory is a separate user folder, + /// unlike other OSes where it is a subdirectory of the user's home. + /// This is only particularly meaningful on multi-user systems. + /// + /// # 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 `UserDirs`. + /// + /// # Implementation-specific behavior + /// + /// Calls [`SHGetKnownFolderPath`] configured to not create the folder if + /// missing for the current user once for each of `FOLDERID_Desktop`, + /// `FOLDERID_Documents`, `FOLDERID_Downloads`, `FOLDERID_LocalAppData`, + /// `FOLDERID_Music`, `FOLDERID_Pictures`, `FOLDERID_Public`, + /// `FOLDERID_RoamingAppData`, and `FOLDERID_Videos`. + /// + /// 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 + /// [`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_Public`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_public + /// [`FOLDERID_Videos`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos + #[unstable(feature = "dir_discovery", issue = "157515")] + fn known_folders() -> io::Result; +} + +#[cfg(windows)] +impl UserDirsExt for UserDirs { + fn known_folders() -> io::Result { + let desktop = get_known_folder_path(&c::FOLDERID_Desktop)?; + let documents = get_known_folder_path(&c::FOLDERID_Documents)?; + let downloads = get_known_folder_path(&c::FOLDERID_Downloads)?; + let local_app_data = get_known_folder_path(&c::FOLDERID_LocalAppData)?; + let music = get_known_folder_path(&c::FOLDERID_Music)?; + let pictures = get_known_folder_path(&c::FOLDERID_Pictures)?; + let public = get_known_folder_path(&c::FOLDERID_Public)?; + let roaming_app_data = get_known_folder_path(&c::FOLDERID_RoamingAppData)?; + let videos = 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 accross machines + + let mut dirs = UserDirs::empty(); + + dirs.home.cache_home = Some(local_app_data.clone()); + dirs.home.config_home = Some(roaming_app_data.clone()); + dirs.home.data_home = Some(roaming_app_data); + dirs.home.state_home = Some(local_app_data); + + dirs.media.desktop = desktop; + dirs.media.documents = documents; + dirs.media.downloads = downloads; + dirs.media.music = music; + dirs.media.pictures = pictures; + dirs.media.public_share = public; + dirs.media.videos = videos; + + dirs + } +} + +/// Retreive a known folder path from the Windows API. +#[cfg(windows)] +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 + // UserDirs does not 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, + /* 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 + 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()) }; + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_fetch_known_folder_paths() { + let dirs = UserDirs::known_folders().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()); + 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.public_share().is_some()); + assert!(dirs.videos().is_some()); + } +} diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index ac1603020312f..32f289f9eff6e 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,7 @@ DUPLICATE_CLOSE_SOURCE DUPLICATE_HANDLE_OPTIONS DUPLICATE_SAME_ACCESS DuplicateHandle +E_INVALIDARG E_NOTIMPL ENABLE_AUTO_POSITION ENABLE_ECHO_INPUT @@ -2136,6 +2138,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 +2278,7 @@ IPV6_MREQ IPV6_MULTICAST_LOOP IPV6_V6ONLY IsThreadAFiber +KF_FLAG_DONT_VERIFY LINGER listen LocalFree @@ -2364,6 +2376,7 @@ ReleaseSRWLockShared RemoveDirectoryW RtlGenRandom RtlNtStatusToDosError +S_OK SD_BOTH SD_RECEIVE SD_SEND @@ -2392,6 +2405,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..e40c86681b941 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,7 @@ 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_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 +2677,16 @@ 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_LocalAppDataLow: GUID = GUID::from_u128(0xa520a1a4_1780_4ff6_bd18_167343c5af16); +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; From 177dcf1bea22584842c66e78b5ec3892237a740e Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:39:23 -0400 Subject: [PATCH 05/43] add darwin fs::UserDirsExt --- library/std/src/os/darwin/fs.rs | 5 + library/std/src/os/darwin/fs/dirs.rs | 247 +++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 library/std/src/os/darwin/fs/dirs.rs diff --git a/library/std/src/os/darwin/fs.rs b/library/std/src/os/darwin/fs.rs index fc2869d6b13f6..ba36ca55011b5 100644 --- a/library/std/src/os/darwin/fs.rs +++ b/library/std/src/os/darwin/fs.rs @@ -7,6 +7,11 @@ 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::UserDirsExt; + /// 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..2a8e605936c5e --- /dev/null +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -0,0 +1,247 @@ +use crate::env::home_dir; +use crate::ffi::CStr; +use crate::fs::UserDirs; +use crate::io::{self, ErrorKind, const_error}; +use crate::path::{Path, PathBuf}; + +trait Sealed {} +impl Sealed for UserDirs {} + +/// Darwin-specific extensions to [`fs::UserDirs`](UserDirs). +#[unstable(feature = "dir_discovery", issue = "157515")] +#[expect(private_bounds, reason = "sealed")] +pub trait UserDirsExt: Sized + Sealed { + /// Load the standard user directory paths for the current user. + /// + /// On iOS, tvOS, watchOS, visionOS, and sandboxed macOS applications, + /// [`cache_home`], [`config_home`], [`data_home`], and [`state_home`] + /// 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 user directory is accessible there. + /// + /// The loaded common directories are: + /// + /// | `UserDirs` | [`SearchPathDirectory`] | + /// | ---------- | ----------------------- | + /// | [`cache_home`] | [`.cachesDirectory`] (`~/Library/Caches`) | + /// | [`config_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`data_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`state_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`desktop`] | [`.desktopDirectory`] (`~/Desktop`) | + /// | [`documents`] | [`.documentDirectory`] (`~/Documents`) | + /// | [`downloads`] | [`.downloadsDirectory`] | + /// | [`music`] | [`.musicDirectory`] (`~/Music`) | + /// | [`pictures`] | [`.picturesDirectory`] (`~/Pictures`) | + /// | [`public_share`] | [`.sharedPublicDirectory`] (`~/Public`) | + /// | [`videos`] | [`.moviesDirectory`] (`~/Movies`) | + /// + /// 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 underlying system directory discovery API returns + /// multiple directories for a queried standard directory, if a + /// directory path is not valid Unicode, or if the [user home](home_dir) + /// cannot be determined. + /// + /// # Implementation-specific behavior + /// + /// Uses the `sysdir(3)` API from `libSystem` to discover the standard + /// user directories. Iterates each of `SYSDIR_DIRECTORY_DOCUMENT`, + /// `SYSDIR_DIRECTORY_DESKTOP`, `SYSDIR_DIRECTORY_CACHES`, + /// `SYSDIR_DIRECTORY_APPLICATION_SUPPORT`, `SYSDIR_DIRECTORY_DOWNLOADS`, + /// `SYSDIR_DIRECTORY_MOVIES`, `SYSDIR_DIRECTORY_MUSIC`, + /// `SYSDIR_DIRECTORY_PICTURES`, `SYSDIR_DIRECTORY_SHARED_PUBLIC` once. + /// + /// 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`]: UserDirs::cache_home + /// [`config_home`]: UserDirs::config_home + /// [`data_home`]: UserDirs::data_home + /// [`state_home`]: UserDirs::state_home + /// [`desktop`]: UserDirs::desktop + /// [`documents`]: UserDirs::documents + /// [`downloads`]: UserDirs::downloads + /// [`music`]: UserDirs::music + /// [`pictures`]: UserDirs::pictures + /// [`public_share`]: UserDirs::public_share + /// [`videos`]: UserDirs::videos + /// + /// [`SearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory + /// [`.cachesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory + /// [`.applicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory + /// [`.desktopDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/desktopdirectory + /// [`.documentDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/documentdirectory + /// [`.downloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory + /// [`.musicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory + /// [`.picturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory + /// [`.sharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory + /// [`.moviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory + #[unstable(feature = "dir_discovery", issue = "157515")] + fn sysdir() -> io::Result; +} + +impl UserDirsExt for UserDirs { + fn sysdir() -> io::Result { + let home = home_dir() + .filter(|p| !p.is_empty()) + .ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; + + let caches = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES) }?; + let application_support = + unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_APPLICATION_SUPPORT) }?; + let desktop = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DESKTOP) }?; + let documents = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOCUMENT) }?; + let downloads = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOWNLOADS) }?; + let movies = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MOVIES) }?; + let music = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MUSIC) }?; + let pictures = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_PICTURES) }?; + let public_share = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_SHARED_PUBLIC) }?; + + let mut dirs = UserDirs::empty(); + + dirs.home.cache = caches; + // Apple puts config/data/state all in Application Support + dirs.home.config = application_support.clone(); + dirs.home.data = application_support.clone(); + dirs.home.state = application_support; + + dirs.media.desktop = desktop; + dirs.media.documents = documents; + dirs.media.downloads = downloads; + dirs.media.music = music; + dirs.media.pictures = pictures; + dirs.media.public_share = public_share; + dirs.media.videos = movies; + + Ok(dirs) + } +} + +/// Get the path for a system directory using `sysdir(3)`. +/// +/// # Safety +/// +/// `kind` must be a valid `sysdir_search_path_directory_t` value. +unsafe fn get_user_sysdir( + home: &Path, + kind: sys::sysdir_search_path_directory_t, +) -> io::Result> { + // SAFETY: `kind` is a valid `sysdir_search_path_directory_t` value, and + // `SYSDIR_DOMAIN_MASK_USER` is a valid `sysdir_search_path_domain_mask_t` value. + let state = + unsafe { sys::sysdir_start_search_path_enumeration(kind, sys::SYSDIR_DOMAIN_MASK_USER) }; + if state == 0 { + // 0 directory paths produced + return Ok(None); + } + + let mut path = [0 as c_char; PATH_MAX]; + // SAFETY: `state` comes from `sysdir_start_search_path_enumeration` + // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX-1` bytes to `path` + let state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; + if state == 0 { + // exactly 1 directory path produced + let Ok(path) = CStr::from_bytes_until_null(&path) else { + return Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path too long", + )); + }; + // read as UTF-8 + let Ok(path) = path.to_str() else { + return Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path not valid UTF-8", + )); + }; + // expand the `~` shorthand + if path == "~" { + Ok(Some(home.into())) + } else if path.starts_with("~/") { + Ok(Some(home.join(&path[2..]))) + } else if path.starts_with("~") { + Err(const_error!( + ErrorKind::InvalidData, + "standard user directory relative to different user", + )) + } else if path.starts_with('/') { + Ok(Some(path.into())) + } else { + Err(const_error!(ErrorKind::InvalidData, "standard user directory path not absolute")) + } + } + + // 2+ directory paths produced + let mut state = state; + // exhaust iterator for cleanup; ignore the paths that get returned + while state != 0 { + // SAFETY: `state` is nonzero and is from a previous call to sysdir_get_next_search_path_enumeration + state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; + } + + Err(const_error!(ErrorKind::InvalidData, "multiple paths returned for standard user directory")) +} + +mod sys { + use crate::ffi::{c_char, c_uint}; + + pub const PATH_MAX: usize = 1024; // matches darwin define + + pub type sysdir_search_path_directory_t = u32; + pub const SYSDIR_DIRECTORY_DOCUMENT: sysdir_search_path_directory_t = 9; + pub const SYSDIR_DIRECTORY_DESKTOP: sysdir_search_path_directory_t = 12; + pub const SYSDIR_DIRECTORY_CACHES: sysdir_search_path_directory_t = 13; + pub const SYSDIR_DIRECTORY_APPLICATION_SUPPORT: sysdir_search_path_directory_t = 14; + pub const SYSDIR_DIRECTORY_DOWNLOADS: sysdir_search_path_directory_t = 15; + pub const SYSDIR_DIRECTORY_MOVIES: sysdir_search_path_directory_t = 17; + pub const SYSDIR_DIRECTORY_MUSIC: sysdir_search_path_directory_t = 18; + pub const SYSDIR_DIRECTORY_PICTURES: sysdir_search_path_directory_t = 19; + pub const SYSDIR_DIRECTORY_SHARED_PUBLIC: sysdir_search_path_directory_t = 21; + + pub type sysdir_search_path_domain_mask_t = c_uint; + pub const SYSDIR_DOMAIN_MASK_USER: sysdir_search_path_domain_mask_t = 0x0001; + + pub type sysdir_search_path_enumeration_state = c_uint; + + unsafe extern "C" { + pub unsafe fn sysdir_start_search_path_enumeration( + dir: sysdir_search_path_directory_t, + domain_mask: sysdir_search_path_domain_mask_t, + ) -> sysdir_search_path_enumeration_state; + pub unsafe fn sysdir_get_next_search_path_enumeration( + state: sysdir_search_path_enumeration_state, + path: *mut c_char, + ) -> sysdir_search_path_enumeration_state; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_fetch_sysdir_paths() { + let dirs = UserDirs::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()); + 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.public_share().is_some()); + assert!(dirs.videos().is_some()); + } +} From 04e6a34cfc0897f988290dcd4851a2f267f052d5 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 18:29:07 -0400 Subject: [PATCH 06/43] remove impl Default for UserDirs This could reasonably be expected to load the OS-specific defaults. Remove the impl to force users to explicitly choose. This also allows us to impl Default with that behavior later, if we so desire. --- library/std/src/fs/dirs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 4d4fa3259d9cf..e052597fcf843 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -24,7 +24,7 @@ use crate::path::{Path, PathBuf}; /// filesystem conventions are provided as extension traits under the `std::os` /// module. #[unstable(feature = "dir_discovery", issue = "157515")] -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct UserDirs { pub(crate) home: UserHomeDirs, pub(crate) media: UserMediaDirs, @@ -70,7 +70,7 @@ impl UserDirs { /// with exactly the directories you want, without any other defaults. #[unstable(feature = "dir_discovery", issue = "157515")] pub fn empty() -> Self { - Self::default() + Self { home: Default::default(), media: Default::default(), search: Default::default() } } /// A base directory relative to which user-specific configuration files @@ -218,7 +218,7 @@ impl UserDirs { /// ``` #[unstable(feature = "dir_discovery", issue = "157515")] pub fn take(&mut self) -> Self { - mem::take(self) + mem::replace(self, Self::empty()) } /// Set the path for [Self::config_home]. From 44d77cf1eaff015c874dbb54ce7b647d98160a2a Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 19:07:57 -0400 Subject: [PATCH 07/43] add UserDirs::user_home --- library/std/src/fs/dirs.rs | 41 ++++++++++++++++++++++- library/std/src/os/darwin/fs/dirs.rs | 5 ++- library/std/src/os/unix/fs/dirs.rs | 47 +++++++++++---------------- library/std/src/os/windows/fs/dirs.rs | 2 +- 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index e052597fcf843..8486f30100a5b 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -1,6 +1,6 @@ use crate::ffi::OsString; -use crate::mem; use crate::path::{Path, PathBuf}; +use crate::{env, mem}; /// A set of known user-specific directories. /// @@ -17,6 +17,10 @@ use crate::path::{Path, PathBuf}; /// so it is important that a program still works correctly even if user paths /// alias each other. /// +/// It is not required that the user directories are for 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. +/// /// # Platform-specific behavior /// /// As the filesystem conventions for user-specific directories varries between @@ -34,6 +38,7 @@ pub struct UserDirs { #[derive(Debug, Default, Clone)] pub(crate) struct UserHomeDirs { + pub(crate) user: Option, pub(crate) cache: Option, pub(crate) config: Option, pub(crate) data: Option, @@ -64,6 +69,21 @@ pub(crate) struct UserSearchDirs { } impl UserDirs { + /// Create a known user directory set with only [`user_home`] set. + /// + /// Unlike [`env::home_dir`], this treats an empty home path as `None`. + /// + /// This is useful with the builder `set_*` methods to create a `UserDirs` + /// with exactly the directories you want, without any other defaults. + /// + /// [`user_home`]: Self::user_home + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn new() -> Self { + let mut dirs = Self::empty(); + dirs.home.user = env::home_dir().filter(|p| !p.is_empty()); + dirs + } + /// Create a known user directory set with no known directories. /// /// This is useful with the builder `set_*` methods to create a `UserDirs` @@ -73,6 +93,18 @@ impl UserDirs { Self { home: Default::default(), media: Default::default(), search: Default::default() } } + /// The path to the user's home directory. + /// + /// Applications putting files in the user's home directory without being + /// directed to is generally discouraged. This should typically be used as + /// a default path for file selection dialogs, not for automatically + /// accessed file paths. Use one of [`config_home`], [`data_home`], + /// [`state_home`], or [`cache_home`] instead as appropriate for the file. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn user_home(&self) -> Option<&Path> { + self.home.user.as_deref() + } + /// A base directory relative to which user-specific configuration files /// should be stored. /// @@ -221,6 +253,13 @@ impl UserDirs { mem::replace(self, Self::empty()) } + /// Set the path for [Self::user_home]. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn set_user_home(&mut self, path: PathBuf) -> &mut Self { + self.home.user = 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 { diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 2a8e605936c5e..35d065c647cd6 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -92,9 +92,8 @@ pub trait UserDirsExt: Sized + Sealed { impl UserDirsExt for UserDirs { fn sysdir() -> io::Result { - let home = home_dir() - .filter(|p| !p.is_empty()) - .ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; + let mut dirs = UserDirs::new(); + let home = dirs.home_dir().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; let caches = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES) }?; let application_support = diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 53ff76e72d3e8..46967cc5c58a4 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -175,34 +175,28 @@ pub trait UserDirsExt: Sized + Sealed { fn set_templates(&mut self, path: PathBuf) -> &mut Self; } -impl UserDirs { - fn set_xdg_base(&mut self, user_home: &Path) { - self.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); - self.home.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); - self.home.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); - self.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); - self.home.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); - - self.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); - self.search.config = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); - } -} - #[unstable(feature = "dir_discovery", issue = "157515")] impl UserDirsExt for UserDirs { fn xdg_base() -> io::Result { - let mut dirs = Self::empty(); - let user_home = user_home()?; - dirs.set_xdg_base(&user_home); + let mut dirs = Self::new(); + let user_home = home_dir() + .filter(|p| !p.is_empty()) + .ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; + + dirs.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); + dirs.home.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); + dirs.home.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); + dirs.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); + dirs.home.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); + + dirs.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); + dirs.search.config = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); + Ok(dirs) } fn xdg_user() -> io::Result { - let mut dirs = Self::empty(); - let user_home = user_home()?; - - // load the base directories first - dirs.set_xdg_base(&user_home); + let mut dirs = Self::xdg_base()?; let spec = match fs::read(dirs.config_home().unwrap().join("user-dirs.dirs")) { Ok(spec) => spec, @@ -233,7 +227,10 @@ impl UserDirsExt for UserDirs { let buffer; const HOME_RELATIVE_PREFIX: &[u8] = b"\"$HOME/"; let expanded = if val.starts_with(HOME_RELATIVE_PREFIX) { - let joined = user_home.join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); + let joined = dirs + .user_home() + .unwrap() + .join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); buffer = OsString::from_iter([OsStr::new("\""), joined.as_os_str()]); buffer.as_bytes() } else { @@ -309,12 +306,6 @@ impl UserDirsExt for UserDirs { } } -fn user_home() -> Result { - 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) } diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 3160d18278fc4..a2f4b640d5f50 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -93,7 +93,7 @@ impl UserDirsExt for UserDirs { // AppData/Local -- system-local, doesn't make sense to sync to another // AppData/Roaming -- data that makes sense to sync accross machines - let mut dirs = UserDirs::empty(); + let mut dirs = UserDirs::new(); dirs.home.cache_home = Some(local_app_data.clone()); dirs.home.config_home = Some(roaming_app_data.clone()); From e224808200c08c60d4a7b6ffca4a70ba4f01eb9c Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 19:43:11 -0400 Subject: [PATCH 08/43] order UserDirs fields more consistently --- library/std/src/fs/dirs.rs | 45 +++++++++++++++--------------- library/std/src/os/unix/fs/dirs.rs | 6 ++-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 8486f30100a5b..71453c121e495 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -62,10 +62,10 @@ pub(crate) struct UserMediaDirs { #[derive(Debug, Default, Clone)] pub(crate) struct UserSearchDirs { - #[allow(dead_code)] // only used for XDG - pub(crate) data: Option, #[allow(dead_code)] // only used for XDG pub(crate) config: Option, + #[allow(dead_code)] // only used for XDG + pub(crate) data: Option, } impl UserDirs { @@ -105,10 +105,24 @@ impl UserDirs { self.home.user.as_deref() } + /// 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. + #[unstable(feature = "dir_discovery", issue = "157515")] + pub fn cache_home(&self) -> Option<&Path> { + self.home.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. #[unstable(feature = "dir_discovery", issue = "157515")] pub fn config_home(&self) -> Option<&Path> { @@ -142,19 +156,6 @@ impl UserDirs { self.home.state.as_deref() } - /// 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. - #[unstable(feature = "dir_discovery", issue = "157515")] - pub fn cache_home(&self) -> Option<&Path> { - self.home.cache.as_deref() - } - /// The OS-privileged user "Desktop" directory, often the `Desktop` /// folder in the user's home directory. /// @@ -260,6 +261,13 @@ impl UserDirs { self } + /// 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.home.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 { @@ -281,13 +289,6 @@ impl UserDirs { self } - /// 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.home.cache = Some(path); - self - } - /// Set the path for [Self::desktop]. #[unstable(feature = "media_dir_discovery", issue = "157515")] pub fn set_desktop(&mut self, path: PathBuf) -> &mut Self { diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 46967cc5c58a4..b08abe7f5f37c 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -183,14 +183,14 @@ impl UserDirsExt for UserDirs { .filter(|p| !p.is_empty()) .ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; - dirs.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); + dirs.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); dirs.home.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); + dirs.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); dirs.home.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); - dirs.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); dirs.home.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); - dirs.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); dirs.search.config = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); + dirs.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); Ok(dirs) } From e3ddb275080652fdd94b9631e64ef47cc571ae07 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 04:38:45 -0400 Subject: [PATCH 09/43] minor fixes --- library/std/src/os/unix/fs/dirs.rs | 2 +- library/std/src/os/windows/fs/dirs.rs | 19 ++++++++++--------- .../std/src/sys/pal/windows/c/bindings.txt | 1 + .../std/src/sys/pal/windows/c/windows_sys.rs | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index b08abe7f5f37c..94a4528a12521 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -18,7 +18,7 @@ impl Sealed for UserDirs {} /// /// 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 convetions on macOS, note that +/// 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. diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index a2f4b640d5f50..a4580e43f9f2a 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -78,6 +78,7 @@ pub trait UserDirsExt: Sized + Sealed { } #[cfg(windows)] +#[unstable(feature = "dir_discovery", issue = "157515")] impl UserDirsExt for UserDirs { fn known_folders() -> io::Result { let desktop = get_known_folder_path(&c::FOLDERID_Desktop)?; @@ -91,14 +92,14 @@ impl UserDirsExt for UserDirs { let videos = 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 accross machines + // AppData/Roaming -- data that makes sense to sync across machines let mut dirs = UserDirs::new(); - dirs.home.cache_home = Some(local_app_data.clone()); - dirs.home.config_home = Some(roaming_app_data.clone()); - dirs.home.data_home = Some(roaming_app_data); - dirs.home.state_home = Some(local_app_data); + dirs.home.cache = local_app_data.clone(); + dirs.home.config = roaming_app_data.clone(); + dirs.home.data = roaming_app_data; + dirs.home.state = local_app_data; dirs.media.desktop = desktop; dirs.media.documents = documents; @@ -108,11 +109,11 @@ impl UserDirsExt for UserDirs { dirs.media.public_share = public; dirs.media.videos = videos; - dirs + Ok(dirs) } } -/// Retreive a known folder path from the Windows API. +/// Retrieve a known folder path from the Windows API. #[cfg(windows)] fn get_known_folder_path(id: &c::GUID) -> io::Result> { // Get the known folder path. hToken = NULL requests the current user @@ -124,7 +125,7 @@ fn get_known_folder_path(id: &c::GUID) -> io::Result> { let hr = unsafe { c::SHGetKnownFolderPath( /* rfid */ id, - /* dwFlags */ c::KF_FLAG_DONT_VERIFY, + /* dwFlags */ c::KF_FLAG_DONT_VERIFY as _, /* hToken */ ptr::null_mut(), /* ppszPath */ &mut pszPath, ) @@ -157,7 +158,7 @@ fn get_known_folder_path(id: &c::GUID) -> io::Result> { // succeeds or not. unsafe { c::CoTaskMemFree(pszPath.cast()) }; - Ok(result) + result } #[cfg(test)] diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index 32f289f9eff6e..761ad275aebd6 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -257,6 +257,7 @@ DUPLICATE_CLOSE_SOURCE DUPLICATE_HANDLE_OPTIONS DUPLICATE_SAME_ACCESS DuplicateHandle +E_FAIL E_INVALIDARG E_NOTIMPL ENABLE_AUTO_POSITION 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 e40c86681b941..5cbe36b95e1b6 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -2389,6 +2389,7 @@ 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; @@ -2681,7 +2682,6 @@ pub const FOLDERID_Desktop: GUID = GUID::from_u128(0xb4bfcc3a_db2c_424c_b029_7fe 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_LocalAppDataLow: GUID = GUID::from_u128(0xa520a1a4_1780_4ff6_bd18_167343c5af16); 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); From fa9eb891b28672df105ef09914ba75fb27a58da3 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 11:50:45 -0400 Subject: [PATCH 10/43] use libc for darwin sysdir(3) --- library/std/src/os/darwin/fs/dirs.rs | 42 ++++++---------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 35d065c647cd6..70fa6ba35416c 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -1,5 +1,4 @@ -use crate::env::home_dir; -use crate::ffi::CStr; +use crate::ffi::{CStr, c_char}; use crate::fs::UserDirs; use crate::io::{self, ErrorKind, const_error}; use crate::path::{Path, PathBuf}; @@ -144,7 +143,7 @@ unsafe fn get_user_sysdir( return Ok(None); } - let mut path = [0 as c_char; PATH_MAX]; + let mut path = [0 as c_char; sys::PATH_MAX]; // SAFETY: `state` comes from `sysdir_start_search_path_enumeration` // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX-1` bytes to `path` let state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; @@ -192,36 +191,13 @@ unsafe fn get_user_sysdir( } mod sys { - use crate::ffi::{c_char, c_uint}; - - pub const PATH_MAX: usize = 1024; // matches darwin define - - pub type sysdir_search_path_directory_t = u32; - pub const SYSDIR_DIRECTORY_DOCUMENT: sysdir_search_path_directory_t = 9; - pub const SYSDIR_DIRECTORY_DESKTOP: sysdir_search_path_directory_t = 12; - pub const SYSDIR_DIRECTORY_CACHES: sysdir_search_path_directory_t = 13; - pub const SYSDIR_DIRECTORY_APPLICATION_SUPPORT: sysdir_search_path_directory_t = 14; - pub const SYSDIR_DIRECTORY_DOWNLOADS: sysdir_search_path_directory_t = 15; - pub const SYSDIR_DIRECTORY_MOVIES: sysdir_search_path_directory_t = 17; - pub const SYSDIR_DIRECTORY_MUSIC: sysdir_search_path_directory_t = 18; - pub const SYSDIR_DIRECTORY_PICTURES: sysdir_search_path_directory_t = 19; - pub const SYSDIR_DIRECTORY_SHARED_PUBLIC: sysdir_search_path_directory_t = 21; - - pub type sysdir_search_path_domain_mask_t = c_uint; - pub const SYSDIR_DOMAIN_MASK_USER: sysdir_search_path_domain_mask_t = 0x0001; - - pub type sysdir_search_path_enumeration_state = c_uint; - - unsafe extern "C" { - pub unsafe fn sysdir_start_search_path_enumeration( - dir: sysdir_search_path_directory_t, - domain_mask: sysdir_search_path_domain_mask_t, - ) -> sysdir_search_path_enumeration_state; - pub unsafe fn sysdir_get_next_search_path_enumeration( - state: sysdir_search_path_enumeration_state, - path: *mut c_char, - ) -> sysdir_search_path_enumeration_state; - } + pub use libc::sysdir_search_path_directory_t::*; + pub use libc::sysdir_search_path_domain_mask_t::*; + pub use libc::{ + PATH_MAX, sysdir_get_next_search_path_enumeration, sysdir_search_path_directory_t, + sysdir_search_path_domain_mask_t, sysdir_search_path_enumeration_state, + sysdir_start_search_path_enumeration, + }; } #[cfg(test)] From d48fa462179f1a58c198f36db1ceca8a60bdeb89 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 15:34:57 -0400 Subject: [PATCH 11/43] skip can_fetch_xdg_user_dirs test on darwin --- library/std/src/os/unix/fs/dirs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 94a4528a12521..a3925cc2c2bb6 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -345,6 +345,7 @@ mod tests { } #[test] + #[cfg_attr(target_vendor = "apple", ignore = "Apple OSes don't use xdg-user-dirs")] fn can_fetch_xdg_user_dirs() { let dirs = UserDirs::xdg_user().unwrap(); From 230559c20058ffc22ca75e23ba821d3465fae2e9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 16:39:03 -0400 Subject: [PATCH 12/43] minor fixes --- library/std/src/os/darwin/fs/dirs.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 70fa6ba35416c..090b398ca5daa 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -89,10 +89,12 @@ pub trait UserDirsExt: Sized + Sealed { fn sysdir() -> io::Result; } +#[unstable(feature = "dir_discovery", issue = "157515")] impl UserDirsExt for UserDirs { fn sysdir() -> io::Result { let mut dirs = UserDirs::new(); - let home = dirs.home_dir().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; + let home = + dirs.user_home().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; let caches = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES) }?; let application_support = @@ -105,8 +107,6 @@ impl UserDirsExt for UserDirs { let pictures = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_PICTURES) }?; let public_share = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_SHARED_PUBLIC) }?; - let mut dirs = UserDirs::empty(); - dirs.home.cache = caches; // Apple puts config/data/state all in Application Support dirs.home.config = application_support.clone(); @@ -126,11 +126,7 @@ impl UserDirsExt for UserDirs { } /// Get the path for a system directory using `sysdir(3)`. -/// -/// # Safety -/// -/// `kind` must be a valid `sysdir_search_path_directory_t` value. -unsafe fn get_user_sysdir( +fn get_user_sysdir( home: &Path, kind: sys::sysdir_search_path_directory_t, ) -> io::Result> { @@ -143,13 +139,13 @@ unsafe fn get_user_sysdir( return Ok(None); } - let mut path = [0 as c_char; sys::PATH_MAX]; - // SAFETY: `state` comes from `sysdir_start_search_path_enumeration` + let mut path = [0 as c_char; sys::PATH_MAX as usize]; + // SAFETY: `state` is nonzero and comes from `sysdir_start_search_path_enumeration` // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX-1` bytes to `path` let state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; if state == 0 { // exactly 1 directory path produced - let Ok(path) = CStr::from_bytes_until_null(&path) else { + let Ok(path) = CStr::from_bytes_until_nul(&path) else { return Err(const_error!( ErrorKind::InvalidData, "standard user directory path too long", @@ -163,7 +159,7 @@ unsafe fn get_user_sysdir( )); }; // expand the `~` shorthand - if path == "~" { + return if path == "~" { Ok(Some(home.into())) } else if path.starts_with("~/") { Ok(Some(home.join(&path[2..]))) @@ -176,7 +172,7 @@ unsafe fn get_user_sysdir( Ok(Some(path.into())) } else { Err(const_error!(ErrorKind::InvalidData, "standard user directory path not absolute")) - } + }; } // 2+ directory paths produced @@ -195,7 +191,6 @@ mod sys { pub use libc::sysdir_search_path_domain_mask_t::*; pub use libc::{ PATH_MAX, sysdir_get_next_search_path_enumeration, sysdir_search_path_directory_t, - sysdir_search_path_domain_mask_t, sysdir_search_path_enumeration_state, sysdir_start_search_path_enumeration, }; } From c8f118ade3da7c2b2153a375a9c2d9f42d215086 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 17:24:47 -0400 Subject: [PATCH 13/43] minor fixes --- library/std/src/os/darwin/fs/dirs.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 090b398ca5daa..322ca1c4f5e8a 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -96,16 +96,16 @@ impl UserDirsExt for UserDirs { let home = dirs.user_home().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; - let caches = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES) }?; + let caches = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES)?; let application_support = - unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_APPLICATION_SUPPORT) }?; - let desktop = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DESKTOP) }?; - let documents = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOCUMENT) }?; - let downloads = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOWNLOADS) }?; - let movies = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MOVIES) }?; - let music = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MUSIC) }?; - let pictures = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_PICTURES) }?; - let public_share = unsafe { get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_SHARED_PUBLIC) }?; + get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_APPLICATION_SUPPORT)?; + let desktop = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DESKTOP)?; + let documents = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOCUMENT)?; + let downloads = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOWNLOADS)?; + let movies = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MOVIES)?; + let music = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MUSIC)?; + let pictures = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_PICTURES)?; + let public_share = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_SHARED_PUBLIC)?; dirs.home.cache = caches; // Apple puts config/data/state all in Application Support @@ -139,10 +139,12 @@ fn get_user_sysdir( return Ok(None); } - let mut path = [0 as c_char; sys::PATH_MAX as usize]; + let mut path = [0u8; sys::PATH_MAX as usize]; // SAFETY: `state` is nonzero and comes from `sysdir_start_search_path_enumeration` // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX-1` bytes to `path` - let state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; + let state = unsafe { + sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr() as *mut c_char) + }; if state == 0 { // exactly 1 directory path produced let Ok(path) = CStr::from_bytes_until_nul(&path) else { From 82aa5c3a2a45d90985d2f99a031c65d7db40fc64 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 10:27:20 -0400 Subject: [PATCH 14/43] allow missing user-dirs.dirs for test --- library/std/src/os/unix/fs/dirs.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index a3925cc2c2bb6..03eb66b965cca 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -347,7 +347,14 @@ mod tests { #[test] #[cfg_attr(target_vendor = "apple", ignore = "Apple OSes don't use xdg-user-dirs")] fn can_fetch_xdg_user_dirs() { - let dirs = UserDirs::xdg_user().unwrap(); + let dirs = match UserDirs::xdg_user() { + 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.cache_home().is_some()); assert!(dirs.config_home().is_some()); From 2f3bec41de322306e8917e3cbc36443303072daf Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 11:05:54 -0400 Subject: [PATCH 15/43] fully cfg gate darwin UserDirsExt --- library/std/src/os/darwin/fs/dirs.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 322ca1c4f5e8a..78d7736c877ab 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -90,6 +90,7 @@ pub trait UserDirsExt: Sized + Sealed { } #[unstable(feature = "dir_discovery", issue = "157515")] +#[cfg(target_vendor = "apple")] impl UserDirsExt for UserDirs { fn sysdir() -> io::Result { let mut dirs = UserDirs::new(); @@ -126,6 +127,7 @@ impl UserDirsExt for UserDirs { } /// Get the path for a system directory using `sysdir(3)`. +#[cfg(target_vendor = "apple")] fn get_user_sysdir( home: &Path, kind: sys::sysdir_search_path_directory_t, @@ -188,6 +190,7 @@ fn get_user_sysdir( Err(const_error!(ErrorKind::InvalidData, "multiple paths returned for standard user directory")) } +#[cfg(target_vendor = "apple")] mod sys { pub use libc::sysdir_search_path_directory_t::*; pub use libc::sysdir_search_path_domain_mask_t::*; @@ -198,6 +201,7 @@ mod sys { } #[cfg(test)] +#[cfg(target_vendor = "apple")] mod tests { use super::*; From 1e279da7e82f8f3ebdf4adab6c645ad85ea362c7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 13:02:23 -0400 Subject: [PATCH 16/43] add missing cast --- library/std/src/os/darwin/fs/dirs.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 78d7736c877ab..4cfa433c3b650 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -184,7 +184,9 @@ fn get_user_sysdir( // exhaust iterator for cleanup; ignore the paths that get returned while state != 0 { // SAFETY: `state` is nonzero and is from a previous call to sysdir_get_next_search_path_enumeration - state = unsafe { sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr()) }; + state = unsafe { + sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr() as *mut c_char) + }; } Err(const_error!(ErrorKind::InvalidData, "multiple paths returned for standard user directory")) From 5c65239a996766fa9f1e8f4e65b9d4c0586403aa Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 14:37:34 -0400 Subject: [PATCH 17/43] fix doc links --- library/std/src/fs/dirs.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 71453c121e495..de3ce514b679d 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -11,8 +11,8 @@ use crate::{env, mem}; /// /// A note of caution, however: multiple of these directory paths may be the /// same as each other, so you should not assume that a file written relative -/// to the [config](Self::config) directory will not conflict with the same -/// relative path in the [data](Self::data) directory, for example. Aliasing +/// to the [config](Self::config_home) directory will not conflict with the same +/// relative path in the [data](Self::data_home) directory, for example. Aliasing /// directories in this way is the common practice of some operating systems, /// so it is important that a program still works correctly even if user paths /// alias each other. @@ -100,6 +100,11 @@ impl UserDirs { /// a default path for file selection dialogs, not for automatically /// accessed file paths. Use one of [`config_home`], [`data_home`], /// [`state_home`], or [`cache_home`] instead as appropriate for the file. + /// + /// [`config_home`]: Self::config_home + /// [`data_home`]: Self::data_home + /// [`state_home`]: Self::state_home + /// [`cache_home`]: Self::cache_home #[unstable(feature = "dir_discovery", issue = "157515")] pub fn user_home(&self) -> Option<&Path> { self.home.user.as_deref() @@ -144,7 +149,7 @@ impl UserDirs { /// /// "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) directory. Common examples include history + /// 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). From 58c5d24d1f6dc1c242f774ae1ae36d23acc5a476 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 15:22:05 -0400 Subject: [PATCH 18/43] fix broken doclinks --- library/std/src/os/darwin/fs/dirs.rs | 2 +- library/std/src/os/unix/fs/dirs.rs | 13 +++++++++---- library/std/src/os/windows/fs/dirs.rs | 11 +++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 4cfa433c3b650..0098cdfb23dbd 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -47,7 +47,7 @@ pub trait UserDirsExt: Sized + Sealed { /// /// Errors if the underlying system directory discovery API returns /// multiple directories for a queried standard directory, if a - /// directory path is not valid Unicode, or if the [user home](home_dir) + /// directory path is not valid Unicode, or if the [user home](UserDirs::user_home) /// cannot be determined. /// /// # Implementation-specific behavior diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 03eb66b965cca..a0e4e4f2e4ea7 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -44,8 +44,8 @@ pub trait UserDirsExt: Sized + Sealed { /// | [`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/`] | + /// | [`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`](home_dir), which uses /// `$HOME` if set and non-empty, but falls back to the system password @@ -97,6 +97,7 @@ pub trait UserDirsExt: Sized + Sealed { /// Errors if the user's home directory cannot be determined or if the /// `$XDG_CONFIG_HOME/user-dirs.dirs` file cannot be read. /// + /// [`xdg_base`]: UserDirsExt::xdg_base /// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ /// [`desktop`]: UserDirs::desktop /// [`documents`]: UserDirs::documents @@ -123,24 +124,28 @@ pub trait UserDirsExt: Sized + Sealed { fn runtime_home(&self) -> Option<&Path>; /// A preference-ordered list of base directories to search for config - /// files *in addition to* [`config_home`](UserDirs::config_home). + /// 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`]: UserDirs::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`](UserDirs::data_home). + /// 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`]: UserDirs::data_home #[unstable(feature = "dir_search_discovery", issue = "157515")] fn data_dirs(&self) -> Option>; diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index a4580e43f9f2a..7274981353719 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -73,6 +73,17 @@ pub trait UserDirsExt: Sized + Sealed { /// [`FOLDERID_Pictures`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_pictures /// [`FOLDERID_Public`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_public /// [`FOLDERID_Videos`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos + /// [`cache_home`]: UserDirs::cache_home + /// [`config_home`]: UserDirs::config_home + /// [`data_home`]: UserDirs::data_home + /// [`state_home`]: UserDirs::state_home + /// [`desktop`]: UserDirs::desktop + /// [`documents`]: UserDirs::documents + /// [`downloads`]: UserDirs::downloads + /// [`music`]: UserDirs::music + /// [`pictures`]: UserDirs::pictures + /// [`public_share`]: UserDirs::public_share + /// [`videos`]: UserDirs::videos #[unstable(feature = "dir_discovery", issue = "157515")] fn known_folders() -> io::Result; } From cb80d923a869dbf7286ed4ed07ea7d18bf259bd7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 16:02:46 -0400 Subject: [PATCH 19/43] fix doclinks --- library/std/src/os/unix/fs/dirs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index a0e4e4f2e4ea7..c53acedff503b 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -44,8 +44,8 @@ pub trait UserDirsExt: Sized + Sealed { /// | [`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/`] | + /// | [`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`](home_dir), which uses /// `$HOME` if set and non-empty, but falls back to the system password From 73d28c31b284a7a8dc983618854effb8acba7044 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 16:43:54 -0400 Subject: [PATCH 20/43] make doc not look like broken doclink --- library/std/src/os/unix/fs/dirs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index c53acedff503b..2f86adb55fbbc 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -44,8 +44,8 @@ pub trait UserDirsExt: Sized + Sealed { /// | [`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/`\] | + /// | [`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`](home_dir), which uses /// `$HOME` if set and non-empty, but falls back to the system password From b33e3c8b69d125706c680199f097abc5a100bab9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:02:43 -0400 Subject: [PATCH 21/43] refactor darwin UserDirsExt --- library/std/src/os/darwin/fs/dirs.rs | 186 ++++++++++++++++----------- 1 file changed, 113 insertions(+), 73 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 0098cdfb23dbd..278b11b9d720e 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -93,20 +93,21 @@ pub trait UserDirsExt: Sized + Sealed { #[cfg(target_vendor = "apple")] impl UserDirsExt for UserDirs { fn sysdir() -> io::Result { + use libc::sysdir_search_path_directory_t::*; + let mut dirs = UserDirs::new(); let home = dirs.user_home().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; - let caches = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_CACHES)?; - let application_support = - get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_APPLICATION_SUPPORT)?; - let desktop = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DESKTOP)?; - let documents = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOCUMENT)?; - let downloads = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_DOWNLOADS)?; - let movies = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MOVIES)?; - let music = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_MUSIC)?; - let pictures = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_PICTURES)?; - let public_share = get_user_sysdir(&home, sys::SYSDIR_DIRECTORY_SHARED_PUBLIC)?; + let caches = sys::get_user_dir(&home, SYSDIR_DIRECTORY_CACHES)?; + let application_support = sys::get_user_dir(&home, SYSDIR_DIRECTORY_APPLICATION_SUPPORT)?; + 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)?; + let public_share = sys::get_user_dir(&home, SYSDIR_DIRECTORY_SHARED_PUBLIC)?; dirs.home.cache = caches; // Apple puts config/data/state all in Application Support @@ -126,80 +127,119 @@ impl UserDirsExt for UserDirs { } } -/// Get the path for a system directory using `sysdir(3)`. +/// Safer wrapper around the sysdir(3) API #[cfg(target_vendor = "apple")] -fn get_user_sysdir( - home: &Path, - kind: sys::sysdir_search_path_directory_t, -) -> io::Result> { - // SAFETY: `kind` is a valid `sysdir_search_path_directory_t` value, and - // `SYSDIR_DOMAIN_MASK_USER` is a valid `sysdir_search_path_domain_mask_t` value. - let state = - unsafe { sys::sysdir_start_search_path_enumeration(kind, sys::SYSDIR_DOMAIN_MASK_USER) }; - if state == 0 { - // 0 directory paths produced - return Ok(None); - } +mod sys { + use crate::io::{self, ErrorKind, const_error}; + use crate::path::{Path, PathBuf}; - let mut path = [0u8; sys::PATH_MAX as usize]; - // SAFETY: `state` is nonzero and comes from `sysdir_start_search_path_enumeration` - // SAFETY: sysdir_get_next_search_path_enumeration will write at most `PATH_MAX-1` bytes to `path` - let state = unsafe { - sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr() as *mut c_char) - }; - if state == 0 { - // exactly 1 directory path produced - let Ok(path) = CStr::from_bytes_until_nul(&path) else { - return Err(const_error!( - ErrorKind::InvalidData, - "standard user directory path too long", - )); + /// 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 { sys::Iter::new(home, kind, SYSDIR_DOMAIN_MASK_USER) }; + let Some(path) = iter.next() else { + return Ok(None); }; - // read as UTF-8 - let Ok(path) = path.to_str() else { + + if iter.next().is_some() { + // more than one path returned (shouldn't happen for SYSDIR_DOMAIN_MASK_USER) return Err(const_error!( ErrorKind::InvalidData, - "standard user directory path not valid UTF-8", + "multiple paths returned for standard user directory", )); - }; - // expand the `~` shorthand - return if path == "~" { - Ok(Some(home.into())) - } else if path.starts_with("~/") { - Ok(Some(home.join(&path[2..]))) - } else if path.starts_with("~") { - Err(const_error!( - ErrorKind::InvalidData, - "standard user directory relative to different user", - )) - } else if path.starts_with('/') { - Ok(Some(path.into())) - } else { - Err(const_error!(ErrorKind::InvalidData, "standard user directory path not absolute")) - }; + } + + Ok(Some(path?)) } - // 2+ directory paths produced - let mut state = state; - // exhaust iterator for cleanup; ignore the paths that get returned - while state != 0 { - // SAFETY: `state` is nonzero and is from a previous call to sysdir_get_next_search_path_enumeration - state = unsafe { - sys::sysdir_get_next_search_path_enumeration(state, path.as_mut_ptr() as *mut c_char) - }; + struct Iter<'a> { + home: &'a Path, + state: libc::sysdir_search_path_enumeration_state, } - Err(const_error!(ErrorKind::InvalidData, "multiple paths returned for standard user directory")) -} + impl Drop for Iter<'_> { + fn drop(&mut self) { + for _ in self {} + } + } -#[cfg(target_vendor = "apple")] -mod sys { - pub use libc::sysdir_search_path_directory_t::*; - pub use libc::sysdir_search_path_domain_mask_t::*; - pub use libc::{ - PATH_MAX, sysdir_get_next_search_path_enumeration, sysdir_search_path_directory_t, - sysdir_start_search_path_enumeration, - }; + 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 libc::c_char, + ) + }; + } + + if self.state == 0 { + // exhausted + return None; + } + + let Ok(path) = std::ffi::CStr::from_bytes_until_nul(&buf) else { + // should be impossible given username length limits, but be defensive + return Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path too long", + )); + }; + + let Ok(path) = path.to_str() else { + // FIXME: is this possible, and if so, how should it be handled? + return Err(const_error!( + ErrorKind::InvalidData, + "standard user directory path not valid UTF-8", + )); + }; + + // expand `~` shorthand + Some(match path { + "~" => Ok(home.into()), + _ if path.starts_with("~/") => Ok(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)] From 4caf68d623e904e79866ea454d255941553ac4af Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:05:42 -0400 Subject: [PATCH 22/43] use objc names in darwin UserDirsExt docs --- library/std/src/os/darwin/fs/dirs.rs | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 278b11b9d720e..c73db9f41e615 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -23,19 +23,19 @@ pub trait UserDirsExt: Sized + Sealed { /// /// The loaded common directories are: /// - /// | `UserDirs` | [`SearchPathDirectory`] | + /// | `UserDirs` | [`NSSearchPathDirectory`] | /// | ---------- | ----------------------- | - /// | [`cache_home`] | [`.cachesDirectory`] (`~/Library/Caches`) | - /// | [`config_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | - /// | [`data_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | - /// | [`state_home`] | [`.applicationSupportDirectory`] (`~/Library/Application Support`) | - /// | [`desktop`] | [`.desktopDirectory`] (`~/Desktop`) | - /// | [`documents`] | [`.documentDirectory`] (`~/Documents`) | - /// | [`downloads`] | [`.downloadsDirectory`] | - /// | [`music`] | [`.musicDirectory`] (`~/Music`) | - /// | [`pictures`] | [`.picturesDirectory`] (`~/Pictures`) | - /// | [`public_share`] | [`.sharedPublicDirectory`] (`~/Public`) | - /// | [`videos`] | [`.moviesDirectory`] (`~/Movies`) | + /// | [`cache_home`] | [`NSCachesDirectory`] (`~/Library/Caches`) | + /// | [`config_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`data_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`state_home`] | [`NSApplicationSupportDirectory`] (`~/Library/Application Support`) | + /// | [`desktop`] | [`NSDesktopDirectory`] (`~/Desktop`) | + /// | [`documents`] | [`NSDocumentDirectory`] (`~/Documents`) | + /// | [`downloads`] | [`NSDownloadsDirectory`] | + /// | [`music`] | [`NSMusicDirectory`] (`~/Music`) | + /// | [`pictures`] | [`NSPicturesDirectory`] (`~/Pictures`) | + /// | [`public_share`] | [`NSSharedPublicDirectory`] (`~/Public`) | + /// | [`videos`] | [`NSMoviesDirectory`] (`~/Movies`) | /// /// Note that the Application Support directory is used for the config, /// data, and state directories. It is always possible for multiple user @@ -75,16 +75,16 @@ pub trait UserDirsExt: Sized + Sealed { /// [`public_share`]: UserDirs::public_share /// [`videos`]: UserDirs::videos /// - /// [`SearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory - /// [`.cachesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory - /// [`.applicationSupportDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory - /// [`.desktopDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/desktopdirectory - /// [`.documentDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/documentdirectory - /// [`.downloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory - /// [`.musicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory - /// [`.picturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory - /// [`.sharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory - /// [`.moviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory + /// [`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 + /// [`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 = "dir_discovery", issue = "157515")] fn sysdir() -> io::Result; } From 0640b15a38a22055a57e344fe5a8e6abf2831b79 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:37:34 -0400 Subject: [PATCH 23/43] add more UserDirs docs --- library/std/src/fs/dirs.rs | 203 ++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index de3ce514b679d..31d01cf09dc2e 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -118,6 +118,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.home.cache.as_deref() @@ -127,8 +145,25 @@ impl UserDirs { /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirs + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.home.config.as_deref() @@ -139,6 +174,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirs + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.home.data.as_deref() @@ -156,6 +209,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirs + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.home.state.as_deref() @@ -166,6 +237,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirs + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.desktop.as_deref() @@ -176,6 +265,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirs + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.documents.as_deref() @@ -186,6 +293,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.downloads.as_deref() @@ -196,6 +321,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.music.as_deref() @@ -206,6 +349,28 @@ impl UserDirs { /// /// 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_PUBLICSHARE_DIR` (`$HOME/Public`) | + /// | [Darwin] (macOS) | [`NSSharedPublicDirectory`] (`$HOME/Public`) | + /// | [Windows] | [`{FOLDERID_Public}`] (`%PUBLIC%`)[^win] | + /// + /// Other paths can be configured via [`set_public_share`](Self::set_public_share). + /// + /// [^win]: On Windows, the standard `%PUBLIC%` is a separate user folder, + /// unlike other OSes where it is a subdirectory of the user's home. + /// This is only particularly meaningful on multi-user systems. + /// + /// [XDG]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`NSSharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory?language=objc + /// [`{FOLDERID_Public}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_public #[unstable(feature = "media_dir_discovery", issue = "157515")] pub fn public_share(&self) -> Option<&Path> { self.media.public_share.as_deref() @@ -216,6 +381,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.pictures.as_deref() @@ -226,6 +409,24 @@ impl UserDirs { /// /// 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]: std::os::unix::fs::dirs::UserDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt + /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [`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.media.videos.as_deref() From 6a6b33f3a2fa0fb67c4a487729bfdb24b83a53e2 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:38:32 -0400 Subject: [PATCH 24/43] add user_home to UserDirs tests --- library/std/src/fs/dirs.rs | 3 +++ library/std/src/os/darwin/fs/dirs.rs | 1 + library/std/src/os/unix/fs/dirs.rs | 2 ++ library/std/src/os/windows/fs/dirs.rs | 1 + 4 files changed, 7 insertions(+) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 31d01cf09dc2e..6b95a076918f1 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -553,6 +553,7 @@ mod tests { fn test_user_dirs_field_hookup_matches() { let mut dirs = UserDirs::empty(); + assert_eq!(dirs.user_home(), None); assert_eq!(dirs.config_home(), None); assert_eq!(dirs.data_home(), None); assert_eq!(dirs.state_home(), None); @@ -566,6 +567,7 @@ mod tests { assert_eq!(dirs.public_share(), None); assert_eq!(dirs.videos(), None); + dirs.set_user_home("/home".into()); dirs.set_config_home("/config".into()); dirs.set_data_home("/data".into()); dirs.set_state_home("/state".into()); @@ -579,6 +581,7 @@ mod tests { dirs.set_public_share("/public_share".into()); dirs.set_videos("/videos".into()); + assert_eq!(dirs.user_home(), Some("/home".as_ref())); 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())); diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index c73db9f41e615..6b419d22eb8bd 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -250,6 +250,7 @@ mod tests { #[test] fn can_fetch_sysdir_paths() { let dirs = UserDirs::sysdir().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()); diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 2f86adb55fbbc..4e4cb1a775d64 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -331,6 +331,7 @@ mod tests { fn can_fetch_xdg_base_dirs() { let dirs = UserDirs::xdg_base().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()); @@ -361,6 +362,7 @@ mod tests { Err(e) => panic!("failed to fetch xdg user dirs: {e:?}"), }; + assert!(dirs.user_home().is_some()); assert!(dirs.cache_home().is_some()); assert!(dirs.config_home().is_some()); assert!(dirs.data_home().is_some()); diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 7274981353719..1b1bcd2380ac7 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -179,6 +179,7 @@ mod tests { #[test] fn can_fetch_known_folder_paths() { let dirs = UserDirs::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()); From 6a73485232af5a22bf9d58ad687dfa024f53e9b9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 16:21:51 -0400 Subject: [PATCH 25/43] fix darwin sysdir iter --- library/std/src/os/darwin/fs/dirs.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 6b419d22eb8bd..57074b27b2855 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -204,24 +204,24 @@ mod sys { let Ok(path) = std::ffi::CStr::from_bytes_until_nul(&buf) else { // should be impossible given username length limits, but be defensive - return Err(const_error!( + return Some(Err(const_error!( ErrorKind::InvalidData, "standard user directory path too long", - )); + ))); }; let Ok(path) = path.to_str() else { // FIXME: is this possible, and if so, how should it be handled? - return Err(const_error!( + return Some(Err(const_error!( ErrorKind::InvalidData, "standard user directory path not valid UTF-8", - )); + ))); }; // expand `~` shorthand Some(match path { - "~" => Ok(home.into()), - _ if path.starts_with("~/") => Ok(home.join(&path[2..])), + "~" => 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", From 46ea0154f7a8a997306d4a0fb3b58921109d0b80 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 17:34:38 -0400 Subject: [PATCH 26/43] fix darwin UserDirsExt typo --- library/std/src/os/darwin/fs/dirs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 57074b27b2855..6fbd2773001c5 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -141,7 +141,7 @@ mod sys { 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 { sys::Iter::new(home, kind, SYSDIR_DOMAIN_MASK_USER) }; + let mut iter = unsafe { Iter::new(home, kind, SYSDIR_DOMAIN_MASK_USER) }; let Some(path) = iter.next() else { return Ok(None); }; From 76ddaa3b91f84917a4a2838010a240c4d7b4dfbc Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:26:22 -0400 Subject: [PATCH 27/43] split std:;fs::UserDirs --- library/std/src/fs.rs | 4 +- library/std/src/fs/dirs.rs | 354 ++++++++++++-------------- library/std/src/os/darwin/fs.rs | 4 +- library/std/src/os/darwin/fs/dirs.rs | 173 ++++++++----- library/std/src/os/unix/fs.rs | 4 +- library/std/src/os/unix/fs/dirs.rs | 326 ++++++++++++------------ library/std/src/os/windows/fs.rs | 4 +- library/std/src/os/windows/fs/dirs.rs | 269 +++++++++++-------- library/std/src/sys/fs/mod.rs | 6 + library/std/src/sys/fs/unix.rs | 12 + 10 files changed, 623 insertions(+), 533 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 59f4529bb3370..a6eabf76ca345 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -51,7 +51,9 @@ use crate::{error, fmt}; pub(crate) mod dirs; #[unstable(feature = "dir_discovery", issue = "157515")] -pub use self::dirs::UserDirs; +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. /// diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 6b95a076918f1..88987439f3b20 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -1,113 +1,69 @@ -use crate::ffi::OsString; +use crate::mem; use crate::path::{Path, PathBuf}; -use crate::{env, mem}; +use crate::sys::fs::{ExtraHomeDirs, ExtraMediaDirs}; -/// A set of known user-specific directories. +/// Common user directory paths used for user-specific application files. /// -/// Most operating systems provide some way to discover the paths to some set -/// of "well-known" directories that it is recommended for applications to use -/// for files accessed at runtime that are not part of the application itself. -/// `UserDirs` provides an OS-agnostic way to manipulate these directories. +/// 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. /// -/// A note of caution, however: multiple of these directory paths may be the -/// same as each other, so you should not assume that a file written relative -/// to the [config](Self::config_home) directory will not conflict with the same -/// relative path in the [data](Self::data_home) directory, for example. Aliasing -/// directories in this way is the common practice of some operating systems, -/// so it is important that a program still works correctly even if user paths -/// alias each other. -/// -/// It is not required that the user directories are for 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 user-specific directories varries between -/// operating systems, constructors for `UserDirs` that discover the host OS's +/// 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 UserDirs { - pub(crate) home: UserHomeDirs, - pub(crate) media: UserMediaDirs, - #[allow(dead_code)] // only used for XDG - pub(crate) search: UserSearchDirs, -} - -#[derive(Debug, Default, Clone)] -pub(crate) struct UserHomeDirs { - pub(crate) user: Option, +pub struct HomeDirs { pub(crate) cache: Option, pub(crate) config: Option, pub(crate) data: Option, pub(crate) state: Option, - #[allow(dead_code)] // only used for XDG - pub(crate) runtime: Option, + pub(crate) extra: ExtraHomeDirs, } -#[derive(Debug, Default, Clone)] -pub(crate) struct UserMediaDirs { +/// 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) public_share: Option, pub(crate) videos: Option, - #[allow(dead_code)] // only used for XDG - pub(crate) templates: Option, -} - -#[derive(Debug, Default, Clone)] -pub(crate) struct UserSearchDirs { - #[allow(dead_code)] // only used for XDG - pub(crate) config: Option, - #[allow(dead_code)] // only used for XDG - pub(crate) data: Option, + pub(crate) extra: ExtraMediaDirs, } -impl UserDirs { - /// Create a known user directory set with only [`user_home`] set. - /// - /// Unlike [`env::home_dir`], this treats an empty home path as `None`. - /// - /// This is useful with the builder `set_*` methods to create a `UserDirs` - /// with exactly the directories you want, without any other defaults. - /// - /// [`user_home`]: Self::user_home - #[unstable(feature = "dir_discovery", issue = "157515")] - pub fn new() -> Self { - let mut dirs = Self::empty(); - dirs.home.user = env::home_dir().filter(|p| !p.is_empty()); - dirs - } - +impl HomeDirs { /// Create a known user directory set with no known directories. /// - /// This is useful with the builder `set_*` methods to create a `UserDirs` + /// 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 { home: Default::default(), media: Default::default(), search: Default::default() } - } - - /// The path to the user's home directory. - /// - /// Applications putting files in the user's home directory without being - /// directed to is generally discouraged. This should typically be used as - /// a default path for file selection dialogs, not for automatically - /// accessed file paths. Use one of [`config_home`], [`data_home`], - /// [`state_home`], or [`cache_home`] instead as appropriate for the file. - /// - /// [`config_home`]: Self::config_home - /// [`data_home`]: Self::data_home - /// [`state_home`]: Self::state_home - /// [`cache_home`]: Self::cache_home - #[unstable(feature = "dir_discovery", issue = "157515")] - pub fn user_home(&self) -> Option<&Path> { - self.home.user.as_deref() + Self { cache: None, config: None, data: None, state: None, extra: Default::default() } } /// A base directory relative to which user-specific non-essential cache @@ -131,14 +87,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_cache_home`](Self::set_cache_home). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.home.cache.as_deref() + self.cache.as_deref() } /// A base directory relative to which user-specific configuration files @@ -159,14 +115,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_config_home`](Self::set_config_home). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirs - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.home.config.as_deref() + self.config.as_deref() } /// A base directory relative to which user-specific data files should be @@ -187,14 +143,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_data_home`](Self::set_data_home). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirs - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.home.data.as_deref() + self.data.as_deref() } /// A base directory relative to which user-specific state files should be @@ -222,14 +178,33 @@ impl UserDirs { /// /// Other paths can be configured via [`set_state_home`](Self::set_state_home). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirs - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.home.state.as_deref() + 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` @@ -250,14 +225,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_desktop`](Self::set_desktop). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirs - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.desktop.as_deref() + self.desktop.as_deref() } /// The OS-privileged user "Documents" directory, often the `Documents` @@ -278,14 +253,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_documents`](Self::set_documents). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirs - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.documents.as_deref() + self.documents.as_deref() } /// The OS-privileged user "Downloads" directory, often the `Downloads` @@ -306,14 +281,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_downloads`](Self::set_downloads). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.downloads.as_deref() + self.downloads.as_deref() } /// The OS-privileged user "Music" directory, often the `Music` @@ -334,46 +309,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_music`](Self::set_music). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.music.as_deref() - } - - /// The OS-privileged user "Public Share" directory, often the `Public` - /// 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_PUBLICSHARE_DIR` (`$HOME/Public`) | - /// | [Darwin] (macOS) | [`NSSharedPublicDirectory`] (`$HOME/Public`) | - /// | [Windows] | [`{FOLDERID_Public}`] (`%PUBLIC%`)[^win] | - /// - /// Other paths can be configured via [`set_public_share`](Self::set_public_share). - /// - /// [^win]: On Windows, the standard `%PUBLIC%` is a separate user folder, - /// unlike other OSes where it is a subdirectory of the user's home. - /// This is only particularly meaningful on multi-user systems. - /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt - /// [`NSSharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory?language=objc - /// [`{FOLDERID_Public}`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_public - #[unstable(feature = "media_dir_discovery", issue = "157515")] - pub fn public_share(&self) -> Option<&Path> { - self.media.public_share.as_deref() + self.music.as_deref() } /// The OS-privileged user "Pictures" directory, often the `Pictures` @@ -394,14 +337,14 @@ impl UserDirs { /// /// Other paths can be configured via [`set_pictures`](Self::set_pictures). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.pictures.as_deref() + self.pictures.as_deref() } /// The OS-privileged user "Videos" directory, often the `Videos` @@ -422,33 +365,33 @@ impl UserDirs { /// /// Other paths can be configured via [`set_videos`](Self::set_videos). /// - /// [XDG]: std::os::unix::fs::dirs::UserDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::UserDirsExt - /// [Windows]: std::os::windows::fs::dirs::UserDirsExt + /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt + /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt + /// [Windows]: std::os::windows::fs::dirs::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.media.videos.as_deref() + self.videos.as_deref() } } -impl UserDirs { +impl HomeDirs { /// Take the contents of this directory set, leaving it empty. /// - /// This is useful with the builder `set_*` methods to build `UserDirs` + /// This is useful with the builder `set_*` methods to build `HomeDirs` /// without needing an intermediate mutable binding. /// /// # Examples /// /// ``` /// #![feature(dir_discovery)] - /// use std::fs::UserDirs; + /// use std::fs::HomeDirs; /// /// # /* /// let base_dir = /* ... */; /// # */ let base_dir = std::path::PathBuf::from("/"); - /// let dirs = UserDirs::empty() + /// 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")) @@ -460,87 +403,103 @@ impl UserDirs { mem::replace(self, Self::empty()) } - /// Set the path for [Self::user_home]. - #[unstable(feature = "dir_discovery", issue = "157515")] - pub fn set_user_home(&mut self, path: PathBuf) -> &mut Self { - self.home.user = Some(path); - self - } - /// 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.home.cache = Some(path); + 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.home.config = Some(path); + 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.home.data = Some(path); + 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.home.state = Some(path); + 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.media.desktop = Some(path); + 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.media.documents = Some(path); + 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.media.downloads = Some(path); + 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.media.music = Some(path); + 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.media.pictures = Some(path); - self - } - - /// Set the path for [Self::public_share]. - #[unstable(feature = "media_dir_discovery", issue = "157515")] - pub fn set_public_share(&mut self, path: PathBuf) -> &mut Self { - self.media.public_share = Some(path); + 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.media.videos = Some(path); + self.videos = Some(path); self } } @@ -550,49 +509,48 @@ mod tests { use super::*; #[test] - fn test_user_dirs_field_hookup_matches() { - let mut dirs = UserDirs::empty(); + fn test_home_dirs_field_hookup_matches() { + let mut dirs = HomeDirs::empty(); - assert_eq!(dirs.user_home(), None); 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.public_share(), None); assert_eq!(dirs.videos(), None); - dirs.set_user_home("/home".into()); - dirs.set_config_home("/config".into()); - dirs.set_data_home("/data".into()); - dirs.set_state_home("/state".into()); - dirs.set_cache_home("/cache".into()); - 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_public_share("/public_share".into()); dirs.set_videos("/videos".into()); - assert_eq!(dirs.user_home(), Some("/home".as_ref())); - 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())); - 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.public_share(), Some("/public_share".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 ba36ca55011b5..a7a76ed80b7e1 100644 --- a/library/std/src/os/darwin/fs.rs +++ b/library/std/src/os/darwin/fs.rs @@ -10,7 +10,9 @@ use crate::time::SystemTime; mod dirs; #[unstable(feature = "dir_discovery", issue = "157515")] -pub use self::dirs::UserDirsExt; +pub use self::dirs::HomeDirsExt; +#[unstable(feature = "media_dir_discovery", issue = "157515")] +pub use self::dirs::MediaDirsExt; /// OS-specific extensions to [`fs::Metadata`]. /// diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 6fbd2773001c5..a57b7096b77f2 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -1,41 +1,36 @@ +use crate::env; use crate::ffi::{CStr, c_char}; -use crate::fs::UserDirs; +use crate::fs::{HomeDirs, MediaDirs}; use crate::io::{self, ErrorKind, const_error}; use crate::path::{Path, PathBuf}; trait Sealed {} -impl Sealed for UserDirs {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} -/// Darwin-specific extensions to [`fs::UserDirs`](UserDirs). +/// Darwin-specific extensions to [`fs::HomeDirs`](HomeDirs). #[unstable(feature = "dir_discovery", issue = "157515")] #[expect(private_bounds, reason = "sealed")] -pub trait UserDirsExt: Sized + 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, - /// [`cache_home`], [`config_home`], [`data_home`], and [`state_home`] - /// are within the application's sandbox bundle. Outside the sandbox, - /// these are subdirectories of the `~/Library` directory on macOS. + /// 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 user directory is accessible there. + /// paths as long as the directory is accessible there. /// /// The loaded common directories are: /// - /// | `UserDirs` | [`NSSearchPathDirectory`] | + /// | `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`) | - /// | [`desktop`] | [`NSDesktopDirectory`] (`~/Desktop`) | - /// | [`documents`] | [`NSDocumentDirectory`] (`~/Documents`) | - /// | [`downloads`] | [`NSDownloadsDirectory`] | - /// | [`music`] | [`NSMusicDirectory`] (`~/Music`) | - /// | [`pictures`] | [`NSPicturesDirectory`] (`~/Pictures`) | - /// | [`public_share`] | [`NSSharedPublicDirectory`] (`~/Public`) | - /// | [`videos`] | [`NSMoviesDirectory`] (`~/Movies`) | /// /// Note that the Application Support directory is used for the config, /// data, and state directories. It is always possible for multiple user @@ -45,39 +40,82 @@ pub trait UserDirsExt: Sized + Sealed { /// /// # Errors /// - /// Errors if the underlying system directory discovery API returns - /// multiple directories for a queried standard directory, if a - /// directory path is not valid Unicode, or if the [user home](UserDirs::user_home) - /// cannot be determined. + /// 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. Iterates each of `SYSDIR_DIRECTORY_DOCUMENT`, - /// `SYSDIR_DIRECTORY_DESKTOP`, `SYSDIR_DIRECTORY_CACHES`, - /// `SYSDIR_DIRECTORY_APPLICATION_SUPPORT`, `SYSDIR_DIRECTORY_DOWNLOADS`, - /// `SYSDIR_DIRECTORY_MOVIES`, `SYSDIR_DIRECTORY_MUSIC`, - /// `SYSDIR_DIRECTORY_PICTURES`, `SYSDIR_DIRECTORY_SHARED_PUBLIC` once. + /// 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`]: UserDirs::cache_home - /// [`config_home`]: UserDirs::config_home - /// [`data_home`]: UserDirs::data_home - /// [`state_home`]: UserDirs::state_home - /// [`desktop`]: UserDirs::desktop - /// [`documents`]: UserDirs::documents - /// [`downloads`]: UserDirs::downloads - /// [`music`]: UserDirs::music - /// [`pictures`]: UserDirs::pictures - /// [`public_share`]: UserDirs::public_share - /// [`videos`]: UserDirs::videos + /// [`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 + /// [`public_share`]: MediaDirs::public_share + /// [`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 @@ -85,43 +123,60 @@ pub trait UserDirsExt: Sized + Sealed { /// [`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 = "dir_discovery", issue = "157515")] + #[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 UserDirsExt for UserDirs { +impl HomeDirsExt for HomeDirs { fn sysdir() -> io::Result { use libc::sysdir_search_path_directory_t::*; - let mut dirs = UserDirs::new(); - let home = - dirs.user_home().ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; + 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)?; - let public_share = sys::get_user_dir(&home, SYSDIR_DIRECTORY_SHARED_PUBLIC)?; - dirs.home.cache = caches; - // Apple puts config/data/state all in Application Support - dirs.home.config = application_support.clone(); - dirs.home.data = application_support.clone(); - dirs.home.state = application_support; - - dirs.media.desktop = desktop; - dirs.media.documents = documents; - dirs.media.downloads = downloads; - dirs.media.music = music; - dirs.media.pictures = pictures; - dirs.media.public_share = public_share; - dirs.media.videos = movies; + dirs.desktop = desktop; + dirs.documents = documents; + dirs.downloads = downloads; + dirs.music = music; + dirs.pictures = pictures; + dirs.videos = movies; Ok(dirs) } @@ -211,7 +266,7 @@ mod sys { }; let Ok(path) = path.to_str() else { - // FIXME: is this possible, and if so, how should it be handled? + // 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", @@ -249,18 +304,18 @@ mod tests { #[test] fn can_fetch_sysdir_paths() { - let dirs = UserDirs::sysdir().unwrap(); - assert!(dirs.user_home().is_some()); + 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.public_share().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 8e8e94458cb27..cf0176bbbe759 100644 --- a/library/std/src/os/unix/fs.rs +++ b/library/std/src/os/unix/fs.rs @@ -24,7 +24,9 @@ mod tests; mod dirs; #[unstable(feature = "dir_discovery", issue = "157515")] -pub use self::dirs::UserDirsExt; +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")] diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 4e4cb1a775d64..4a5f7d5a3421b 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -2,19 +2,20 @@ use crate::env::{JoinPathsError, SplitPaths, home_dir, join_paths, split_paths, var_os}; use crate::ffi::{OsStr, OsString}; -use crate::fs::{self, UserDirs}; +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 UserDirs {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} -/// XDG-specific extensions to [`fs::UserDirs`](UserDirs). +/// 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] and the [xdg-user-dirs] -/// tool. These conventions have been largely adopted by Linux distributions. +/// [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`. @@ -24,18 +25,15 @@ impl Sealed for UserDirs {} /// follow along with any legacy path compatibility you might need to support. /// /// [xdg-basedir]: https://specifications.freedesktop.org/basedir/ -/// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ #[unstable(feature = "dir_discovery", issue = "157515")] #[expect(private_bounds, reason = "sealed")] -pub trait UserDirsExt: Sized + Sealed { +pub trait HomeDirsExt: Sized + Sealed { /// Load the user directory paths according to the /// [XDG Base Directory Specification][xdg-basedir]. /// - /// Sets [`cache_home`], [`config_home`], [`data_home`], and - /// [`state_home`], as well as the XDG-specific [`runtime_home`], - /// [`config_dirs`], and [`data_dirs`]. Each of these are set to the value - /// of their corresponding `XDG_*` environment variable if it is set and - /// non-empty, else to the default value defined by the specification. + /// 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 | /// | ----- | -------------------- | ------------- | @@ -50,65 +48,22 @@ pub trait UserDirsExt: Sized + Sealed { /// Note that `$HOME` here means [`env::home_dir`](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 common to both the shell - /// and the `xdg-user-dirs` tool. + /// `$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`]: UserDirs::cache_home - /// [`config_home`]: UserDirs::config_home - /// [`data_home`]: UserDirs::data_home - /// [`state_home`]: UserDirs::state_home - /// [`runtime_home`]: UserDirsExt::runtime_home - /// [`config_dirs`]: UserDirsExt::config_dirs - /// [`data_dirs`]: UserDirsExt::data_dirs + /// [`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_base() -> io::Result; - - /// Load the user directory paths according to the xdg-user-dirs tool. - /// - /// In addition to the base directories set by [`xdg_base`], this also - /// reads the `$XDG_CONFIG_HOME/user-dirs.dirs` file as defined by the - /// [xdg-user-dirs] tool to set [`desktop`], [`documents`], [`downloads`], - /// [`music`], [`pictures`], [`public_share`], and [`videos`], as well as - /// the XDG-specific [`templates`]. - /// - /// `user-dirs.dirs` uses "a shell format, so it's easy to access from a - /// shell script," and the way the [xdg-user-dirs] tool suggests loading - /// the configuration is to `source` the file in a shell. Instead of using - /// the shell (and potentially executing arbitrary code), we directly read - /// and parse the file. - /// - /// Only lines in the `XDG_{NAME}_DIR={path}` format are processed; all - /// other lines are ignored. `{NAME}` is a known directory name defined by - /// the xdg-user-dirs tool, and `{path}` is a double-quoted shell-escaped - /// path; any line that does not match this format is silently ignored. - /// Additionally, we follow the documentation and only expand a leading - /// `$HOME/` prefix in the path; other shell expansions may work in other - /// tools, but will be unexpanded in the returned path here if present. - /// Furthermore, paths which are neither rooted nor relative to `$HOME` - /// are ignored, as they are not valid according to the specification. - /// - /// # Errors - /// - /// Errors if the user's home directory cannot be determined or if the - /// `$XDG_CONFIG_HOME/user-dirs.dirs` file cannot be read. - /// - /// [`xdg_base`]: UserDirsExt::xdg_base - /// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ - /// [`desktop`]: UserDirs::desktop - /// [`documents`]: UserDirs::documents - /// [`downloads`]: UserDirs::downloads - /// [`music`]: UserDirs::music - /// [`pictures`]: UserDirs::pictures - /// [`public_share`]: UserDirs::public_share - /// [`videos`]: UserDirs::videos - /// [`templates`]: UserDirsExt::templates - #[unstable(feature = "media_dir_discovery", issue = "157515")] - fn xdg_user() -> io::Result; + fn xdg() -> io::Result; /// A base directory relative to which user-specific runtime files /// (such as sockets, named pipes, etc) should be stored. @@ -132,7 +87,7 @@ pub trait UserDirsExt: Sized + Sealed { /// necessarily present in this list, and is considered more important /// than any base directory in this list. /// - /// [`config_home`]: UserDirs::config_home + /// [`config_home`]: HomeDirs::config_home #[unstable(feature = "dir_search_discovery", issue = "157515")] fn config_dirs(&self) -> Option>; @@ -145,18 +100,10 @@ pub trait UserDirsExt: Sized + Sealed { /// necessarily present in this list, and is considered more important /// than any base directory in this list. /// - /// [`data_home`]: UserDirs::data_home + /// [`data_home`]: HomeDirs::data_home #[unstable(feature = "dir_search_discovery", issue = "157515")] fn data_dirs(&self) -> Option>; - /// 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::runtime_home]. #[unstable(feature = "dir_discovery", issue = "157515")] fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self; @@ -174,6 +121,74 @@ pub trait UserDirsExt: Sized + Sealed { &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")] +#[cfg(unix)] +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`](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")] @@ -181,29 +196,67 @@ pub trait UserDirsExt: Sized + Sealed { } #[unstable(feature = "dir_discovery", issue = "157515")] -impl UserDirsExt for UserDirs { - fn xdg_base() -> io::Result { - let mut dirs = Self::new(); - let user_home = home_dir() - .filter(|p| !p.is_empty()) - .ok_or(const_error!(ErrorKind::NotFound, "no home directory"))?; - - dirs.home.cache = Some(xdg_dir("XDG_CACHE_HOME", || user_home.join(".cache"))); - dirs.home.config = Some(xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config"))); - dirs.home.data = Some(xdg_dir("XDG_DATA_HOME", || user_home.join(".local/share"))); - dirs.home.state = Some(xdg_dir("XDG_STATE_HOME", || user_home.join(".local/state"))); - dirs.home.runtime = var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()).map(PathBuf::from); - - dirs.search.config = Some(xdg_env("XDG_CONFIG_DIRS", "/etc/xdg")); - dirs.search.data = Some(xdg_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")); +#[cfg(unix)] +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 xdg_user() -> io::Result { - let mut dirs = Self::xdg_base()?; + 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) + } - let spec = match fs::read(dirs.config_home().unwrap().join("user-dirs.dirs")) { + 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")] +#[cfg(unix)] +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!( @@ -232,10 +285,7 @@ impl UserDirsExt for UserDirs { let buffer; const HOME_RELATIVE_PREFIX: &[u8] = b"\"$HOME/"; let expanded = if val.starts_with(HOME_RELATIVE_PREFIX) { - let joined = dirs - .user_home() - .unwrap() - .join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); + let joined = user_home.join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); buffer = OsString::from_iter([OsStr::new("\""), joined.as_os_str()]); buffer.as_bytes() } else { @@ -251,14 +301,13 @@ impl UserDirsExt for UserDirs { // load the known user directories match var { - b"XDG_DESKTOP_DIR" => dirs.media.desktop = Some(path_from_bytes(&path)), - b"XDG_DOCUMENTS_DIR" => dirs.media.documents = Some(path_from_bytes(&path)), - b"XDG_DOWNLOAD_DIR" => dirs.media.downloads = Some(path_from_bytes(&path)), - b"XDG_MUSIC_DIR" => dirs.media.music = Some(path_from_bytes(&path)), - b"XDG_PICTURES_DIR" => dirs.media.pictures = Some(path_from_bytes(&path)), - b"XDG_PUBLICSHARE_DIR" => dirs.media.public_share = Some(path_from_bytes(&path)), - b"XDG_VIDEOS_DIR" => dirs.media.videos = Some(path_from_bytes(&path)), - b"XDG_TEMPLATES_DIR" => dirs.media.templates = Some(path_from_bytes(&path)), + b"XDG_DESKTOP_DIR" => dirs.desktop = Some(path_from_bytes(&path)), + b"XDG_DOCUMENTS_DIR" => dirs.documents = Some(path_from_bytes(&path)), + b"XDG_DOWNLOAD_DIR" => dirs.downloads = Some(path_from_bytes(&path)), + b"XDG_MUSIC_DIR" => dirs.music = Some(path_from_bytes(&path)), + b"XDG_PICTURES_DIR" => dirs.pictures = Some(path_from_bytes(&path)), + b"XDG_VIDEOS_DIR" => dirs.videos = Some(path_from_bytes(&path)), + b"XDG_TEMPLATES_DIR" => dirs.extra.templates = Some(path_from_bytes(&path)), _ => { // ignore unknown variable assignment, matching shell permissiveness } @@ -268,49 +317,22 @@ impl UserDirsExt for UserDirs { Ok(dirs) } - fn runtime_home(&self) -> Option<&Path> { - self.home.runtime.as_deref() - } - - fn config_dirs(&self) -> Option> { - self.search.config.as_ref().map(|s| split_paths(s)) - } - - fn data_dirs(&self) -> Option> { - self.search.data.as_ref().map(|s| split_paths(s)) - } - fn templates(&self) -> Option<&Path> { - self.media.templates.as_deref() - } - - fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self { - self.home.runtime = Some(path); - self - } - - fn set_config_dirs( - &mut self, - paths: impl IntoIterator>, - ) -> Result<&mut Self, JoinPathsError> { - self.search.config = Some(join_paths(paths)?); - Ok(self) - } - - fn set_data_dirs( - &mut self, - paths: impl IntoIterator>, - ) -> Result<&mut Self, JoinPathsError> { - self.search.data = Some(join_paths(paths)?); - Ok(self) + self.extra.templates.as_deref() } fn set_templates(&mut self, path: PathBuf) -> &mut Self { - self.media.templates = Some(path); + self.extra.templates = Some(path); self } } +fn user_home() -> io::Result { + 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) } @@ -329,9 +351,8 @@ mod tests { #[test] fn can_fetch_xdg_base_dirs() { - let dirs = UserDirs::xdg_base().unwrap(); + let dirs = HomeDirs::xdg().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()); @@ -339,21 +360,12 @@ mod tests { // dirs.runtime() may not exist assert!(dirs.config_dirs().is_some()); assert!(dirs.data_dirs().is_some()); - - assert!(dirs.desktop().is_none()); - assert!(dirs.documents().is_none()); - assert!(dirs.downloads().is_none()); - assert!(dirs.music().is_none()); - assert!(dirs.pictures().is_none()); - assert!(dirs.public_share().is_none()); - assert!(dirs.videos().is_none()); - assert!(dirs.templates().is_none()); } #[test] #[cfg_attr(target_vendor = "apple", ignore = "Apple OSes don't use xdg-user-dirs")] - fn can_fetch_xdg_user_dirs() { - let dirs = match UserDirs::xdg_user() { + 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 @@ -362,21 +374,11 @@ mod tests { Err(e) => panic!("failed to fetch xdg user dirs: {e:?}"), }; - assert!(dirs.user_home().is_some()); - 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()); - 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.public_share().is_some()); assert!(dirs.videos().is_some()); assert!(dirs.templates().is_some()); } diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index a5f19628c148d..afae587bac5e8 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -14,7 +14,9 @@ use crate::{io, sys}; mod dirs; #[unstable(feature = "dir_discovery", issue = "157515")] -pub use self::dirs::UserDirsExt; +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")] diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 1b1bcd2380ac7..3c9b425d2a517 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -1,48 +1,76 @@ -use crate::fs::UserDirs; +use crate::fs::{HomeDirs, MediaDirs}; use crate::io; -#[cfg(windows)] -use crate::{ - io::{ErrorKind, const_error}, - path::PathBuf, - ptr, slice, - sys::{c, os2path}, -}; trait Sealed {} -impl Sealed for UserDirs {} +impl Sealed for HomeDirs {} +impl Sealed for MediaDirs {} -/// Windows-specific extensions to [`fs::UserDirs`](UserDirs). +/// Windows-specific extensions to [`fs::HomeDirs`](HomeDirs). #[unstable(feature = "dir_discovery", issue = "157515")] #[expect(private_bounds, reason = "sealed")] -pub trait UserDirsExt: Sized + Sealed { +pub trait HomeDirsExt: Sized + Sealed { /// Load the known user folder paths using the [Known Folders] API. /// /// The loaded known folders are: /// - /// | `UserDirs` | [`KNOWNFOLDERID`] | + /// | `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 `UserDirs`. + /// + /// # 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`) | - /// | [`public_share`] | [`FOLDERID_Public`] (`%PUBLIC%`)[^public] | /// | [`videos`] | [`FOLDERID_Videos`] (`%USERPROFILE%\Videos`) | /// - /// 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 Apple platforms, making it even more important to not - /// assume files in different user directories cannot alias each other. - /// - /// [^public]: The default public directory is a separate user folder, - /// unlike other OSes where it is a subdirectory of the user's home. - /// This is only particularly meaningful on multi-user systems. - /// /// # Errors /// /// Errors if the underlying system discovery API returns an error. @@ -51,11 +79,8 @@ pub trait UserDirsExt: Sized + Sealed { /// /// # Implementation-specific behavior /// - /// Calls [`SHGetKnownFolderPath`] configured to not create the folder if - /// missing for the current user once for each of `FOLDERID_Desktop`, - /// `FOLDERID_Documents`, `FOLDERID_Downloads`, `FOLDERID_LocalAppData`, - /// `FOLDERID_Music`, `FOLDERID_Pictures`, `FOLDERID_Public`, - /// `FOLDERID_RoamingAppData`, and `FOLDERID_Videos`. + /// 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 @@ -64,112 +89,134 @@ pub trait UserDirsExt: Sized + Sealed { /// [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 /// [`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_Public`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_public /// [`FOLDERID_Videos`]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#folderid_videos - /// [`cache_home`]: UserDirs::cache_home - /// [`config_home`]: UserDirs::config_home - /// [`data_home`]: UserDirs::data_home - /// [`state_home`]: UserDirs::state_home - /// [`desktop`]: UserDirs::desktop - /// [`documents`]: UserDirs::documents - /// [`downloads`]: UserDirs::downloads - /// [`music`]: UserDirs::music - /// [`pictures`]: UserDirs::pictures - /// [`public_share`]: UserDirs::public_share - /// [`videos`]: UserDirs::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 UserDirsExt for UserDirs { +impl HomeDirsExt for HomeDirs { fn known_folders() -> io::Result { - let desktop = get_known_folder_path(&c::FOLDERID_Desktop)?; - let documents = get_known_folder_path(&c::FOLDERID_Documents)?; - let downloads = get_known_folder_path(&c::FOLDERID_Downloads)?; - let local_app_data = get_known_folder_path(&c::FOLDERID_LocalAppData)?; - let music = get_known_folder_path(&c::FOLDERID_Music)?; - let pictures = get_known_folder_path(&c::FOLDERID_Pictures)?; - let public = get_known_folder_path(&c::FOLDERID_Public)?; - let roaming_app_data = get_known_folder_path(&c::FOLDERID_RoamingAppData)?; - let videos = get_known_folder_path(&c::FOLDERID_Videos)?; + 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 = UserDirs::new(); + let mut dirs = HomeDirs::new(); - dirs.home.cache = local_app_data.clone(); - dirs.home.config = roaming_app_data.clone(); - dirs.home.data = roaming_app_data; - dirs.home.state = local_app_data; + dirs.cache = local_app_data.clone(); + dirs.config = roaming_app_data.clone(); + dirs.data = roaming_app_data; + dirs.state = local_app_data; - dirs.media.desktop = desktop; - dirs.media.documents = documents; - dirs.media.downloads = downloads; - dirs.media.music = music; - dirs.media.pictures = pictures; - dirs.media.public_share = public; - dirs.media.videos = videos; + Ok(dirs) + } +} + +#[cfg(windows)] +#[unstable(feature = "dir_discovery", issue = "157515")] +impl UserDirsExt for UserDirs { + 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 public = sys::get_known_folder_path(&c::FOLDERID_Public)?; + 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::new(); + + dirs.desktop = desktop; + dirs.documents = documents; + dirs.downloads = downloads; + dirs.music = music; + dirs.pictures = pictures; + dirs.public_share = public; + dirs.videos = videos; Ok(dirs) } } -/// Retrieve a known folder path from the Windows API. #[cfg(windows)] -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 - // UserDirs does not 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 - 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 +mod sys { + use crate::io::{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 + // UserDirs does not 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 + #[cfg(debug_assertions)] + unreachable!("should not call get_known_folder_path on a virtual folder"); + 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)] @@ -178,11 +225,13 @@ mod tests { #[test] fn can_fetch_known_folder_paths() { - let dirs = UserDirs::known_folders().unwrap(); + 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()); diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index 0c297c5766b82..009be6f0a95ec 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -9,6 +9,7 @@ cfg_select! { any(target_family = "unix", target_os = "wasi") => { mod unix; use unix as imp; + pub use unix::{ExtraHomeDirs, ExtraMediaDirs}; #[cfg(not(target_os = "wasi"))] pub use unix::{chown, fchown, lchown, mkfifo}; #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] @@ -63,6 +64,11 @@ pub use imp::{ ReadDir, }; +#[cfg(not(unix))] +pub type ExtraHomeDirs = (); +#[cfg(not(unix))] +pub type 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..d215f0d6e2093 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1966,6 +1966,18 @@ impl fmt::Debug for Mode { } } +#[derive(Debug, Default, Clone)] +pub struct ExtraHomeDirs { + pub runtime: Option, + pub config_path: Option, + pub data_path: Option, +} + +#[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() { From c39364a055f7b2b90c20dfe575bf33d0e6306b85 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:37:47 -0400 Subject: [PATCH 28/43] fix darwin fs::dirs typo --- library/std/src/os/darwin/fs/dirs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index a57b7096b77f2..f87bda403f999 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -257,7 +257,7 @@ mod sys { return None; } - let Ok(path) = std::ffi::CStr::from_bytes_until_nul(&buf) else { + let Ok(path) = crate::ffi::CStr::from_bytes_until_nul(&buf) else { // should be impossible given username length limits, but be defensive return Some(Err(const_error!( ErrorKind::InvalidData, From 9bd1cdc150c549fea75982fb75892c9bb9650a97 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:54:57 -0400 Subject: [PATCH 29/43] fix imports --- library/std/src/os/windows/fs/dirs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 3c9b425d2a517..86e80caa194f7 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -161,7 +161,7 @@ impl UserDirsExt for UserDirs { #[cfg(windows)] mod sys { - use crate::io::{ErrorKind, const_error}; + use crate::io::{self, ErrorKind, const_error}; use crate::path::PathBuf; use crate::sys::{c, os2path}; use crate::{ptr, slice}; From d453917cd3b1d7bfe4ad27bc072efada786b8d32 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 21:42:30 -0400 Subject: [PATCH 30/43] refactor xdg unquote --- library/std/src/os/unix/fs/dirs.rs | 94 ++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 4a5f7d5a3421b..65a933816cca0 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -1,6 +1,7 @@ // use shlex::bytes::Shlex; -use crate::env::{JoinPathsError, SplitPaths, home_dir, join_paths, split_paths, var_os}; +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}; @@ -45,7 +46,7 @@ pub trait HomeDirsExt: Sized + Sealed { /// | [`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`](home_dir), which uses + /// 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. @@ -140,7 +141,6 @@ pub trait HomeDirsExt: Sized + Sealed { /// [xdg-user-dirs]: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ #[unstable(feature = "media_dir_discovery", issue = "157515")] #[expect(private_bounds, reason = "sealed")] -#[cfg(unix)] pub trait MediaDirsExt: Sized + Sealed { /// Load the user directory paths according to the [xdg-user-dirs] tool. /// @@ -164,7 +164,7 @@ pub trait MediaDirsExt: Sized + Sealed { /// `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`](home_dir); shell expansion 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. @@ -281,35 +281,33 @@ impl MediaDirsExt for MediaDirs { let Some(var) = split.next() else { continue }; let Some(val) = split.next() else { continue }; debug_assert_eq!(split.next(), None); - // expand the only allowed expansion: a leading `$HOME/` prefix, still quoted - let buffer; - const HOME_RELATIVE_PREFIX: &[u8] = b"\"$HOME/"; - let expanded = if val.starts_with(HOME_RELATIVE_PREFIX) { - let joined = user_home.join(OsStr::from_bytes(&val[HOME_RELATIVE_PREFIX.len()..])); - buffer = OsString::from_iter([OsStr::new("\""), joined.as_os_str()]); - buffer.as_bytes() + + // 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 { - val + PathBuf::from(OsString::from_vec(path.into_owned())) }; - // the path value is shell-escaped, so unescape it - // FIXME: get shlex working as dep-of-std - // let mut lex = Shlex::new(expanded); - // let Some(path) = lex.next() else { continue }; - // let None = lex.next() else { continue }; - let path = &expanded[1..expanded.len() - 1]; // FIXME: placeholder quote strip - // load the known user directories match var { - b"XDG_DESKTOP_DIR" => dirs.desktop = Some(path_from_bytes(&path)), - b"XDG_DOCUMENTS_DIR" => dirs.documents = Some(path_from_bytes(&path)), - b"XDG_DOWNLOAD_DIR" => dirs.downloads = Some(path_from_bytes(&path)), - b"XDG_MUSIC_DIR" => dirs.music = Some(path_from_bytes(&path)), - b"XDG_PICTURES_DIR" => dirs.pictures = Some(path_from_bytes(&path)), - b"XDG_VIDEOS_DIR" => dirs.videos = Some(path_from_bytes(&path)), - b"XDG_TEMPLATES_DIR" => dirs.extra.templates = Some(path_from_bytes(&path)), + 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, matching shell permissiveness + // ignore unknown variable assignment } } } @@ -328,7 +326,7 @@ impl MediaDirsExt for MediaDirs { } fn user_home() -> io::Result { - home_dir() + env::home_dir() .filter(|p| !p.is_empty()) .ok_or(const_error!(ErrorKind::NotFound, "no home directory")) } @@ -341,8 +339,44 @@ fn xdg_env(env: &str, fallback: &str) -> OsString { var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| OsString::from(fallback)) } -fn path_from_bytes(bytes: &[u8]) -> PathBuf { - PathBuf::from(OsString::from_vec(bytes.to_vec())) +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)] From 2e7a433fa82d4e18a19f8eb6927a134e3a582fae Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 21:43:11 -0400 Subject: [PATCH 31/43] remove commented shlex dependency --- library/std/Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/library/std/Cargo.toml b/library/std/Cargo.toml index 1ab706fdc83f4..4c433d4970cda 100644 --- a/library/std/Cargo.toml +++ b/library/std/Cargo.toml @@ -62,11 +62,6 @@ path = "../windows_link" rand = { version = "0.9.0", default-features = false, features = ["alloc"] } rand_xorshift = "0.4.0" -# [target.'cfg(unix)'.dependencies] -# shlex = { version = "1.3.0", default-features = false, features = [ -# "rustc-dep-of-std", # allowlisted but missing rustc-dep-of-std feature -# ] } - [target.'cfg(any(all(target_family = "wasm", not(any(unix, target_os = "wasi"))), target_os = "xous", target_os = "vexos", all(target_vendor = "fortanix", target_env = "sgx")))'.dependencies] dlmalloc = { version = "0.2.10", features = ['rustc-dep-of-std'] } From 5bf2537fc4b08ef16a181fb22562db2e6df32a0b Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 22:04:23 -0400 Subject: [PATCH 32/43] fix doclinks --- library/std/src/fs/dirs.rs | 60 +++++++++++++-------------- library/std/src/os/darwin/fs/dirs.rs | 1 - library/std/src/os/windows/fs/dirs.rs | 2 - 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 88987439f3b20..95cac155c6ba4 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -87,9 +87,9 @@ impl HomeDirs { /// /// Other paths can be configured via [`set_cache_home`](Self::set_cache_home). /// - /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt - /// [Windows]: std::os::windows::fs::dirs::HomeDirsExt + /// [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")] @@ -115,9 +115,9 @@ impl HomeDirs { /// /// Other paths can be configured via [`set_config_home`](Self::set_config_home). /// - /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt - /// [Windows]: std::os::windows::fs::dirs::HomeDirsExt + /// [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")] @@ -143,9 +143,9 @@ impl HomeDirs { /// /// Other paths can be configured via [`set_data_home`](Self::set_data_home). /// - /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt - /// [Windows]: std::os::windows::fs::dirs::HomeDirsExt + /// [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")] @@ -178,9 +178,9 @@ impl HomeDirs { /// /// Other paths can be configured via [`set_state_home`](Self::set_state_home). /// - /// [XDG]: std::os::unix::fs::dirs::HomeDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::HomeDirsExt - /// [Windows]: std::os::windows::fs::dirs::HomeDirsExt + /// [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")] @@ -225,9 +225,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_desktop`](Self::set_desktop). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] @@ -253,9 +253,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_documents`](Self::set_documents). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] @@ -281,9 +281,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_downloads`](Self::set_downloads). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] @@ -309,9 +309,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_music`](Self::set_music). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] @@ -337,9 +337,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_pictures`](Self::set_pictures). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] @@ -365,9 +365,9 @@ impl MediaDirs { /// /// Other paths can be configured via [`set_videos`](Self::set_videos). /// - /// [XDG]: std::os::unix::fs::dirs::MediaDirsExt - /// [Darwin]: std::os::darwin::fs::dirs::MediaDirsExt - /// [Windows]: std::os::windows::fs::dirs::MediaDirsExt + /// [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")] diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index f87bda403f999..7cc33fd6b5d32 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -112,7 +112,6 @@ pub trait MediaDirsExt: Sized + Sealed { /// [`downloads`]: MediaDirs::downloads /// [`music`]: MediaDirs::music /// [`pictures`]: MediaDirs::pictures - /// [`public_share`]: MediaDirs::public_share /// [`videos`]: MediaDirs::videos /// /// [`NSSearchPathDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory?language=objc diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 86e80caa194f7..a0895038229e3 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -152,7 +152,6 @@ impl UserDirsExt for UserDirs { dirs.downloads = downloads; dirs.music = music; dirs.pictures = pictures; - dirs.public_share = public; dirs.videos = videos; Ok(dirs) @@ -238,7 +237,6 @@ mod tests { assert!(dirs.downloads().is_some()); assert!(dirs.music().is_some()); assert!(dirs.pictures().is_some()); - assert!(dirs.public_share().is_some()); assert!(dirs.videos().is_some()); } } From 0eb2d6d730d74ff63cf52bf5bc04acbf1e83de4c Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 22:22:42 -0400 Subject: [PATCH 33/43] fix remaining UserDirs references --- library/std/src/os/windows/fs/dirs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index a0895038229e3..fd85e8e91879b 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -30,7 +30,7 @@ pub trait HomeDirsExt: Sized + Sealed { /// /// 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 `UserDirs`. + /// in a `None` value for that path in the returned `HomeDirs`. /// /// # Implementation-specific behavior /// @@ -75,7 +75,7 @@ pub trait MediaDirsExt: Sized + Sealed { /// /// 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 `UserDirs`. + /// in a `None` value for that path in the returned `MediaDirs`. /// /// # Implementation-specific behavior /// @@ -130,7 +130,7 @@ impl HomeDirsExt for HomeDirs { #[cfg(windows)] #[unstable(feature = "dir_discovery", issue = "157515")] -impl UserDirsExt for UserDirs { +impl MediaDirsExt for MediaDirs { fn known_folders() -> io::Result { use crate::sys::c; @@ -169,7 +169,7 @@ mod sys { 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 - // UserDirs does not guarantee that the directories at the paths exist. + // 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. From 90f7788b2ac137834d1faac76e8d9712fb289f26 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 10:48:10 -0400 Subject: [PATCH 34/43] fix renames --- library/std/src/os/windows/fs/dirs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index fd85e8e91879b..a05cfdef6236c 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -117,7 +117,7 @@ impl HomeDirsExt for HomeDirs { // 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::new(); + let mut dirs = HomeDirs::empty(); dirs.cache = local_app_data.clone(); dirs.config = roaming_app_data.clone(); @@ -145,7 +145,7 @@ impl MediaDirsExt for MediaDirs { // 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::new(); + let mut dirs = MediaDirs::empty(); dirs.desktop = desktop; dirs.documents = documents; From fa28a6a0d3b35a31a9c544a875d5e43e4342663d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 11:10:50 -0400 Subject: [PATCH 35/43] fixes --- library/std/src/fs/dirs.rs | 2 ++ library/std/src/os/windows/fs/dirs.rs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index 95cac155c6ba4..bb984e0b7efc7 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -25,6 +25,7 @@ pub struct HomeDirs { pub(crate) config: Option, pub(crate) data: Option, pub(crate) state: Option, + #[allow(dead_code)] pub(crate) extra: ExtraHomeDirs, } @@ -53,6 +54,7 @@ pub struct MediaDirs { pub(crate) music: Option, pub(crate) pictures: Option, pub(crate) videos: Option, + #[allow(dead_code)] pub(crate) extra: ExtraMediaDirs, } diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index a05cfdef6236c..5472645d44258 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -139,7 +139,6 @@ impl MediaDirsExt for MediaDirs { 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 public = sys::get_known_folder_path(&c::FOLDERID_Public)?; let videos = sys::get_known_folder_path(&c::FOLDERID_Videos)?; // AppData/Local -- system-local, doesn't make sense to sync to another From 9098b4645f7eac139ea21da23d74f9474726f586 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 11:38:22 -0400 Subject: [PATCH 36/43] fix --- library/std/src/os/windows/fs/dirs.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 5472645d44258..b1d611e84ebb4 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -191,12 +191,14 @@ mod sys { } c::E_FAIL => { // This known folder id exists but does not have a path - #[cfg(debug_assertions)] - unreachable!("should not call get_known_folder_path on a virtual folder"); - Err(const_error!( - ErrorKind::InvalidInput, - "virtual known folders do not have paths" - )) + 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 From 74ae37e9e6e37c84b3157b4b36bbf836800a75de Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 13:33:02 -0400 Subject: [PATCH 37/43] fix imports --- library/std/src/os/darwin/fs/dirs.rs | 8 ++++---- library/std/src/os/unix/fs/dirs.rs | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 7cc33fd6b5d32..7ffd2e220176f 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -1,8 +1,7 @@ use crate::env; -use crate::ffi::{CStr, c_char}; use crate::fs::{HomeDirs, MediaDirs}; use crate::io::{self, ErrorKind, const_error}; -use crate::path::{Path, PathBuf}; +use crate::path::PathBuf; trait Sealed {} impl Sealed for HomeDirs {} @@ -184,6 +183,7 @@ impl MediaDirsExt for MediaDirs { /// 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}; @@ -246,7 +246,7 @@ mod sys { self.state = unsafe { libc::sysdir_get_next_search_path_enumeration( self.state, - buf.as_mut_ptr() as *mut libc::c_char, + buf.as_mut_ptr() as *mut c_char, ) }; } @@ -256,7 +256,7 @@ mod sys { return None; } - let Ok(path) = crate::ffi::CStr::from_bytes_until_nul(&buf) else { + 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, diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 65a933816cca0..ac11b5f60ed48 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -1,5 +1,3 @@ -// use shlex::bytes::Shlex; - use crate::borrow::Cow; use crate::env::{self, JoinPathsError, SplitPaths, join_paths, split_paths, var_os}; use crate::ffi::{OsStr, OsString}; From 058b060cf551192648d7e28b1e5d8d7b8c5e813b Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 15:10:01 -0400 Subject: [PATCH 38/43] fix wasi pal --- library/std/src/sys/fs/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index 009be6f0a95ec..f9a2918109976 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -64,9 +64,9 @@ pub use imp::{ ReadDir, }; -#[cfg(not(unix))] +#[cfg(not(any(target_family = "unix", target_os = "wasi")))] pub type ExtraHomeDirs = (); -#[cfg(not(unix))] +#[cfg(not(any(target_family = "unix", target_os = "wasi")))] pub type ExtraMediaDirs = (); pub fn read_dir(path: &Path) -> io::Result { From dcb34b6e11d346b0852e51fa69034405af3c219e Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 16:58:41 -0400 Subject: [PATCH 39/43] maybe fix import warning --- library/std/src/os/unix/fs/dirs.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index ac11b5f60ed48..027a5715bf2c2 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -3,7 +3,6 @@ use crate::env::{self, JoinPathsError, SplitPaths, join_paths, split_paths, var_ 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 {} @@ -250,6 +249,8 @@ impl HomeDirsExt for HomeDirs { #[cfg(unix)] impl MediaDirsExt for MediaDirs { fn xdg() -> io::Result { + use crate::os::unix::ffi::{OsStrExt, OsStringExt}; + let mut dirs = MediaDirs::empty(); let user_home = user_home()?; let config_home = xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config")); From 2f81f4b5bd7363d85cf4d8b103ee0751d3ceb363 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 17:47:20 -0400 Subject: [PATCH 40/43] unix doesnt mean cfg(unix) --- library/std/src/os/unix/fs/dirs.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 027a5715bf2c2..57da2f98ea8f4 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -3,6 +3,7 @@ use crate::env::{self, JoinPathsError, SplitPaths, join_paths, split_paths, var_ 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 {} @@ -193,7 +194,6 @@ pub trait MediaDirsExt: Sized + Sealed { } #[unstable(feature = "dir_discovery", issue = "157515")] -#[cfg(unix)] impl HomeDirsExt for HomeDirs { fn xdg() -> io::Result { let mut dirs = HomeDirs::empty(); @@ -246,11 +246,8 @@ impl HomeDirsExt for HomeDirs { } #[unstable(feature = "media_dir_discovery", issue = "157515")] -#[cfg(unix)] impl MediaDirsExt for MediaDirs { fn xdg() -> io::Result { - use crate::os::unix::ffi::{OsStrExt, OsStringExt}; - let mut dirs = MediaDirs::empty(); let user_home = user_home()?; let config_home = xdg_dir("XDG_CONFIG_HOME", || user_home.join(".config")); From b8dccd8e076630f1576f19dbc2b266a1bd90a8df Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 18:50:17 -0400 Subject: [PATCH 41/43] wasi no fs? --- library/std/src/sys/fs/mod.rs | 3 +-- library/std/src/sys/fs/unix.rs | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index f9a2918109976..d76a2b0777142 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -9,9 +9,8 @@ cfg_select! { any(target_family = "unix", target_os = "wasi") => { mod unix; use unix as imp; - pub use unix::{ExtraHomeDirs, ExtraMediaDirs}; #[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"))] diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index d215f0d6e2093..5c24794035467 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1966,6 +1966,7 @@ impl fmt::Debug for Mode { } } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -1973,6 +1974,7 @@ pub struct ExtraHomeDirs { pub data_path: Option, } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option, From 3a0106de75726a1573d05364ee8f7d3614166eeb Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 15 Jul 2026 10:44:59 -0400 Subject: [PATCH 42/43] used or unused make up your mind --- library/std/src/sys/fs/mod.rs | 3 ++- library/std/src/sys/fs/unix.rs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index d76a2b0777142..f9a2918109976 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -9,8 +9,9 @@ cfg_select! { any(target_family = "unix", target_os = "wasi") => { mod unix; use unix as imp; + pub use unix::{ExtraHomeDirs, ExtraMediaDirs}; #[cfg(not(target_os = "wasi"))] - pub use unix::{chown, fchown, lchown, mkfifo, ExtraHomeDirs, ExtraMediaDirs}; + pub use unix::{chown, fchown, lchown, mkfifo}; #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] pub use unix::chroot; #[cfg(not(target_os = "wasi"))] diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 5c24794035467..d215f0d6e2093 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1966,7 +1966,6 @@ impl fmt::Debug for Mode { } } -#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -1974,7 +1973,6 @@ pub struct ExtraHomeDirs { pub data_path: Option, } -#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option, From ce2ad684f599af2433d6b108626a2afd9355fdd8 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 15 Jul 2026 14:53:28 -0400 Subject: [PATCH 43/43] handle wasi right? --- library/std/src/fs/dirs.rs | 4 ++-- library/std/src/sys/fs/common.rs | 3 +++ library/std/src/sys/fs/mod.rs | 9 +++------ library/std/src/sys/fs/unix.rs | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index bb984e0b7efc7..b6fd5253af07b 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -25,7 +25,7 @@ pub struct HomeDirs { pub(crate) config: Option, pub(crate) data: Option, pub(crate) state: Option, - #[allow(dead_code)] + #[cfg_attr(not(unix), expect(dead_code, reason = "no extra home dirs"))] pub(crate) extra: ExtraHomeDirs, } @@ -54,7 +54,7 @@ pub struct MediaDirs { pub(crate) music: Option, pub(crate) pictures: Option, pub(crate) videos: Option, - #[allow(dead_code)] + #[cfg_attr(not(unix), expect(dead_code, reason = "no extra media dirs"))] pub(crate) extra: ExtraMediaDirs, } 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 f9a2918109976..03341116d3e84 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -9,9 +9,8 @@ cfg_select! { any(target_family = "unix", target_os = "wasi") => { mod unix; use unix as imp; - pub use unix::{ExtraHomeDirs, ExtraMediaDirs}; #[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"))] @@ -64,10 +63,8 @@ pub use imp::{ ReadDir, }; -#[cfg(not(any(target_family = "unix", target_os = "wasi")))] -pub type ExtraHomeDirs = (); -#[cfg(not(any(target_family = "unix", target_os = "wasi")))] -pub type ExtraMediaDirs = (); +#[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 diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index d215f0d6e2093..5c24794035467 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1966,6 +1966,7 @@ impl fmt::Debug for Mode { } } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -1973,6 +1974,7 @@ pub struct ExtraHomeDirs { pub data_path: Option, } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option,