From ed744e071f276d0dd19daffcf0fc683a7dcfd1b6 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:46:31 -0700 Subject: [PATCH] freemkv-keysources: keydb search is local to the executable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keydb_search_paths() returns exactly [/keydb.cfg] (or empty if current_exe is unavailable) — no %APPDATA%/.config/XDG lookup. Matches libfreemkv::keydb::default_path. --- src/paths.rs | 233 ++++++++++++--------------------------------------- 1 file changed, 52 insertions(+), 181 deletions(-) diff --git a/src/paths.rs b/src/paths.rs index eff8a9d..715da2b 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -1,78 +1,37 @@ -//! Where the `keydb.cfg` lives, per OS. +//! Where the `keydb.cfg` lives: local to the executable, local ONLY. //! //! Key-path policy belongs with the key sources (this crate), not the library: //! libfreemkv is handed a path and reads it. The CLI/app asks here for the -//! ordered list of locations to *search* (first existing wins) and for the -//! single *default* location to write to (e.g. `update-keys`/save). +//! list of locations to *search* (first existing wins) and for the single +//! *default* location to write to (e.g. `update-keys`/save). //! -//! Resolution order: -//! -//! - **Windows**: `%APPDATA%\freemkv\keydb.cfg` FIRST (the idiomatic per-user -//! roaming config dir), then the legacy `%USERPROFILE%\.config\freemkv\keydb.cfg` -//! for back-compat with installs that predate this fix. -//! - **Linux / macOS**: `$XDG_CONFIG_HOME/freemkv/keydb.cfg` (if `XDG_CONFIG_HOME` -//! is set), then `$HOME/.config/freemkv/keydb.cfg` — the long-standing default, -//! unchanged so existing users keep working. -//! -//! Pure `std::env` — `%APPDATA%`, `%USERPROFILE%`, `$HOME`, `$XDG_CONFIG_HOME` -//! are all environment variables, so no `dirs`-style crate is pulled in. +//! freemkv is a portable, standalone binary: the `keydb.cfg` lives *next to* +//! the executable — `/keydb.cfg` — and nowhere else. There +//! is no OS-specific config-dir lookup (`%APPDATA%`, `%USERPROFILE%\.config`, +//! `$XDG_CONFIG_HOME`, `$HOME/.config`). Drop the exe and its `keydb.cfg` in +//! the same folder and it works. Callers needing a custom location pass +//! `--keydb PATH`, which bypasses this module entirely. use std::path::PathBuf; -/// The keydb filename plus its `freemkv` subdir, joined onto a base dir. -fn keydb_under(base: PathBuf) -> PathBuf { - base.join("freemkv").join("keydb.cfg") -} - -/// The ordered list of `keydb.cfg` locations to search, most-idiomatic first. +/// The single `keydb.cfg` location to search: next to the current executable. +/// +/// Returns exactly one path — `/keydb.cfg` — on success. +/// Returns an empty list if the executable's own directory can't be determined +/// (`std::env::current_exe()` fails or has no parent); there is deliberately no +/// OS config-dir fallback (portable / local-only). /// /// The caller picks the first path that exists on disk (see /// [`existing_keydb_path`]); for writing a freshly-downloaded keydb, use -/// [`default_keydb_path`] (the first entry — the canonical location). -/// -/// Always returns at least one entry on a normally-configured system; returns -/// an empty list only if none of the relevant env vars are set. +/// [`default_keydb_path`]. pub fn keydb_search_paths() -> Vec { - let mut paths = Vec::new(); - - if cfg!(windows) { - // Idiomatic Windows location first. - if let Ok(appdata) = std::env::var("APPDATA") { - if !appdata.is_empty() { - paths.push(keydb_under(PathBuf::from(appdata))); - } - } - // Legacy XDG-style dotfolder under the user profile, for back-compat. - if let Ok(profile) = std::env::var("USERPROFILE") { - if !profile.is_empty() { - paths.push( - PathBuf::from(profile) - .join(".config") - .join("freemkv") - .join("keydb.cfg"), - ); - } - } - } else { - // Honour XDG_CONFIG_HOME if the user set it, then the historical default. - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - if !xdg.is_empty() { - paths.push(keydb_under(PathBuf::from(xdg))); - } - } - if let Ok(home) = std::env::var("HOME") { - if !home.is_empty() { - paths.push( - PathBuf::from(home) - .join(".config") - .join("freemkv") - .join("keydb.cfg"), - ); - } - } + match std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|dir| dir.join("keydb.cfg"))) + { + Some(path) => vec![path], + None => Vec::new(), } - - paths } /// The first search path that exists on disk, if any. @@ -85,9 +44,8 @@ pub fn existing_keydb_path() -> Option { /// The canonical default location to WRITE the keydb to (e.g. after a download). /// -/// This is the first (most idiomatic) entry of [`keydb_search_paths`]: -/// `%APPDATA%\freemkv\keydb.cfg` on Windows, `~/.config/freemkv/keydb.cfg` -/// elsewhere. Returns `None` only when the relevant env vars are unset. +/// This is the sole entry of [`keydb_search_paths`]: `/keydb.cfg`. +/// Returns `None` only when the executable's own directory can't be determined. pub fn default_keydb_path() -> Option { keydb_search_paths().into_iter().next() } @@ -96,129 +54,42 @@ pub fn default_keydb_path() -> Option { mod tests { use super::*; - /// Serialize env-mutating tests: they share the process environment. - fn lock() -> std::sync::MutexGuard<'static, ()> { - static M: std::sync::Mutex<()> = std::sync::Mutex::new(()); - M.lock().unwrap_or_else(|e| e.into_inner()) + /// The expected exe-local keydb path, computed the same way the code does. + /// Under `cargo test`, `current_exe()` is the test binary under `target/…`. + fn expected_local() -> Option { + std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|dir| dir.join("keydb.cfg"))) } - /// Build the search list under an explicit env, restoring the prior env - /// afterwards. Avoids depending on the host's real HOME/APPDATA. - fn with_env(vars: &[(&str, Option<&str>)], f: impl FnOnce()) { - let _g = lock(); - let keys = ["APPDATA", "USERPROFILE", "HOME", "XDG_CONFIG_HOME"]; - let saved: Vec<(&str, Option)> = - keys.iter().map(|k| (*k, std::env::var(k).ok())).collect(); - // Clear all, then apply the requested overrides. - for k in keys { - unsafe { std::env::remove_var(k) }; - } - for (k, v) in vars { - match v { - Some(val) => unsafe { std::env::set_var(k, val) }, - None => unsafe { std::env::remove_var(k) }, + #[test] + fn search_paths_is_exactly_exe_local_keydb() { + let paths = keydb_search_paths(); + match expected_local() { + Some(expected) => { + assert_eq!( + paths, + vec![expected], + "search list must be exactly [/keydb.cfg]" + ); } - } - f(); - // Restore. - for (k, v) in saved { - match v { - Some(val) => unsafe { std::env::set_var(k, val) }, - None => unsafe { std::env::remove_var(k) }, + None => { + // No exe dir available → empty, no OS fallback (local only). + assert!( + paths.is_empty(), + "no exe dir means an empty search list, never an OS fallback" + ); } } } - #[cfg(windows)] #[test] - fn windows_prefers_appdata_then_legacy_userprofile() { - with_env( - &[ - ("APPDATA", Some(r"C:\Users\matt\AppData\Roaming")), - ("USERPROFILE", Some(r"C:\Users\matt")), - ], - || { - let paths = keydb_search_paths(); - assert_eq!(paths.len(), 2, "APPDATA + legacy USERPROFILE"); - assert_eq!( - paths[0], - PathBuf::from(r"C:\Users\matt\AppData\Roaming") - .join("freemkv") - .join("keydb.cfg"), - "APPDATA location must be searched first on Windows" - ); - assert_eq!( - paths[1], - PathBuf::from(r"C:\Users\matt") - .join(".config") - .join("freemkv") - .join("keydb.cfg"), - "legacy .config dotfolder is the back-compat fallback" - ); - assert_eq!(default_keydb_path(), Some(paths[0].clone())); - }, - ); - } - - #[cfg(windows)] - #[test] - fn windows_falls_back_to_legacy_when_appdata_unset() { - with_env( - &[("APPDATA", None), ("USERPROFILE", Some(r"C:\Users\matt"))], - || { - let paths = keydb_search_paths(); - assert_eq!(paths.len(), 1, "only the legacy USERPROFILE path"); - assert_eq!( - paths[0], - PathBuf::from(r"C:\Users\matt") - .join(".config") - .join("freemkv") - .join("keydb.cfg") - ); - }, - ); - } - - #[cfg(not(windows))] - #[test] - fn unix_default_is_home_dotconfig() { - with_env( - &[("HOME", Some("/u/me")), ("XDG_CONFIG_HOME", None)], - || { - let paths = keydb_search_paths(); - assert_eq!(paths.len(), 1, "just the $HOME/.config default"); - assert_eq!( - paths[0], - PathBuf::from("/u/me") - .join(".config") - .join("freemkv") - .join("keydb.cfg"), - "Linux/macOS default must remain ~/.config/freemkv/keydb.cfg" - ); - assert_eq!(default_keydb_path(), Some(paths[0].clone())); - }, - ); - } - - #[cfg(not(windows))] - #[test] - fn unix_honours_xdg_config_home_first() { - with_env( - &[ - ("XDG_CONFIG_HOME", Some("/u/me/.cfg")), - ("HOME", Some("/u/me")), - ], - || { - let paths = keydb_search_paths(); - assert_eq!(paths.len(), 2, "XDG dir + $HOME/.config fallback"); - assert_eq!( - paths[0], - PathBuf::from("/u/me/.cfg") - .join("freemkv") - .join("keydb.cfg"), - "XDG_CONFIG_HOME, when set, is searched before ~/.config" - ); - }, + fn default_path_matches_search_head() { + // The write default is the single search entry, or None if unavailable. + assert_eq!(default_keydb_path(), expected_local()); + assert_eq!( + default_keydb_path(), + keydb_search_paths().into_iter().next() ); } }