From e8dd95a2bd26fef1f403fc810c4e5b28c00ed813 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Wed, 8 Apr 2026 17:40:27 +0300 Subject: [PATCH] fix: use FHS-compliant /var/lib/numa as Linux data dir default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit numa's default system-wide data directory was hardcoded to /usr/local/var/numa for all Unix platforms. This is the right path on macOS (Homebrew prefix convention) but non-FHS on Linux, where Arch / Fedora / Debian / etc. expect persistent state under /var/lib/. The mismatch was invisible to existing users (numa creates the dir silently on first run) but immediately surfaces when packaging for a distro — see PR #33 (community contribution to add an Arch AUR package) which had to add fragile sed-based path patching at PKGBUILD build time. The fix moves the path decision into a small helper: - daemon_data_dir() — cfg-gated platform dispatch (linux/macos) - resolve_linux_data_dir() — pure function, takes "does X exist?" as parameters, returns the right path Linux behavior: - Fresh install → /var/lib/numa (FHS) - Upgrading from pre-v0.10.1 install → /usr/local/var/numa (legacy) - Both paths exist → /var/lib/numa (FHS wins) The legacy fallback is critical: existing v0.10.0 Linux users have their CA cert + services.json under /usr/local/var/numa. Returning the new path unconditionally would cause CA regeneration on upgrade, breaking every browser that had trusted the previous CA. The fallback is checked at startup via std::path::Path::exists, so the upgrade is seamless and zero-config. macOS behavior is unchanged — /usr/local/var/numa is still correct because Homebrew's prefix is /usr/local. Test coverage: - resolve_linux_data_dir is a pure function gated cfg(any(linux,test)) so the same code path is unit-tested on every platform's CI run. - Four tests cover all combinations of (legacy_exists, fhs_exists), asserting the migration logic stays correct under future edits. The default config in numa.toml is also updated to document the new per-platform default paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- numa.toml | 11 ++++---- src/lib.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/numa.toml b/numa.toml index 35d92de..77ba231 100644 --- a/numa.toml +++ b/numa.toml @@ -2,11 +2,12 @@ bind_addr = "0.0.0.0:53" api_port = 5380 # api_bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN dashboard access -# data_dir = "/usr/local/var/numa" # where numa stores TLS CA and cert material - # (default: /usr/local/var/numa on unix, - # %PROGRAMDATA%\numa on windows). Override for - # containerized deploys or tests that can't - # write to the system path. +# data_dir = "/var/lib/numa" # where numa stores TLS CA and cert material + # Defaults: /var/lib/numa on linux (FHS), + # /usr/local/var/numa on macos (homebrew prefix), + # %PROGRAMDATA%\numa on windows. Override for + # containerized deploys or tests that can't + # write to the system path. # [upstream] # mode = "forward" # "forward" (default) — relay to upstream diff --git a/src/lib.rs b/src/lib.rs index 347e72f..08c9df4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,10 @@ pub type Error = Box; pub type Result = std::result::Result; /// Shared config directory for persistent data (services.json, etc). -/// Unix: ~/.config/numa/ (or /usr/local/var/numa/ when running as root daemon) +/// Unix users: ~/.config/numa/ +/// Linux root daemon: /var/lib/numa (FHS) — falls back to /usr/local/var/numa +/// if a pre-v0.10.1 install already lives there. +/// macOS root daemon: /usr/local/var/numa (Homebrew prefix) /// Windows: %APPDATA%\numa pub fn config_dir() -> std::path::PathBuf { #[cfg(windows)] @@ -63,13 +66,15 @@ fn config_dir_unix() -> std::path::PathBuf { } // Running as root daemon (launchd/systemd) — use system-wide path - std::path::PathBuf::from("/usr/local/var/numa") + daemon_data_dir() } /// Default system-wide data directory for TLS certs. Overridable via /// `[server] data_dir = "..."` in numa.toml — this function only provides /// the fallback when the config doesn't set it. -/// Unix: /usr/local/var/numa +/// Linux: /var/lib/numa (FHS) — falls back to /usr/local/var/numa if a +/// pre-v0.10.1 install already has data there. +/// macOS: /usr/local/var/numa (Homebrew prefix) /// Windows: %PROGRAMDATA%\numa pub fn data_dir() -> std::path::PathBuf { #[cfg(windows)] @@ -81,6 +86,75 @@ pub fn data_dir() -> std::path::PathBuf { } #[cfg(not(windows))] { + daemon_data_dir() + } +} + +/// Resolve the system-wide data directory for the running platform. +/// Honors backwards compatibility with pre-v0.10.1 installs that still +/// have their CA cert + services.json under `/usr/local/var/numa`. +#[cfg(not(windows))] +fn daemon_data_dir() -> std::path::PathBuf { + #[cfg(target_os = "linux")] + { + std::path::PathBuf::from(resolve_linux_data_dir( + std::path::Path::new(LEGACY_LINUX_DATA_DIR).exists(), + std::path::Path::new(FHS_LINUX_DATA_DIR).exists(), + )) + } + #[cfg(target_os = "macos")] + { + // macOS uses the Homebrew prefix convention; no FHS migration needed. std::path::PathBuf::from("/usr/local/var/numa") } } + +#[cfg(any(target_os = "linux", test))] +const FHS_LINUX_DATA_DIR: &str = "/var/lib/numa"; +#[cfg(any(target_os = "linux", test))] +const LEGACY_LINUX_DATA_DIR: &str = "/usr/local/var/numa"; + +/// Pure path-decision logic for Linux. Returns the FHS-compliant default +/// for fresh installs, or the legacy pre-v0.10.1 path if data already +/// lives there (so users don't lose their CA cert on upgrade). Extracted +/// as a pure function so the migration logic is unit-testable without +/// touching the real filesystem. +#[cfg(any(target_os = "linux", test))] +fn resolve_linux_data_dir(legacy_exists: bool, fhs_exists: bool) -> &'static str { + if legacy_exists && !fhs_exists { + LEGACY_LINUX_DATA_DIR + } else { + FHS_LINUX_DATA_DIR + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn linux_data_dir_fresh_install_uses_fhs() { + // No data anywhere → fresh install gets the FHS path. + assert_eq!(resolve_linux_data_dir(false, false), "/var/lib/numa"); + } + + #[test] + fn linux_data_dir_upgrading_install_keeps_legacy() { + // Pre-v0.10.1 install: legacy path has data, FHS path doesn't yet. + // Migration must keep using legacy so the user doesn't lose their CA. + assert_eq!(resolve_linux_data_dir(true, false), "/usr/local/var/numa"); + } + + #[test] + fn linux_data_dir_after_migration_uses_fhs() { + // Both paths exist (e.g., user manually copied data to FHS path). + // Prefer FHS since the legacy path is no longer the canonical home. + assert_eq!(resolve_linux_data_dir(true, true), "/var/lib/numa"); + } + + #[test] + fn linux_data_dir_only_fhs_uses_fhs() { + // Only FHS path has data — straightforward fresh-FHS case. + assert_eq!(resolve_linux_data_dir(false, true), "/var/lib/numa"); + } +}