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..b51b8ac916c24 --- /dev/null +++ b/library/std/src/os/unix/xdg.rs @@ -0,0 +1,167 @@ +//! 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") +}