home_dir() was already computed internally (home_or_root) but not exposed — breadarr-shared's own expand_home() helper needs it to fix the same bug class documented in this module: its own fallback (when HOME itself isn't set) returned the literal unexpanded "~/..." input string instead of a real path. Also documented breadmon/src/profile.rs's profiles_dir(), fixed in that repo's own commit.
123 lines
4.4 KiB
Rust
123 lines
4.4 KiB
Rust
//! XDG base directory helpers.
|
|
//!
|
|
//! Several repos independently rolled `dirs::data_local_dir().unwrap_or_else(||
|
|
//! PathBuf::from("~/.local/share"))`-shaped fallbacks. The literal-tilde
|
|
//! string is the bug: `PathBuf`/`std::fs` never expand `~`, so on the rare
|
|
//! box where `dirs` can't resolve a home directory (no `HOME` env var, e.g.
|
|
//! some container/systemd-service contexts) the fallback silently resolves
|
|
//! to a directory literally named `~` in the process's current working
|
|
//! directory instead of the user's actual home. Confirmed present in:
|
|
//! - `breadclip-core/src/lib.rs:171-175` (`data_dir`)
|
|
//! - `breadpad-shared/src/classifier.rs:34-39` (`model_dir`)
|
|
//! - `breadpad-shared/src/config.rs:214-219` and `:221-226`
|
|
//! (`config_path`, `style_css_path`)
|
|
//! - `breadmon/src/profile.rs:31-35` (`profiles_dir`)
|
|
//! - `breadarr-shared/src/config.rs:316-321`'s own `expand_home` helper,
|
|
//! which had the same bug in a different shape: its *own* fallback (when
|
|
//! `HOME` itself isn't set) returned the literal, unexpanded input string
|
|
//! rather than a real path.
|
|
//!
|
|
//! The helpers here resolve a real `$HOME` (via `dirs::home_dir()`, which
|
|
//! itself falls back to reading `HOME` directly) before ever falling back,
|
|
//! so the fallback path is always an absolute, expanded path.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
/// A real, absolute home directory — `dirs::home_dir()`, falling back to
|
|
/// `/root` only if that itself fails (no `HOME` env var *and* no passwd-db
|
|
/// entry, e.g. some minimal container contexts). Never a literal `"~"`.
|
|
pub fn home_dir() -> PathBuf {
|
|
home_or_root()
|
|
}
|
|
|
|
fn home_or_root() -> PathBuf {
|
|
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/root"))
|
|
}
|
|
|
|
/// `$XDG_CONFIG_HOME` (only if it's set to an absolute path) or `~/.config`,
|
|
/// joined with `app`.
|
|
pub fn config_dir(app: &str) -> PathBuf {
|
|
config_home().join(app)
|
|
}
|
|
|
|
/// The bare `$XDG_CONFIG_HOME` (or `~/.config`) directory, with no app name
|
|
/// joined on — for callers that build up multiple sub-paths themselves
|
|
/// (e.g. `bos-settings`, which joins a different bread* app's name per
|
|
/// config file it edits).
|
|
pub fn config_home() -> PathBuf {
|
|
base_config_dir()
|
|
}
|
|
|
|
/// `$XDG_DATA_HOME` (only if absolute) or `~/.local/share`, joined with `app`.
|
|
pub fn data_dir(app: &str) -> PathBuf {
|
|
dirs::data_local_dir()
|
|
.unwrap_or_else(|| home_or_root().join(".local/share"))
|
|
.join(app)
|
|
}
|
|
|
|
/// `$XDG_CACHE_HOME` (only if absolute) or `~/.cache`, joined with `app`.
|
|
pub fn cache_dir(app: &str) -> PathBuf {
|
|
dirs::cache_dir()
|
|
.unwrap_or_else(|| home_or_root().join(".cache"))
|
|
.join(app)
|
|
}
|
|
|
|
/// `$XDG_RUNTIME_DIR`, falling back to `/tmp` — matches the fallback every
|
|
/// consumer (breadbox, breadclip, breadmon) already used for PID/socket
|
|
/// scratch files, which don't need to survive a reboot.
|
|
pub fn runtime_dir() -> PathBuf {
|
|
std::env::var_os("XDG_RUNTIME_DIR")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
|
}
|
|
|
|
fn base_config_dir() -> PathBuf {
|
|
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
|
|
let p = PathBuf::from(xdg);
|
|
if p.is_absolute() {
|
|
return p;
|
|
}
|
|
}
|
|
dirs::config_dir().unwrap_or_else(|| home_or_root().join(".config"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn config_dir_joins_app_name() {
|
|
let d = config_dir("breadpad");
|
|
assert!(d.ends_with("breadpad"));
|
|
assert!(d.is_absolute());
|
|
}
|
|
|
|
#[test]
|
|
fn home_dir_is_absolute_and_never_a_literal_tilde() {
|
|
let d = home_dir();
|
|
assert!(d.is_absolute());
|
|
assert!(!d.components().any(|c| c.as_os_str() == "~"));
|
|
}
|
|
|
|
#[test]
|
|
fn data_dir_never_contains_literal_tilde() {
|
|
// Regression guard for the exact bug this module replaces: the
|
|
// fallback must never be a literal "~/..." path component.
|
|
let d = data_dir("breadclip");
|
|
assert!(!d.components().any(|c| c.as_os_str() == "~"));
|
|
assert!(d.is_absolute());
|
|
}
|
|
|
|
#[test]
|
|
fn cache_dir_is_absolute() {
|
|
assert!(cache_dir("breadsearch").is_absolute());
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_dir_falls_back_to_tmp() {
|
|
let _lock = crate::env_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
|
// We don't unset XDG_RUNTIME_DIR here (test isolation), just confirm
|
|
// the function returns *something* absolute either way.
|
|
assert!(runtime_dir().is_absolute());
|
|
}
|
|
}
|