freemkv-keysources: keydb search is local to the executable

keydb_search_paths() returns exactly [<exe dir>/keydb.cfg] (or empty if
current_exe is unavailable) — no %APPDATA%/.config/XDG lookup. Matches
libfreemkv::keydb::default_path.
This commit is contained in:
Matthew Jackson
2026-06-24 20:46:31 -07:00
parent 2d0d0e3966
commit ed744e071f
+49 -178
View File
@@ -1,79 +1,38 @@
//! 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: //! 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 //! 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 //! list of locations to *search* (first existing wins) and for the single
//! single *default* location to write to (e.g. `update-keys`/save). //! *default* location to write to (e.g. `update-keys`/save).
//! //!
//! Resolution order: //! freemkv is a portable, standalone binary: the `keydb.cfg` lives *next to*
//! //! the executable — `<dir of current exe>/keydb.cfg` — and nowhere else. There
//! - **Windows**: `%APPDATA%\freemkv\keydb.cfg` FIRST (the idiomatic per-user //! is no OS-specific config-dir lookup (`%APPDATA%`, `%USERPROFILE%\.config`,
//! roaming config dir), then the legacy `%USERPROFILE%\.config\freemkv\keydb.cfg` //! `$XDG_CONFIG_HOME`, `$HOME/.config`). Drop the exe and its `keydb.cfg` in
//! for back-compat with installs that predate this fix. //! the same folder and it works. Callers needing a custom location pass
//! - **Linux / macOS**: `$XDG_CONFIG_HOME/freemkv/keydb.cfg` (if `XDG_CONFIG_HOME` //! `--keydb PATH`, which bypasses this module entirely.
//! 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.
use std::path::PathBuf; use std::path::PathBuf;
/// The keydb filename plus its `freemkv` subdir, joined onto a base dir. /// The single `keydb.cfg` location to search: next to the current executable.
fn keydb_under(base: PathBuf) -> PathBuf { ///
base.join("freemkv").join("keydb.cfg") /// Returns exactly one path — `<dir of current exe>/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
/// The ordered list of `keydb.cfg` locations to search, most-idiomatic first. /// OS config-dir fallback (portable / local-only).
/// ///
/// The caller picks the first path that exists on disk (see /// The caller picks the first path that exists on disk (see
/// [`existing_keydb_path`]); for writing a freshly-downloaded keydb, use /// [`existing_keydb_path`]); for writing a freshly-downloaded keydb, use
/// [`default_keydb_path`] (the first entry — the canonical location). /// [`default_keydb_path`].
///
/// 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.
pub fn keydb_search_paths() -> Vec<PathBuf> { pub fn keydb_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new(); match std::env::current_exe()
.ok()
if cfg!(windows) { .and_then(|exe| exe.parent().map(|dir| dir.join("keydb.cfg")))
// Idiomatic Windows location first. {
if let Ok(appdata) = std::env::var("APPDATA") { Some(path) => vec![path],
if !appdata.is_empty() { None => Vec::new(),
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"),
);
}
}
}
paths
}
/// The first search path that exists on disk, if any. /// The first search path that exists on disk, if any.
/// ///
@@ -85,9 +44,8 @@ pub fn existing_keydb_path() -> Option<PathBuf> {
/// The canonical default location to WRITE the keydb to (e.g. after a download). /// 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`]: /// This is the sole entry of [`keydb_search_paths`]: `<dir of current exe>/keydb.cfg`.
/// `%APPDATA%\freemkv\keydb.cfg` on Windows, `~/.config/freemkv/keydb.cfg` /// Returns `None` only when the executable's own directory can't be determined.
/// elsewhere. Returns `None` only when the relevant env vars are unset.
pub fn default_keydb_path() -> Option<PathBuf> { pub fn default_keydb_path() -> Option<PathBuf> {
keydb_search_paths().into_iter().next() keydb_search_paths().into_iter().next()
} }
@@ -96,129 +54,42 @@ pub fn default_keydb_path() -> Option<PathBuf> {
mod tests { mod tests {
use super::*; use super::*;
/// Serialize env-mutating tests: they share the process environment. /// The expected exe-local keydb path, computed the same way the code does.
fn lock() -> std::sync::MutexGuard<'static, ()> { /// Under `cargo test`, `current_exe()` is the test binary under `target/…`.
static M: std::sync::Mutex<()> = std::sync::Mutex::new(()); fn expected_local() -> Option<PathBuf> {
M.lock().unwrap_or_else(|e| e.into_inner()) 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<String>)> =
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) },
}
}
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) },
}
}
}
#[cfg(windows)]
#[test] #[test]
fn windows_prefers_appdata_then_legacy_userprofile() { fn search_paths_is_exactly_exe_local_keydb() {
with_env(
&[
("APPDATA", Some(r"C:\Users\matt\AppData\Roaming")),
("USERPROFILE", Some(r"C:\Users\matt")),
],
|| {
let paths = keydb_search_paths(); let paths = keydb_search_paths();
assert_eq!(paths.len(), 2, "APPDATA + legacy USERPROFILE"); match expected_local() {
Some(expected) => {
assert_eq!( assert_eq!(
paths[0], paths,
PathBuf::from(r"C:\Users\matt\AppData\Roaming") vec![expected],
.join("freemkv") "search list must be exactly [<exe dir>/keydb.cfg]"
.join("keydb.cfg"),
"APPDATA location must be searched first on Windows"
); );
assert_eq!( }
paths[1], None => {
PathBuf::from(r"C:\Users\matt") // No exe dir available → empty, no OS fallback (local only).
.join(".config") assert!(
.join("freemkv") paths.is_empty(),
.join("keydb.cfg"), "no exe dir means an empty search list, never an OS fallback"
"legacy .config dotfolder is the back-compat fallback"
);
assert_eq!(default_keydb_path(), Some(paths[0].clone()));
},
); );
} }
}
}
#[cfg(windows)]
#[test] #[test]
fn windows_falls_back_to_legacy_when_appdata_unset() { fn default_path_matches_search_head() {
with_env( // The write default is the single search entry, or None if unavailable.
&[("APPDATA", None), ("USERPROFILE", Some(r"C:\Users\matt"))], assert_eq!(default_keydb_path(), expected_local());
|| {
let paths = keydb_search_paths();
assert_eq!(paths.len(), 1, "only the legacy USERPROFILE path");
assert_eq!( assert_eq!(
paths[0], default_keydb_path(),
PathBuf::from(r"C:\Users\matt") keydb_search_paths().into_iter().next()
.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"
);
},
); );
} }
} }