From fed84bc129557a478dccf5cea2d329c28804b032 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 22 Jun 2026 16:54:41 -0400 Subject: [PATCH 01/60] 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 f5c60f7562d4a..762ad071e967c 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 25aa3bf7893f4..5605642ff17d3 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 64b248eaea8b7a2dc44f2d50955348d8e7506569 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:38:14 -0400 Subject: [PATCH 02/60] 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 388bf4a55e11f..6dd9c3ba3dea9 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 c64b404b698e624fcc4f967f586b407a9f0ed1bb Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:38:52 -0400 Subject: [PATCH 03/60] 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 701e899a32acf..b724fa144afb1 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 8a07e4b85dad60a431c53116b7db4755a2551def Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:39:12 -0400 Subject: [PATCH 04/60] 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 71379202874ae..81984a0d0701b 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 @@ -2139,6 +2141,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 @@ -2270,6 +2281,7 @@ IPV6_MREQ IPV6_MULTICAST_LOOP IPV6_V6ONLY IsThreadAFiber +KF_FLAG_DONT_VERIFY LINGER listen LocalFree @@ -2368,6 +2380,7 @@ ReleaseSRWLockShared RemoveDirectoryW RtlGenRandom RtlNtStatusToDosError +S_OK SD_BOTH SD_RECEIVE SD_SEND @@ -2396,6 +2409,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 9e867ec9dfae7..4885f4cc57076 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); @@ -93,6 +94,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); @@ -2388,6 +2390,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; @@ -2700,6 +2703,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; @@ -2941,6 +2954,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 { @@ -3358,6 +3373,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 e7015a3dc80789426d3bcb5326fdf905817e51b1 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 17:39:23 -0400 Subject: [PATCH 05/60] 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 2200574101fc2b969dcbf970371381a3f1f1223e Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 18:29:07 -0400 Subject: [PATCH 06/60] 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 42aeaa5e3b12c68c80078f9447f7ad34dc190e5d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 19:07:57 -0400 Subject: [PATCH 07/60] 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 67241355d995c32730cdaaeb7dffdfb8926460a5 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 7 Jul 2026 19:43:11 -0400 Subject: [PATCH 08/60] 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 b52be2bd400bead51089301d28daf06e05d02933 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 04:38:45 -0400 Subject: [PATCH 09/60] 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 81984a0d0701b..e8892b7f9d49f 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 4885f4cc57076..7a9c538d9ea24 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -2390,6 +2390,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; @@ -2707,7 +2708,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 cf00aa5ca5b15e032e7ce1cc4e328f7e677c7759 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 11:50:45 -0400 Subject: [PATCH 10/60] 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 5a41b4dcc4fcbf0d245a51a69d3e7ee142d802f3 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 15:34:57 -0400 Subject: [PATCH 11/60] 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 803bd4689858ef78bb7feb322595b33e024bc3d1 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 16:39:03 -0400 Subject: [PATCH 12/60] 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 6e61768be1c44982fbfbd1fed82868cd409670d9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 8 Jul 2026 17:24:47 -0400 Subject: [PATCH 13/60] 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 141d4ba761feab1ee9caf18d6da8d7d0895c97f4 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 10:27:20 -0400 Subject: [PATCH 14/60] 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 957dd6b46b79d8615b261606f4c27c6b55ebc6c6 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 11:05:54 -0400 Subject: [PATCH 15/60] 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 33696f9d4529c25ee2936b64e2f80ab92b96e90f Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 13:02:23 -0400 Subject: [PATCH 16/60] 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 0f18752853b9b741a508d453d09510709b5c16e6 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 14:37:34 -0400 Subject: [PATCH 17/60] 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 d93dabaf4febef5ba5581edd2cb6170a81981d44 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 15:22:05 -0400 Subject: [PATCH 18/60] 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 fffff2819094fb4c75ba263ee0adce8f2fbfb51d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 16:02:46 -0400 Subject: [PATCH 19/60] 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 2a0c77e489ad8bb2c7b599587215190d1dbd6742 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 9 Jul 2026 16:43:54 -0400 Subject: [PATCH 20/60] 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 d19f2f899b37bc7873941d0cc05fa1882bfde87d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:02:43 -0400 Subject: [PATCH 21/60] 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 4bbd3c0232543a71cc2271ed1e44eff086a74c71 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:05:42 -0400 Subject: [PATCH 22/60] 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 778bf908739a4553c88c6b0d422dd31bb8e032c0 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:37:34 -0400 Subject: [PATCH 23/60] 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 6b6ed77ad36e1abd2ee0620fe4b0895d0f5f6272 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 15:38:32 -0400 Subject: [PATCH 24/60] 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 847bc3f700b83184cd4f25ed78d36abf1c104611 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 16:21:51 -0400 Subject: [PATCH 25/60] 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 3fc3c02cff2a0175eea62dbdd192646bbdb91883 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 17:34:38 -0400 Subject: [PATCH 26/60] 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 8dbd25d1515728de414a7f0cc1ebbf351214cf39 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:26:22 -0400 Subject: [PATCH 27/60] 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 6dd9c3ba3dea9..09a4f596ed168 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 d39dd4c0910ca..e54af97f92315 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2012,6 +2012,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 d410551b43568fbac44a4121a8da757c9c58eb10 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:37:47 -0400 Subject: [PATCH 28/60] 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 8663dfce984cf6277d56bcf86c6931d2a661d07c Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 20:54:57 -0400 Subject: [PATCH 29/60] 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 c8bdfc5afec3806f7bf49d181b929114d29fc1a9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 21:42:30 -0400 Subject: [PATCH 30/60] 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 e74f8190aa4fcf8e2ec607e822914a26e01e86bb Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 21:43:11 -0400 Subject: [PATCH 31/60] 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 b724fa144afb1..701e899a32acf 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 952e019128cd86e501ffa36f992791618473aa1d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 22:04:23 -0400 Subject: [PATCH 32/60] 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 a0728c51371ccff57c8feaf61fddccf3e2d1d16c Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Mon, 13 Jul 2026 22:22:42 -0400 Subject: [PATCH 33/60] 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 e99806e2b464606a4b4df145becc408de1ef7c18 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 10:48:10 -0400 Subject: [PATCH 34/60] 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 72e5972645b2be38c276fa630a1d5aac520a8287 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 11:10:50 -0400 Subject: [PATCH 35/60] 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 1f2cd596f4f9819caf0293095c08da5080533f13 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 11:38:22 -0400 Subject: [PATCH 36/60] 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 030f02c98c44fe5fa090b018898e09c8ad16500f Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 13:33:02 -0400 Subject: [PATCH 37/60] 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 1b4a1e7fd3ef1d10f40e21baeabb4fe3a94fe199 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 15:10:01 -0400 Subject: [PATCH 38/60] 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 fc34234968790ec714a35ccf8b9c5ffd15d33566 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 16:58:41 -0400 Subject: [PATCH 39/60] 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 7840fbae00ceacd400beb8a3de2d7312808502ab Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 17:47:20 -0400 Subject: [PATCH 40/60] 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 2cfedc2d80f5c46ea3945dffde6369c57842b77f Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Tue, 14 Jul 2026 18:50:17 -0400 Subject: [PATCH 41/60] 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 e54af97f92315..da7b89e75cb95 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2012,6 +2012,7 @@ impl fmt::Debug for Mode { } } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -2019,6 +2020,7 @@ pub struct ExtraHomeDirs { pub data_path: Option, } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option, From 6de219b21b8d5a356a7f0a1be518dc44bae519f0 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 15 Jul 2026 10:44:59 -0400 Subject: [PATCH 42/60] 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 da7b89e75cb95..e54af97f92315 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2012,7 +2012,6 @@ impl fmt::Debug for Mode { } } -#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -2020,7 +2019,6 @@ pub struct ExtraHomeDirs { pub data_path: Option, } -#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option, From 7e402f3ea6990abc95461441652490d1be87e2be Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 15 Jul 2026 14:53:28 -0400 Subject: [PATCH 43/60] 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 68aed39d1dcdf..90c86bf3e31f3 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -93,3 +93,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 e54af97f92315..da7b89e75cb95 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2012,6 +2012,7 @@ impl fmt::Debug for Mode { } } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraHomeDirs { pub runtime: Option, @@ -2019,6 +2020,7 @@ pub struct ExtraHomeDirs { pub data_path: Option, } +#[cfg(not(target_os = "wasi"))] #[derive(Debug, Default, Clone)] pub struct ExtraMediaDirs { pub templates: Option, From bd5ad1a61bbf87f6e19c4343ec1008417c9401b8 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 22 Jul 2026 14:34:03 -0400 Subject: [PATCH 44/60] clarify HomeDirs use case --- library/std/src/fs/dirs.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/library/std/src/fs/dirs.rs b/library/std/src/fs/dirs.rs index b6fd5253af07b..f73b7bdf717e1 100644 --- a/library/std/src/fs/dirs.rs +++ b/library/std/src/fs/dirs.rs @@ -71,8 +71,12 @@ impl HomeDirs { /// 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. + /// "Cache" files are temporary data that can be used to cache redundant + /// work of an application, but which can be discarded arbitrarily and + /// recreated as necessary. Files in this directory may potentially be + /// automatically purged any time they are not currently open, or they + /// may not, depending on system configuration. A robust application + /// should ensure that its caches do not grow unboundedly. /// /// This is the same directory for all applications. As such, applications /// should use a subdirectory for application-specific cache files. @@ -102,6 +106,12 @@ impl HomeDirs { /// A base directory relative to which user-specific configuration files /// should be stored. /// + /// "Config" files are configuration managed by the user, either by editing + /// the files directly or through a managing application. Configuration is + /// generally expected to be meaningful to the user and portable enough to + /// back up and synchronize across the same user's account on multiple + /// systems. + /// /// This is the same directory for all applications. As such, applications /// should use a subdirectory for application-specific configuration files. /// @@ -130,6 +140,13 @@ impl HomeDirs { /// A base directory relative to which user-specific data files should be /// stored. /// + /// "Data" files are application-specific data that is meaningful to the + /// user in some way and does not implicitly rely on system configuration + /// or details of how the application is installed otherwise irrelevant to + /// the user. As such, data makes sense to back up and synchronize between + /// the same user's account on multiple systems. If a file is specific to + /// a single machine, it's probably [state](Self::state_home). + /// /// This is the same directory for all applications. As such, applications /// should use a subdirectory for application-specific data files. /// From e2209806b2137268805cf9cf465c86bf40e37dcd Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 22 Jul 2026 15:11:04 -0400 Subject: [PATCH 45/60] use impl(self) to seal UserDirsExt --- library/std/src/os/darwin/fs/dirs.rs | 10 ++-------- library/std/src/os/unix/fs/dirs.rs | 10 ++-------- library/std/src/os/windows/fs/dirs.rs | 10 ++-------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 7ffd2e220176f..c2d985cd11b16 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -3,14 +3,9 @@ use crate::fs::{HomeDirs, MediaDirs}; use crate::io::{self, ErrorKind, const_error}; use crate::path::PathBuf; -trait Sealed {} -impl Sealed for HomeDirs {} -impl Sealed for MediaDirs {} - /// Darwin-specific extensions to [`fs::HomeDirs`](HomeDirs). #[unstable(feature = "dir_discovery", issue = "157515")] -#[expect(private_bounds, reason = "sealed")] -pub trait HomeDirsExt: Sized + Sealed { +pub impl(self) trait HomeDirsExt: Sized { /// Load the standard user directory paths for the current user. /// /// On iOS, tvOS, watchOS, visionOS, and sandboxed macOS applications, @@ -69,8 +64,7 @@ pub trait HomeDirsExt: Sized + Sealed { /// Darwin-specific extensions to [`fs::MediaDirs`](MediaDirs). #[unstable(feature = "media_dir_discovery", issue = "157515")] -#[expect(private_bounds, reason = "sealed")] -pub trait MediaDirsExt: Sized + Sealed { +pub impl(self) trait MediaDirsExt: Sized { /// Load the standard user directory paths for the current user. /// /// The produced directory paths are not guaranteed to be the canonical diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 57da2f98ea8f4..ff671504db648 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -6,10 +6,6 @@ use crate::io::{self, ErrorKind, const_error}; use crate::os::unix::ffi::{OsStrExt, OsStringExt}; use crate::path::{Path, PathBuf}; -trait Sealed {} -impl Sealed for HomeDirs {} -impl Sealed for MediaDirs {} - /// XDG-specific extensions to [`fs::HomeDirs`](HomeDirs). /// /// The XDG conventions are defined by the Freedesktop.org project in the @@ -25,8 +21,7 @@ impl Sealed for MediaDirs {} /// /// [xdg-basedir]: https://specifications.freedesktop.org/basedir/ #[unstable(feature = "dir_discovery", issue = "157515")] -#[expect(private_bounds, reason = "sealed")] -pub trait HomeDirsExt: Sized + Sealed { +pub impl(self) trait HomeDirsExt: Sized { /// Load the user directory paths according to the /// [XDG Base Directory Specification][xdg-basedir]. /// @@ -138,8 +133,7 @@ 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")] -pub trait MediaDirsExt: Sized + Sealed { +pub impl(self) trait MediaDirsExt: Sized { /// Load the user directory paths according to the [xdg-user-dirs] tool. /// /// This directly reads and parses the `$XDG_CONFIG_HOME/user-dirs.dirs` diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index b1d611e84ebb4..dba32b255bf6f 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -1,14 +1,9 @@ use crate::fs::{HomeDirs, MediaDirs}; use crate::io; -trait Sealed {} -impl Sealed for HomeDirs {} -impl Sealed for MediaDirs {} - /// Windows-specific extensions to [`fs::HomeDirs`](HomeDirs). #[unstable(feature = "dir_discovery", issue = "157515")] -#[expect(private_bounds, reason = "sealed")] -pub trait HomeDirsExt: Sized + Sealed { +pub impl(self) trait HomeDirsExt: Sized { /// Load the known user folder paths using the [Known Folders] API. /// /// The loaded known folders are: @@ -56,8 +51,7 @@ pub trait HomeDirsExt: Sized + Sealed { /// Windows-specific extensions to [`fs::MediaDirs`](MediaDirs). #[unstable(feature = "media_dir_discovery", issue = "157515")] -#[expect(private_bounds, reason = "sealed")] -pub trait MediaDirsExt: Sized + Sealed { +pub impl(self) trait MediaDirsExt: Sized { /// Load the known user folder paths using the [Known Folders] API. /// /// The loaded known folders are: From a540c4964092b517903b7702d48925aa221b3df7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 22 Jul 2026 17:34:14 -0400 Subject: [PATCH 46/60] Add windows HomeDirExt::appdir_env --- library/std/src/os/windows/fs/dirs.rs | 71 +++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index dba32b255bf6f..b66ea48bf8cc8 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -1,9 +1,56 @@ use crate::fs::{HomeDirs, MediaDirs}; -use crate::io; +use crate::path::PathBuf; +use crate::{env, io}; /// Windows-specific extensions to [`fs::HomeDirs`](HomeDirs). #[unstable(feature = "dir_discovery", issue = "157515")] pub impl(self) trait HomeDirsExt: Sized { + /// Load the known user folder paths from environment variables. + /// + /// The loaded known folders are: + /// + /// | `HomeDirs` | Environment Variable | + /// | ---------- | -------------------- | + /// | [`cache_home`] | `%LOCALAPPDATA%` (`%USERPROFILE%\AppData\Local`) | + /// | [`config_home`] | `%APPDATA%` (`%USERPROFILE%\AppData\Roaming`) | + /// | [`data_home`] | `%APPDATA%` (`%USERPROFILE%\AppData\Roaming`) | + /// | [`state_home`] | `%LOCALAPPDATA%` (`%USERPROFILE%\AppData\Local`) | + /// + /// Note that caches/state are both put in `AppData\Local`, and config/data + /// in `AppData\Roaming`. 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 `%APPDATA%` or `%LOCALAPPDATA%` are not set or are non-UTF-8. + /// + /// # Implementation-specific behavior + /// + /// Windows keeps these environment variables updated to contain the paths + /// to the configured folder path, but it is possible for the environment + /// variables to not match the underlying system, such as when the user or + /// a program modifies the environment directly, or if the configuration + /// changed after the environment block was copied from the system. + /// + /// Unlike [`known_folders`](Self::known_folders), this does not require + /// `Shell32.dll` and thus does not require the overhead of linking in + /// DLLs that may result in Windows considering the application as a + /// graphical application. + /// + /// 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. The lack of configuration + /// for a folder not currently in this list will not be an error and + /// will result in a `None` value for that path in the returned value. + /// + /// [`cache_home`]: HomeDirs::cache_home + /// [`config_home`]: HomeDirs::config_home + /// [`data_home`]: HomeDirs::data_home + /// [`state_home`]: HomeDirs::state_home + fn appdata_env() -> io::Result; + /// Load the known user folder paths using the [Known Folders] API. /// /// The loaded known folders are: @@ -29,8 +76,8 @@ pub impl(self) trait HomeDirsExt: Sized { /// /// # Implementation-specific behavior /// - /// Calls [`SHGetKnownFolderPath`] for the current user once for each known - /// folder. Does not create the folder if missing. + /// Calls [`SHGetKnownFolderPath`] from `Shell32.dll` 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 @@ -102,6 +149,24 @@ pub impl(self) trait MediaDirsExt: Sized { #[cfg(windows)] #[unstable(feature = "dir_discovery", issue = "157515")] impl HomeDirsExt for HomeDirs { + fn appdata_env() -> io::Result { + let wrap_err = |e: env::VarError| io::Error::new(io::ErrorKind::NotFound, e); + let local_app_data = PathBuf::from(env::var("LOCALAPPDATA").map_err(wrap_err)?); + let roaming_app_data = PathBuf::from(env::var("APPDATA").map_err(wrap_err)?); + + // AppData/Local -- system-local, doesn't make sense to sync to another + // AppData/Roaming -- data that makes sense to sync across machines + + let mut dirs = HomeDirs::empty(); + + dirs.cache = Some(local_app_data.clone()); + dirs.config = Some(roaming_app_data.clone()); + dirs.data = Some(roaming_app_data); + dirs.state = Some(local_app_data); + + Ok(dirs) + } + fn known_folders() -> io::Result { use crate::sys::c; From 23bd91d4131e8e9e6c39622cc0a41ca33ddc6027 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Wed, 22 Jul 2026 22:08:56 -0400 Subject: [PATCH 47/60] lazy-load shell32.dll --- library/std/src/sys/pal/windows/c.rs | 19 ++ .../std/src/sys/pal/windows/c/bindings.txt | 4 +- .../std/src/sys/pal/windows/c/windows_sys.rs | 5 +- library/std/src/sys/pal/windows/compat.rs | 173 +++++++++++++++++- 4 files changed, 196 insertions(+), 5 deletions(-) diff --git a/library/std/src/sys/pal/windows/c.rs b/library/std/src/sys/pal/windows/c.rs index c2ddb4931365a..2fd7feb398dd0 100644 --- a/library/std/src/sys/pal/windows/c.rs +++ b/library/std/src/sys/pal/windows/c.rs @@ -245,3 +245,22 @@ windows_link::link!("ws2_32.dll" "system" fn GetHostNameW(name : PWSTR, namelen unsafe extern "C" { pub fn atexit(cb: unsafe extern "C" fn()) -> c_int; } + +// Functions in DLLs that we want to lazily load +compat_fn_with_fallback! { + #[lazy] + pub static SHELL32: &CStr = c"shell32"; + + pub fn SHGetKnownFolderPath(rfid: *const GUID, dwflags: u32, htoken: HANDLE, ppszpath: *mut PWSTR) -> HRESULT { + panic!("Known Folders not available") + } +} + +compat_fn_with_fallback! { + #[lazy] + pub static OLE32: &CStr = c"ole32"; + + pub fn CoTaskMemFree(pv: *const core::ffi::c_void) -> () { + panic!("COM not available") + } +} diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index e8892b7f9d49f..d7b2bbbd99bea 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -33,7 +33,6 @@ CONSOLE_MODE CONSOLE_READCONSOLE_CONTROL CONTEXT CopyFileExW -CoTaskMemFree CP_UTF8 CREATE_ALWAYS CREATE_BREAKAWAY_FROM_JOB @@ -2285,6 +2284,8 @@ IsThreadAFiber KF_FLAG_DONT_VERIFY LINGER listen +LOAD_LIBRARY_SEARCH_SYSTEM32 +LoadLibraryExA LocalFree LOCKFILE_EXCLUSIVE_LOCK LOCKFILE_FAIL_IMMEDIATELY @@ -2410,7 +2411,6 @@ 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 7a9c538d9ea24..2677ad3fcd4fb 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -7,7 +7,6 @@ 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); @@ -74,6 +73,7 @@ windows_link::link!("kernel32.dll" "system" fn InitOnceBeginInitialize(lpinitonc windows_link::link!("kernel32.dll" "system" fn InitOnceComplete(lpinitonce : *mut INIT_ONCE, dwflags : u32, lpcontext : *const core::ffi::c_void) -> BOOL); windows_link::link!("kernel32.dll" "system" fn InitializeProcThreadAttributeList(lpattributelist : LPPROC_THREAD_ATTRIBUTE_LIST, dwattributecount : u32, dwflags : u32, lpsize : *mut usize) -> BOOL); windows_link::link!("kernel32.dll" "system" fn IsThreadAFiber() -> BOOL); +windows_link::link!("kernel32.dll" "system" fn LoadLibraryExA(lplibfilename : PCSTR, hfile : HANDLE, dwflags : LOAD_LIBRARY_FLAGS) -> HMODULE); windows_link::link!("kernel32.dll" "system" fn LocalFree(hmem : HLOCAL) -> HLOCAL); windows_link::link!("kernel32.dll" "system" fn LockFileEx(hfile : HANDLE, dwflags : LOCK_FILE_FLAGS, dwreserved : u32, nnumberofbytestolocklow : u32, nnumberofbytestolockhigh : u32, lpoverlapped : *mut OVERLAPPED) -> BOOL); windows_link::link!("kernel32.dll" "system" fn MoveFileExW(lpexistingfilename : PCWSTR, lpnewfilename : PCWSTR, dwflags : MOVE_FILE_FLAGS) -> BOOL); @@ -94,7 +94,6 @@ 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); @@ -2962,6 +2961,8 @@ pub struct LINGER { pub l_onoff: u16, pub l_linger: u16, } +pub type LOAD_LIBRARY_FLAGS = u32; +pub const LOAD_LIBRARY_SEARCH_SYSTEM32: LOAD_LIBRARY_FLAGS = 2048u32; pub const LOCKFILE_EXCLUSIVE_LOCK: LOCK_FILE_FLAGS = 2u32; pub const LOCKFILE_FAIL_IMMEDIATELY: LOCK_FILE_FLAGS = 1u32; pub type LOCK_FILE_FLAGS = u32; diff --git a/library/std/src/sys/pal/windows/compat.rs b/library/std/src/sys/pal/windows/compat.rs index c465ceb2301ce..ce15c1a25bf76 100644 --- a/library/std/src/sys/pal/windows/compat.rs +++ b/library/std/src/sys/pal/windows/compat.rs @@ -19,9 +19,12 @@ //! function is called. In the worst case, multiple threads may all end up //! importing the same function unnecessarily. +use crate::cell::UnsafeCell; use crate::ffi::{CStr, c_void}; -use crate::ptr::NonNull; +use crate::ptr::{self, NonNull}; +use crate::sync::atomic::{AtomicUsize, Ordering}; use crate::sys::c; +use crate::sys::pal::windows::futex::{futex_wait, futex_wake_all}; // This uses a static initializer to preload some imported functions. // The CRT (C runtime) executes static initializers before `main` @@ -118,6 +121,24 @@ impl Module { } } + /// Try to load a module handle. + /// + /// # SAFETY + /// + /// This should only be used for system modules that will not be unloaded + /// for the lifetime of std (e.g. shell32 and combase). + pub unsafe fn load(name: &CStr) -> Option { + // SAFETY: A CStr is always null terminated. + unsafe { + let module = c::LoadLibraryExA( + /* lpLibFileName */ name.as_ptr().cast::(), + /* hReserved */ ptr::null_mut(), + /* dwFlags */ c::LOAD_LIBRARY_SEARCH_SYSTEM32, + ); + NonNull::new(module).map(Module) + } + } + // Try to get the address of a function. pub fn proc_address(self, name: &CStr) -> Option> { unsafe { @@ -131,6 +152,95 @@ impl Module { } } +/// Represents a lazy-loaded module. +/// +/// Note that the modules std depends on must not be unloaded. +/// Therefore a `Module` is always valid for the lifetime of std. +/// +/// # State machine +/// +/// - `(ptr: *mut u8, len: usize is 1..MAX)` -- module not yet loaded; +/// module name is `CStr::from_unchecked(&*ptr::from_raw_parts(ptr, len))` +/// - `(ptr: *mut u8, len: usize is MAX)` -- locked; a thread is loading the module +/// - `(val: Option, len: usize is 0)` -- module loaded (or failed to load) +pub(in crate::sys) struct LazyModule(UnsafeCell>>, AtomicUsize); +unsafe impl Sync for LazyModule {} +impl LazyModule { + /// Create a lazy-loaded handle to a module. + /// + /// # SAFETY + /// + /// This should only be used for system modules that will not be unloaded + /// for the lifetime of std (e.g. shell32 and combase). + pub const unsafe fn new(name: &'static CStr) -> Self { + let bytes = name.to_bytes_with_nul(); + let len = bytes.len(); + let ptr = ptr::from_ref(bytes).cast_mut().cast(); + Self(UnsafeCell::new(NonNull::new(ptr)), AtomicUsize::new(len)) + } + + /// Get the module handle, loading it if necessary. + pub fn load(&self) -> Option { + match self.1.load(Ordering::Acquire) { + // SAFETY: This module has been loaded. + 0 => unsafe { self.get_unchecked() }, + // SAFETY: This module is being loaded. + usize::MAX => unsafe { self.wait_unchecked() }, + len => self.load_inner(len), + } + } + + /// Get the already-acquired module handle. + /// + /// # SAFETY + /// + /// The `LazyModule` must be loaded. (`self.1 is 0`) + unsafe fn get_unchecked(&self) -> Option { + unsafe { *self.0.get() }.map(Module) + } + + /// Wait for this module to be loaded. + /// + /// # SAFETY + /// + /// Some thread must have started loading the module. (`self.1 is (0 | usize::MAX)`) + unsafe fn wait_unchecked(&self) -> Option { + futex_wait(&self.1, usize::MAX, None); + unsafe { *self.0.get() }.map(Module) + } + + /// Wake any threads waiting for this module to be loaded. + fn wake(&self) { + futex_wake_all(&self.1); + } + + #[cold] + fn load_inner(&self, len: usize) -> Option { + match self.1.compare_exchange(len, usize::MAX, Ordering::Relaxed, Ordering::Acquire) { + // SAFETY: this module has been loaded. + Err(0) => return unsafe { self.get_unchecked() }, + // SAFETY: this module is being loaded. + Err(usize::MAX) => return unsafe { self.wait_unchecked() }, + Err(_) => unreachable!(), + Ok(_) => {} + } + + let module = try { + // SAFETY: `self.0`` is a valid pointer to `len` bytes, and we have self.1 locked. + let bytes = NonNull::slice_from_raw_parts(unsafe { *self.0.get() }?.cast(), len); + // SAFETY: `bytes` was created from a valid CStr, so it is null-terminated. + let name = unsafe { CStr::from_bytes_with_nul_unchecked(bytes.as_ref()) }; + unsafe { Module::new(name).or_else(|| Module::load(name))? } + }; + + // SAFETY: We have `self.1` locked, and `self.1 == 0` expects `self.0` to be `Option`. + unsafe { *self.0.get() = module.map(|module| module.0) }; + self.1.store(0, Ordering::Release); + self.wake(); + module + } +} + /// Load a function or use a fallback implementation if that fails. macro_rules! compat_fn_with_fallback { (pub static $module:ident: &CStr = $name:expr; $( @@ -193,6 +303,67 @@ macro_rules! compat_fn_with_fallback { #[allow(unused)] $(#[$meta])* $vis use $symbol::call as $symbol; + )*); + (#[lazy] pub static $module:ident: &CStr = $name:expr; $( + $(#[$meta:meta])* + $vis:vis fn $symbol:ident($($argname:ident: $argtype:ty),*) -> $rettype:ty $fallback_body:block + )*) => ( + pub static $module: &CStr = $name; + $( + $(#[$meta])* + pub mod $symbol { + #[allow(unused_imports)] + use super::*; + use crate::mem; + use crate::ffi::CStr; + use crate::sync::atomic::{Atomic, AtomicPtr, Ordering}; + use crate::sys::compat::{LazyModule, Module}; + + type F = unsafe extern "system" fn($($argtype),*) -> $rettype; + + /// `PTR` contains a function pointer to one of three functions. + /// It starts with the `load` function. + /// When that is called it attempts to load the requested symbol. + /// If it succeeds, `PTR` is set to the address of that symbol. + /// If it fails, then `PTR` is set to `fallback`. + static PTR: Atomic<*mut c_void> = AtomicPtr::new(load as unsafe extern "system" fn($($argname: $argtype),*) -> $rettype as *mut _); + + unsafe extern "system" fn load($($argname: $argtype),*) -> $rettype { + unsafe { + static MODULE: LazyModule = unsafe { LazyModule::new($module) }; + let func = load_from_module(MODULE.load()); + func($($argname),*) + } + } + + fn load_from_module(module: Option) -> F { + unsafe { + static SYMBOL_NAME: &CStr = ansi_str!(sym $symbol); + if let Some(f) = module.and_then(|m| m.proc_address(SYMBOL_NAME)) { + PTR.store(f.as_ptr(), Ordering::Relaxed); + mem::transmute(f) + } else { + PTR.store(fallback as unsafe extern "system" fn($($argname: $argtype),*) -> $rettype as *mut _, Ordering::Relaxed); + fallback + } + } + } + + #[allow(unused_variables)] + unsafe extern "system" fn fallback($($argname: $argtype),*) -> $rettype { + $fallback_body + } + + #[inline(always)] + pub unsafe fn call($($argname: $argtype),*) -> $rettype { + unsafe { + let func: F = mem::transmute(PTR.load(Ordering::Relaxed)); + func($($argname),*) + } + } + } + $(#[$meta])* + $vis use $symbol::call as $symbol; )*) } From 423b88466e422d23d02759b0c6f830e4e4101c58 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:05:29 -0400 Subject: [PATCH 48/60] comment why we lazy-load these DLLs --- library/std/src/sys/pal/windows/c.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/std/src/sys/pal/windows/c.rs b/library/std/src/sys/pal/windows/c.rs index 2fd7feb398dd0..58f3d74352495 100644 --- a/library/std/src/sys/pal/windows/c.rs +++ b/library/std/src/sys/pal/windows/c.rs @@ -248,6 +248,7 @@ unsafe extern "C" { // Functions in DLLs that we want to lazily load compat_fn_with_fallback! { + // Avoid eagerly linking shell32.dll, which marks the loading application as graphical. #[lazy] pub static SHELL32: &CStr = c"shell32"; @@ -257,6 +258,7 @@ compat_fn_with_fallback! { } compat_fn_with_fallback! { + // Only used with SHGetKnownFolderPath, so avoid eager load overhead. #[lazy] pub static OLE32: &CStr = c"ole32"; From 594fa179f10e9a1991dbf80a26012a262424071d Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:08:57 -0400 Subject: [PATCH 49/60] Avoid using futex on Win7 --- library/std/src/sys/pal/windows/compat.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/pal/windows/compat.rs b/library/std/src/sys/pal/windows/compat.rs index ce15c1a25bf76..f158894dfa5be 100644 --- a/library/std/src/sys/pal/windows/compat.rs +++ b/library/std/src/sys/pal/windows/compat.rs @@ -24,6 +24,7 @@ use crate::ffi::{CStr, c_void}; use crate::ptr::{self, NonNull}; use crate::sync::atomic::{AtomicUsize, Ordering}; use crate::sys::c; +#[cfg(not(target_family = "win7"))] use crate::sys::pal::windows::futex::{futex_wait, futex_wake_all}; // This uses a static initializer to preload some imported functions. @@ -205,12 +206,18 @@ impl LazyModule { /// /// Some thread must have started loading the module. (`self.1 is (0 | usize::MAX)`) unsafe fn wait_unchecked(&self) -> Option { - futex_wait(&self.1, usize::MAX, None); - unsafe { *self.0.get() }.map(Module) + while self.1.load(Ordering::Acquire) != 0 { + #[cfg(target_family = "win7")] + futex_wait(&self.1, usize::MAX, None); + #[cfg(not(target_family = "win7"))] + crate::hint::spin_loop(); // no WaitOnAddress, so fall back to busy-wait + } + unsafe { self.get_unchecked() } } /// Wake any threads waiting for this module to be loaded. fn wake(&self) { + #[cfg(not(target_family = "win7"))] futex_wake_all(&self.1); } From a1ec10eeea6c962cbc6f56edb2c8400aaf9ef6cc Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:12:11 -0400 Subject: [PATCH 50/60] fix feature gating --- library/std/src/os/windows/fs/dirs.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index b66ea48bf8cc8..85d81b726dfbe 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -49,6 +49,7 @@ pub impl(self) trait HomeDirsExt: Sized { /// [`config_home`]: HomeDirs::config_home /// [`data_home`]: HomeDirs::data_home /// [`state_home`]: HomeDirs::state_home + #[unstable(feature = "dir_discovery", issue = "157515")] fn appdata_env() -> io::Result; /// Load the known user folder paths using the [Known Folders] API. @@ -188,7 +189,7 @@ impl HomeDirsExt for HomeDirs { } #[cfg(windows)] -#[unstable(feature = "dir_discovery", issue = "157515")] +#[unstable(feature = "media_dir_discovery", issue = "157515")] impl MediaDirsExt for MediaDirs { fn known_folders() -> io::Result { use crate::sys::c; From cc54b7cad6e9be246e50b10f2a015072e33b551a Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:12:57 -0400 Subject: [PATCH 51/60] fix can_fetch_known_folder_paths test --- library/std/src/os/windows/fs/dirs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 85d81b726dfbe..ffe65a3577f0a 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -286,13 +286,12 @@ mod tests { #[test] fn can_fetch_known_folder_paths() { let dirs = HomeDirs::known_folders().unwrap(); - assert!(dirs.user_home().is_some()); assert!(dirs.cache_home().is_some()); assert!(dirs.config_home().is_some()); assert!(dirs.data_home().is_some()); + assert!(dirs.state_home().is_some()); let dirs = MediaDirs::known_folders().unwrap(); - assert!(dirs.state_home().is_some()); assert!(dirs.desktop().is_some()); assert!(dirs.documents().is_some()); assert!(dirs.downloads().is_some()); From 2e5ed945227c57679973ab43faa4b491a3be3a08 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:15:21 -0400 Subject: [PATCH 52/60] don't panic in shell32/ole32 fallbacks --- library/std/src/sys/pal/windows/c.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/pal/windows/c.rs b/library/std/src/sys/pal/windows/c.rs index 58f3d74352495..ec32430d95e3e 100644 --- a/library/std/src/sys/pal/windows/c.rs +++ b/library/std/src/sys/pal/windows/c.rs @@ -253,7 +253,7 @@ compat_fn_with_fallback! { pub static SHELL32: &CStr = c"shell32"; pub fn SHGetKnownFolderPath(rfid: *const GUID, dwflags: u32, htoken: HANDLE, ppszpath: *mut PWSTR) -> HRESULT { - panic!("Known Folders not available") + unsafe { SetLastError(ERROR_CALL_NOT_IMPLEMENTED as u32); E_NOTIMPL } } } @@ -263,6 +263,6 @@ compat_fn_with_fallback! { pub static OLE32: &CStr = c"ole32"; pub fn CoTaskMemFree(pv: *const core::ffi::c_void) -> () { - panic!("COM not available") + // without OLE32 there's no COM alloc, so no-op COM free is fine } } From 2007586a43ac988b1ef066a8b777402c2d5494bb Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 00:35:40 -0400 Subject: [PATCH 53/60] properly filter out relative paths in XDG MediaDirsExt --- library/std/src/os/unix/fs/dirs.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index ff671504db648..7c2b118f35aa9 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -272,11 +272,6 @@ impl MediaDirsExt for MediaDirs { let Some(val) = split.next() else { continue }; debug_assert_eq!(split.next(), None); - // ensure the path is absolute or $HOME-relative - if !val.starts_with(b"\"/") && !val.starts_with(b"\"$HOME/") { - continue; - } - // the path value is quoted; unquote it let Some(path) = xdg_unquote(val) else { continue }; @@ -287,15 +282,23 @@ impl MediaDirsExt for MediaDirs { PathBuf::from(OsString::from_vec(path.into_owned())) }; + // ignore relative paths + if path.is_relative() { + continue; + } + + // setting to the home dir disables the directory configuration + let path = (path != user_home).then_some(path); + // load the known user directories match var { - b"XDG_DESKTOP_DIR" => dirs.desktop = Some(path), - b"XDG_DOCUMENTS_DIR" => dirs.documents = Some(path), - b"XDG_DOWNLOAD_DIR" => dirs.downloads = Some(path), - b"XDG_MUSIC_DIR" => dirs.music = Some(path), - b"XDG_PICTURES_DIR" => dirs.pictures = Some(path), - b"XDG_VIDEOS_DIR" => dirs.videos = Some(path), - b"XDG_TEMPLATES_DIR" => dirs.extra.templates = Some(path), + b"XDG_DESKTOP_DIR" => dirs.desktop = path, + b"XDG_DOCUMENTS_DIR" => dirs.documents = path, + b"XDG_DOWNLOAD_DIR" => dirs.downloads = path, + b"XDG_MUSIC_DIR" => dirs.music = path, + b"XDG_PICTURES_DIR" => dirs.pictures = path, + b"XDG_VIDEOS_DIR" => dirs.videos = path, + b"XDG_TEMPLATES_DIR" => dirs.extra.templates = path, _ => { // ignore unknown variable assignment } From 7f5f83935c4edbc80c6f5b32519676730f547f62 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 01:21:36 -0400 Subject: [PATCH 54/60] improve user_dirs.dirs parsing --- library/std/src/os/darwin/fs/dirs.rs | 4 +- library/std/src/os/unix/fs/dirs.rs | 105 ++++++++++++++++----------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index c2d985cd11b16..8a18f1d1083d1 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -121,8 +121,8 @@ pub impl(self) trait MediaDirsExt: Sized { fn user_home() -> io::Result { env::home_dir() - .filter(|p| !p.is_empty()) - .ok_or(const_error!(ErrorKind::NotFound, "no home directory")) + .filter(|p| p.is_absolute()) + .ok_or(const_error!(ErrorKind::InvalidData, "home path not absolute")) } #[unstable(feature = "dir_discovery", issue = "157515")] diff --git a/library/std/src/os/unix/fs/dirs.rs b/library/std/src/os/unix/fs/dirs.rs index 7c2b118f35aa9..3c9dd2279ca39 100644 --- a/library/std/src/os/unix/fs/dirs.rs +++ b/library/std/src/os/unix/fs/dirs.rs @@ -1,4 +1,3 @@ -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}; @@ -257,41 +256,11 @@ impl MediaDirsExt for MediaDirs { 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); - - // the path value is quoted; unquote it - let Some(path) = xdg_unquote(val) else { continue }; - - // expand the $HOME prefix if present - let path = if path.starts_with(b"$HOME/") { - user_home.join(OsStr::from_bytes(&path[6..])) - } else { - PathBuf::from(OsString::from_vec(path.into_owned())) - }; - - // ignore relative paths - if path.is_relative() { - continue; - } - - // setting to the home dir disables the directory configuration - let path = (path != user_home).then_some(path); - + for (xdg, path) in + spec.split(|&b| b == b'\n').flat_map(|line| parse_user_dirs_line(line, &user_home)) + { // load the known user directories - match var { + match xdg { b"XDG_DESKTOP_DIR" => dirs.desktop = path, b"XDG_DOCUMENTS_DIR" => dirs.documents = path, b"XDG_DOWNLOAD_DIR" => dirs.downloads = path, @@ -320,8 +289,8 @@ impl MediaDirsExt for MediaDirs { fn user_home() -> io::Result { env::home_dir() - .filter(|p| !p.is_empty()) - .ok_or(const_error!(ErrorKind::NotFound, "no home directory")) + .filter(|p| p.is_absolute()) + .ok_or(const_error!(ErrorKind::InvalidData, "home path not absolute")) } fn xdg_dir(env: &str, fallback: impl FnOnce() -> PathBuf) -> PathBuf { @@ -332,15 +301,51 @@ fn xdg_env(env: &str, fallback: &str) -> OsString { var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| OsString::from(fallback)) } -fn xdg_unquote(bytes: &[u8]) -> Option> { - let [b'"', bytes @ .., b'"'] = bytes else { return None }; - - if !bytes.contains(&b'\\') { - return Some(Cow::Borrowed(bytes)); +fn parse_user_dirs_line<'a>( + line: &'a [u8], + user_home: &Path, +) -> Option<(&'a [u8], Option)> { + // trim whitespace + let trimmed = line.trim_ascii(); + // skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with(&[b'#']) { + return None; } + // 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 var = split.next()?; + let val = split.next()?; + debug_assert_eq!(split.next(), None); + + // the path value is quoted; unquote it + let path = xdg_unquote(val, user_home)?; + + let path = Some(path) + // ignore non-absolute paths + .filter(|path| path.is_absolute()) + // setting to the home dir disables the directory configuration + .filter(|path| path != user_home); + + Some((var, path)) +} + +fn xdg_unquote(bytes: &[u8], user_home: &Path) -> Option { + let [b'"', bytes @ .., b'"'] = bytes else { return None }; + let mut rest = bytes; let mut s = Vec::with_capacity(rest.len()); + + // expand leading $HOME only + if rest.starts_with(b"$HOME/") { + s.extend_from_slice(user_home.as_os_str().as_bytes()); + if !user_home.has_trailing_sep() { + s.push(b'/'); + } + rest = &rest[6..]; + } + loop { let i = rest .iter() @@ -369,7 +374,7 @@ fn xdg_unquote(bytes: &[u8]) -> Option> { } } - Some(Cow::Owned(s)) + Some(PathBuf::from(OsString::from_vec(s))) } #[cfg(test)] @@ -409,4 +414,18 @@ mod tests { assert!(dirs.videos().is_some()); assert!(dirs.templates().is_some()); } + + #[test] + fn test_user_dirs_parsing() { + assert_eq!( + parse_user_dirs_line( + br#"XDG_TEMPLATES_DIR="$HOME/My \"Quoted\" Templates""#, + Path::new("/home/ferris"), + ), + Some(( + &b"XDG_TEMPLATES_DIR"[..], + Some(Path::new("/home/ferris/My \"Quoted\" Templates").to_path_buf()), + )) + ); + } } From 095a693b55b01f0b545ad5e78f11a149135434c1 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 01:26:13 -0400 Subject: [PATCH 55/60] require absolute %APPDATA% paths --- library/std/src/os/windows/fs/dirs.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index ffe65a3577f0a..03a6eaf304073 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -24,7 +24,7 @@ pub impl(self) trait HomeDirsExt: Sized { /// /// # Errors /// - /// Errors if `%APPDATA%` or `%LOCALAPPDATA%` are not set or are non-UTF-8. + /// Errors if `%APPDATA%` or `%LOCALAPPDATA%` are not set to absolute paths. /// /// # Implementation-specific behavior /// @@ -151,9 +151,14 @@ pub impl(self) trait MediaDirsExt: Sized { #[unstable(feature = "dir_discovery", issue = "157515")] impl HomeDirsExt for HomeDirs { fn appdata_env() -> io::Result { - let wrap_err = |e: env::VarError| io::Error::new(io::ErrorKind::NotFound, e); - let local_app_data = PathBuf::from(env::var("LOCALAPPDATA").map_err(wrap_err)?); - let roaming_app_data = PathBuf::from(env::var("APPDATA").map_err(wrap_err)?); + let roaming_app_data = env::var_os("APPDATA") + .map(PathBuf::from) + .filter(|p| p.is_absolute()) + .ok_or(const_error!(io::ErrorKind::InvalidData, "non-absolute %APPDATA%"))?; + let local_app_data = env::var_os("LOCALAPPDATA") + .map(PathBuf::from) + .filter(|p| p.is_absolute()) + .ok_or(const_error!(io::ErrorKind::InvalidData, "non-absolute %LOCALAPPDATA%"))?; // AppData/Local -- system-local, doesn't make sense to sync to another // AppData/Roaming -- data that makes sense to sync across machines From 14cee7b669cd62ae55880639beb6fa1cdb4f53fa Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 01:33:21 -0400 Subject: [PATCH 56/60] improve windows userdirs impl --- library/std/src/os/darwin/fs/dirs.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/library/std/src/os/darwin/fs/dirs.rs b/library/std/src/os/darwin/fs/dirs.rs index 8a18f1d1083d1..fb436b413ac29 100644 --- a/library/std/src/os/darwin/fs/dirs.rs +++ b/library/std/src/os/darwin/fs/dirs.rs @@ -9,7 +9,7 @@ pub impl(self) trait HomeDirsExt: Sized { /// Load the standard user directory paths for the current user. /// /// On iOS, tvOS, watchOS, visionOS, and sandboxed macOS applications, - /// these directories are within the application's sandbox bundle. Outside + /// these directories are within the application's container. Outside /// the sandbox, these are subdirectories of the `~/Library` directory on /// macOS. /// @@ -34,11 +34,10 @@ pub impl(self) trait HomeDirsExt: Sized { /// /// # Errors /// - /// Errors if the the [user home](env::home_dir) cannot be determined. + /// Errors if 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 user domain always returns subdirectory paths of `~`; and // - the username and OS defined path segments are always valid UTF-8. /// /// # Implementation-specific behavior @@ -113,7 +112,6 @@ pub impl(self) trait MediaDirsExt: Sized { /// [`NSDownloadsDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/downloadsdirectory?language=objc /// [`NSMusicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/musicdirectory?language=objc /// [`NSPicturesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/picturesdirectory?language=objc - /// [`NSSharedPublicDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/sharedpublicdirectory?language=objc /// [`NSMoviesDirectory`]: https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/moviesdirectory?language=objc #[unstable(feature = "media_dir_discovery", issue = "157515")] fn sysdir() -> io::Result; @@ -193,6 +191,7 @@ mod sys { let Some(path) = iter.next() else { return Ok(None); }; + let path = path?; if iter.next().is_some() { // more than one path returned (shouldn't happen for SYSDIR_DOMAIN_MASK_USER) @@ -202,7 +201,7 @@ mod sys { )); } - Ok(Some(path?)) + Ok(Some(path)) } struct Iter<'a> { @@ -251,7 +250,7 @@ mod sys { } let Ok(path) = CStr::from_bytes_until_nul(&buf) else { - // should be impossible given username length limits, but be defensive + // should be impossible given home-relative paths, but be defensive return Some(Err(const_error!( ErrorKind::InvalidData, "standard user directory path too long", From cbde09292dec36ec5baf9d423eb9cc70e8872ee7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 01:38:35 -0400 Subject: [PATCH 57/60] fix win7 futex handling --- library/std/src/sys/pal/windows/compat.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/std/src/sys/pal/windows/compat.rs b/library/std/src/sys/pal/windows/compat.rs index f158894dfa5be..63a02b46c203c 100644 --- a/library/std/src/sys/pal/windows/compat.rs +++ b/library/std/src/sys/pal/windows/compat.rs @@ -24,7 +24,7 @@ use crate::ffi::{CStr, c_void}; use crate::ptr::{self, NonNull}; use crate::sync::atomic::{AtomicUsize, Ordering}; use crate::sys::c; -#[cfg(not(target_family = "win7"))] +#[cfg(not(target_vendor = "win7"))] use crate::sys::pal::windows::futex::{futex_wait, futex_wake_all}; // This uses a static initializer to preload some imported functions. @@ -207,9 +207,9 @@ impl LazyModule { /// Some thread must have started loading the module. (`self.1 is (0 | usize::MAX)`) unsafe fn wait_unchecked(&self) -> Option { while self.1.load(Ordering::Acquire) != 0 { - #[cfg(target_family = "win7")] + #[cfg(not(target_vendor = "win7"))] futex_wait(&self.1, usize::MAX, None); - #[cfg(not(target_family = "win7"))] + #[cfg(target_vendor = "win7")] crate::hint::spin_loop(); // no WaitOnAddress, so fall back to busy-wait } unsafe { self.get_unchecked() } From f956d962f9a816ff1f1eeb23953b9ae04de75fe2 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 01:45:36 -0400 Subject: [PATCH 58/60] unstable gate fix --- 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 03a6eaf304073..837bee06b5e8d 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -143,7 +143,7 @@ pub impl(self) trait MediaDirsExt: Sized { /// [`music`]: MediaDirs::music /// [`pictures`]: MediaDirs::pictures /// [`videos`]: MediaDirs::videos - #[unstable(feature = "dir_discovery", issue = "157515")] + #[unstable(feature = "media_dir_discovery", issue = "157515")] fn known_folders() -> io::Result; } From 64fa9fd927838053b3034481cda698d4cc5f6618 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 12:33:00 -0400 Subject: [PATCH 59/60] windows fs::dirs fixes --- library/std/src/os/windows/fs/dirs.rs | 1 + library/std/src/sys/pal/windows/compat.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/std/src/os/windows/fs/dirs.rs b/library/std/src/os/windows/fs/dirs.rs index 837bee06b5e8d..9b8418ca1fe77 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -1,4 +1,5 @@ use crate::fs::{HomeDirs, MediaDirs}; +use crate::io::{ErrorKind, const_error}; use crate::path::PathBuf; use crate::{env, io}; diff --git a/library/std/src/sys/pal/windows/compat.rs b/library/std/src/sys/pal/windows/compat.rs index 63a02b46c203c..572959947a937 100644 --- a/library/std/src/sys/pal/windows/compat.rs +++ b/library/std/src/sys/pal/windows/compat.rs @@ -217,7 +217,7 @@ impl LazyModule { /// Wake any threads waiting for this module to be loaded. fn wake(&self) { - #[cfg(not(target_family = "win7"))] + #[cfg(not(target_vendor = "win7"))] futex_wake_all(&self.1); } From a11e762a56c12934d61d344099c7aba4b8e35eb9 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 23 Jul 2026 12:52:20 -0400 Subject: [PATCH 60/60] yip yip --- 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 9b8418ca1fe77..ae7c99530445b 100644 --- a/library/std/src/os/windows/fs/dirs.rs +++ b/library/std/src/os/windows/fs/dirs.rs @@ -155,11 +155,11 @@ impl HomeDirsExt for HomeDirs { let roaming_app_data = env::var_os("APPDATA") .map(PathBuf::from) .filter(|p| p.is_absolute()) - .ok_or(const_error!(io::ErrorKind::InvalidData, "non-absolute %APPDATA%"))?; + .ok_or(const_error!(ErrorKind::InvalidData, "non-absolute %APPDATA%"))?; let local_app_data = env::var_os("LOCALAPPDATA") .map(PathBuf::from) .filter(|p| p.is_absolute()) - .ok_or(const_error!(io::ErrorKind::InvalidData, "non-absolute %LOCALAPPDATA%"))?; + .ok_or(const_error!(ErrorKind::InvalidData, "non-absolute %LOCALAPPDATA%"))?; // AppData/Local -- system-local, doesn't make sense to sync to another // AppData/Roaming -- data that makes sense to sync across machines