fix: config path advisory ignores XDG file on interactive root (#81) (#83)

Port-53 and TLS-data-dir advisories told users to create
~/.config/numa/numa.toml, but config_dir() routed root to
/var/lib/numa/ and load_config never consulted the XDG path, so
the file the user created was silently ignored.

New suggested_config_path() helper prefers $HOME/.config/numa/
when HOME is set (and isn't "/" or empty), with config_dir() as
lazy fallback. Used by both advisories and by load_config as an
additional candidate, so the advised path is the path numa
actually reads. Runtime state (services.json, TLS CA) stays in
FHS — config_dir()/data_dir() are intentionally unchanged to
keep continuity with the installed daemon.

End-to-end replication + regression check in
tests/docker/issue-81.sh: four scenarios (replication and
existing-install, each against main and fix), all matching
expectations.
This commit was merged in pull request #83.
This commit is contained in:
Razvan Dimescu
2026-04-12 02:17:33 +03:00
committed by GitHub
parent 289f2b973b
commit 22bebb85a0
6 changed files with 288 additions and 9 deletions

View File

@@ -44,6 +44,42 @@ pub fn hostname() -> String {
.unwrap_or_else(|| "numa".to_string())
}
/// Path to suggest to an interactive user when asking them to create
/// `numa.toml`. Prefers `$HOME/.config/numa/numa.toml` when HOME is set
/// (actionable without sudo); falls back to `config_dir()` otherwise.
///
/// Note: `config_dir()` routes interactive root to FHS (`/var/lib/numa`)
/// so that runtime state like `services.json` stays continuous with the
/// installed daemon. This helper exists specifically to give advisories
/// and `load_config` an XDG-aware path for user-authored config, without
/// moving runtime state out of FHS — see issue #81.
pub(crate) fn suggested_config_path() -> std::path::PathBuf {
#[cfg(not(windows))]
{
resolve_suggested_config_path(std::env::var("HOME").ok().as_deref(), config_dir)
}
#[cfg(windows)]
{
config_dir().join("numa.toml")
}
}
#[cfg(not(windows))]
fn resolve_suggested_config_path<F>(home: Option<&str>, fallback_dir: F) -> std::path::PathBuf
where
F: FnOnce() -> std::path::PathBuf,
{
if let Some(home) = home {
if !home.is_empty() && home != "/" {
return std::path::PathBuf::from(home)
.join(".config")
.join("numa")
.join("numa.toml");
}
}
fallback_dir().join("numa.toml")
}
/// Shared config directory for persistent data (services.json, etc).
/// Unix users: ~/.config/numa/
/// Linux root daemon: /var/lib/numa (FHS) — falls back to /usr/local/var/numa
@@ -163,4 +199,73 @@ mod tests {
fn linux_data_dir_only_fhs_uses_fhs() {
assert_eq!(resolve_linux_data_dir(false, true), "/var/lib/numa");
}
#[cfg(not(windows))]
fn fhs() -> std::path::PathBuf {
std::path::PathBuf::from("/var/lib/numa")
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_prefers_home() {
assert_eq!(
resolve_suggested_config_path(Some("/home/alice"), fhs),
std::path::PathBuf::from("/home/alice/.config/numa/numa.toml"),
);
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_prefers_root_home_over_fhs() {
// Interactive root: HOME=/root is a real user context, not a daemon signal.
// Advisory must point where load_config will actually look — issue #81.
assert_eq!(
resolve_suggested_config_path(Some("/root"), fhs),
std::path::PathBuf::from("/root/.config/numa/numa.toml"),
);
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_falls_back_when_home_unset() {
assert_eq!(
resolve_suggested_config_path(None, fhs),
std::path::PathBuf::from("/var/lib/numa/numa.toml"),
);
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_falls_back_when_home_is_root() {
// systemd services sometimes have HOME=/ — don't treat that as a real home.
assert_eq!(
resolve_suggested_config_path(Some("/"), fhs),
std::path::PathBuf::from("/var/lib/numa/numa.toml"),
);
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_falls_back_when_home_is_empty() {
assert_eq!(
resolve_suggested_config_path(Some(""), fhs),
std::path::PathBuf::from("/var/lib/numa/numa.toml"),
);
}
#[cfg(not(windows))]
#[test]
fn suggested_config_path_skips_fallback_when_home_valid() {
// Happy path shouldn't probe the filesystem via config_dir().
let called = std::cell::Cell::new(false);
let fallback = || {
called.set(true);
std::path::PathBuf::from("/should/not/be/used")
};
let _ = resolve_suggested_config_path(Some("/home/alice"), fallback);
assert!(
!called.get(),
"fallback must not be invoked when HOME is valid"
);
}
}