From 073e6496f6e482eb25dbd2bbbb61f7e441f74cef Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Fri, 5 Jun 2026 21:53:22 -0400 Subject: [PATCH 1/7] Add xdg_basedir API --- library/std/src/env.rs | 1 + library/std/src/os/unix/mod.rs | 1 + library/std/src/os/unix/xdg.rs | 166 +++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 library/std/src/os/unix/xdg.rs diff --git a/library/std/src/env.rs b/library/std/src/env.rs index a4a5e8561dfcf..b377110462d61 100644 --- a/library/std/src/env.rs +++ b/library/std/src/env.rs @@ -600,6 +600,7 @@ 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 78c957270c451..e3f15a00a0944 100644 --- a/library/std/src/os/unix/mod.rs +++ b/library/std/src/os/unix/mod.rs @@ -94,6 +94,7 @@ 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 new file mode 100644 index 0000000000000..6f54e7f266444 --- /dev/null +++ b/library/std/src/os/unix/xdg.rs @@ -0,0 +1,166 @@ +//! XDG (X Desktop Group) related functionality for Unix platforms. +//! +//! The [XDG Base Directory Specification][basedir] defines where user-specific +//! files should be looked for relative to a set of base directories. 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::{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: impl AsRef) -> 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 caches 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 +// +// This stores Option so we can track when we have a trailing empty component. +// None is an exhausted iterator, Some("") is a trailing empty component. +#[derive(Debug, Clone)] +pub struct XdgDirsIter(Option); + +impl XdgDirsIter { + fn new(env: &str, default: impl AsRef) -> Self { + let dirs = var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| default.as_ref().into()); + Self(Some(dirs)) + } +} + +impl Iterator for XdgDirsIter { + type Item = PathBuf; + + fn next(&mut self) -> Option { + let dirs = self.0.take()?; + let next = split_paths(&dirs).next()?; + let len = next.as_os_str().len(); + let mut bytes = dirs.into_encoded_bytes(); + if len < bytes.len() { + // Remove the path about to be returned and the separator after it. + bytes.drain(..len + 1); + // SAFETY: UNIX guarantees that the path separator is b':'. As `bytes` + // now holds the suffix after the separator, it's a valid OsStr. + self.0 = Some(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }); + } + Some(next) + } + + fn size_hint(&self) -> (usize, Option) { + let Some(dirs) = &self.0 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 9534d0f0639f76ca67f5f10376515491e0988662 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Sun, 7 Jun 2026 21:11:11 -0400 Subject: [PATCH 2/7] update module doc comment after review --- library/std/src/os/unix/xdg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index 6f54e7f266444..e1540db702342 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -1,7 +1,7 @@ //! XDG (X Desktop Group) related functionality for Unix platforms. //! -//! The [XDG Base Directory Specification][basedir] defines where user-specific -//! files should be looked for relative to a set of base directories. The +//! The [XDG Base Directory Specification][basedir] defines where 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. //! From 2b3d2733eeaea93921502337a94241d94d739de7 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Sun, 7 Jun 2026 21:12:30 -0400 Subject: [PATCH 3/7] reword xdg::cache_home_dir doc per review --- library/std/src/os/unix/xdg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index e1540db702342..cb04722bd55b8 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -36,7 +36,7 @@ fn xdg_home_dir() -> PathBuf { } } -fn xdg_dir(env: &str, fallback_home_subdir: impl AsRef) -> PathBuf { +fn xdg_dir(env: &str, fallback_home_subdir: impl AsRef) -> PathBuf { var_os(env) .filter(|s| !s.is_empty()) .map(PathBuf::from) @@ -72,7 +72,7 @@ pub fn state_home_dir() -> PathBuf { xdg_dir("XDG_STATE_HOME", ".local/state") } -/// A base directory relative to which user-specific non-essential caches should be written. +/// 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}/**/*`. From 7b4630613a45751b592d093ae647c2898907e4f4 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Sun, 7 Jun 2026 21:14:25 -0400 Subject: [PATCH 4/7] remove unnecessary helper generics --- library/std/src/os/unix/xdg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index cb04722bd55b8..23ba125f64ca7 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -36,7 +36,7 @@ fn xdg_home_dir() -> PathBuf { } } -fn xdg_dir(env: &str, fallback_home_subdir: impl AsRef) -> PathBuf { +fn xdg_dir(env: &str, fallback_home_subdir: &str) -> PathBuf { var_os(env) .filter(|s| !s.is_empty()) .map(PathBuf::from) @@ -96,8 +96,8 @@ pub fn cache_home_dir() -> PathBuf { pub struct XdgDirsIter(Option); impl XdgDirsIter { - fn new(env: &str, default: impl AsRef) -> Self { - let dirs = var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| default.as_ref().into()); + fn new(env: &str, default: &str) -> Self { + let dirs = var_os(env).filter(|s| !s.is_empty()).unwrap_or_else(|| default.into()); Self(Some(dirs)) } } From 593a847e5fe4f1e8d886b4ce6ce38518ed60af13 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Sun, 7 Jun 2026 21:25:10 -0400 Subject: [PATCH 5/7] reduce byte shuffling in XdgDirsIter --- library/std/src/os/unix/xdg.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index 23ba125f64ca7..f15926e98500b 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -89,16 +89,24 @@ pub fn cache_home_dir() -> PathBuf { /// /// [`xdg::data_dirs`]: data_dirs /// [`xdg::config_dirs`]: config_dirs -// -// This stores Option so we can track when we have a trailing empty component. -// None is an exhausted iterator, Some("") is a trailing empty component. #[derive(Debug, Clone)] -pub struct XdgDirsIter(Option); +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(Some(dirs)) + 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) } + }) } } @@ -106,22 +114,15 @@ impl Iterator for XdgDirsIter { type Item = PathBuf; fn next(&mut self) -> Option { - let dirs = self.0.take()?; - let next = split_paths(&dirs).next()?; + let rest = self.remaining()?; + let next = split_paths(rest).next()?; let len = next.as_os_str().len(); - let mut bytes = dirs.into_encoded_bytes(); - if len < bytes.len() { - // Remove the path about to be returned and the separator after it. - bytes.drain(..len + 1); - // SAFETY: UNIX guarantees that the path separator is b':'. As `bytes` - // now holds the suffix after the separator, it's a valid OsStr. - self.0 = Some(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }); - } + 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.0 else { return (0, Some(0)) }; + let Some(dirs) = self.remaining() else { return (0, Some(0)) }; split_paths(dirs).size_hint() } } From 6e670de6167f2ada1469b7441200a64c889189b6 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Sun, 7 Jun 2026 22:04:48 -0400 Subject: [PATCH 6/7] remove unused import --- library/std/src/os/unix/xdg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index f15926e98500b..cd03ead26b48a 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -23,7 +23,7 @@ use crate::env::{home_dir, split_paths, var_os}; use crate::ffi::{OsStr, OsString}; -use crate::path::{Path, PathBuf}; +use crate::path::PathBuf; fn xdg_home_dir() -> PathBuf { // Note: home_dir can return `Some("")` in some cases. We assume that in From 378da804d6fdf2d7beb63ab3943d688897e4c870 Mon Sep 17 00:00:00 2001 From: Crystal Durham Date: Thu, 11 Jun 2026 17:10:05 -0400 Subject: [PATCH 7/7] Remove redundant word in doc comment --- library/std/src/os/unix/xdg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/unix/xdg.rs b/library/std/src/os/unix/xdg.rs index cd03ead26b48a..b51b8ac916c24 100644 --- a/library/std/src/os/unix/xdg.rs +++ b/library/std/src/os/unix/xdg.rs @@ -1,6 +1,6 @@ //! XDG (X Desktop Group) related functionality for Unix platforms. //! -//! The [XDG Base Directory Specification][basedir] defines where a set of base +//! 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.