use anyhow::{Context, Result};
use std::path::PathBuf;
pub(super) fn data_dir_home_fallback() -> Result<PathBuf> {
tracing::warn!(
"data_dir: dirs::data_local_dir() returned None (launchd / supervised spawn?); \
falling back to $HOME-based resolution (issue #718)"
);
let home = resolve_home_dir();
let Some(home) = home else {
tracing::error!(
"data_dir: FATAL — cannot resolve data directory: \
dirs::data_local_dir() returned None AND $HOME is unset AND \
passwd db lookup failed. \
Set TRUSTY_DATA_DIR to an absolute path in the launchd plist \
EnvironmentVariables (issue #718)."
);
anyhow::bail!(
"cannot resolve trusty-search data directory: \
dirs::data_local_dir() returned None and $HOME is unset. \
Set TRUSTY_DATA_DIR in the launchd plist to an absolute path."
);
};
anyhow::ensure!(
home.is_absolute(),
"HOME-based data dir resolution failed: $HOME={} is not absolute (issue #718)",
home.display()
);
#[cfg(target_os = "macos")]
let dir = home
.join("Library")
.join("Application Support")
.join("trusty-search");
#[cfg(not(target_os = "macos"))]
let dir = home.join(".local").join("share").join("trusty-search");
tracing::warn!(
"data_dir: HOME-based fallback: {} (issue #718 — \
set TRUSTY_DATA_DIR in launchd plist to suppress this warning)",
dir.display()
);
std::fs::create_dir_all(&dir)
.context("create trusty-search data dir (HOME fallback, issue #718)")?;
Ok(dir)
}
fn resolve_home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
let path = PathBuf::from(home);
if path.is_absolute() {
tracing::debug!(
"data_dir: home resolved from $HOME env var: {}",
path.display()
);
return Some(path);
}
}
#[cfg(unix)]
{
if let Some(home) = passwd_home_dir() {
tracing::debug!(
"data_dir: home resolved from passwd db (uid={}): {}",
nix::unistd::getuid(),
home.display()
);
return Some(home);
}
}
None
}
#[cfg(unix)]
fn passwd_home_dir() -> Option<PathBuf> {
let uid = nix::unistd::getuid();
match nix::unistd::User::from_uid(uid) {
Ok(Some(user)) => {
let home = user.dir;
if home.is_absolute() {
Some(home)
} else {
tracing::warn!(
"data_dir: passwd db entry for uid={} has relative home dir '{}' — ignoring",
uid,
home.display()
);
None
}
}
Ok(None) => {
tracing::warn!(
"data_dir: passwd db has no entry for uid={} (uid not found)",
uid
);
None
}
Err(e) => {
tracing::warn!(
"data_dir: passwd db lookup failed for uid={}: {} — \
$HOME env var is required in launchd plist (issue #718)",
uid,
e
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn data_dir_override_yields_absolute_path() {
let tmp = tempfile::tempdir().unwrap();
let abs_path = tmp.path().join("ts_abs_test_718");
std::fs::create_dir_all(&abs_path).unwrap();
unsafe {
std::env::set_var("TRUSTY_DATA_DIR", &abs_path);
}
let result = crate::service::persistence::data_dir();
unsafe {
std::env::remove_var("TRUSTY_DATA_DIR");
}
let dir = result.expect("data_dir with absolute TRUSTY_DATA_DIR must succeed");
assert!(
dir.is_absolute(),
"data_dir() must always return an absolute path (cwd-independent); got: {}",
dir.display()
);
}
#[test]
fn data_dir_home_fallback_path_is_absolute() {
let home = PathBuf::from("/tmp/fake-home-718");
#[cfg(target_os = "macos")]
let expected = home
.join("Library")
.join("Application Support")
.join("trusty-search");
#[cfg(not(target_os = "macos"))]
let expected = home.join(".local").join("share").join("trusty-search");
assert!(
expected.is_absolute(),
"HOME-fallback path must be absolute; got: {}",
expected.display()
);
assert!(
expected.starts_with(&home),
"HOME-fallback path {} must be rooted under HOME {}",
expected.display(),
home.display()
);
}
#[test]
#[serial]
fn resolve_home_dir_prefers_env_var() {
let tmp = tempfile::tempdir().unwrap();
let fake_home = tmp.path().join("fakehome-718-test");
std::fs::create_dir_all(&fake_home).unwrap();
unsafe { std::env::set_var("HOME", &fake_home) };
let result = resolve_home_dir();
unsafe { std::env::remove_var("HOME") };
let resolved = result.expect("resolve_home_dir must succeed when $HOME is absolute");
assert_eq!(
resolved, fake_home,
"resolve_home_dir must return $HOME path when set and absolute"
);
assert!(resolved.is_absolute(), "resolved home must be absolute");
}
#[test]
#[serial]
#[cfg(unix)]
fn resolve_home_dir_passwd_fallback_is_absolute() {
let saved = std::env::var("HOME").ok();
unsafe { std::env::remove_var("HOME") };
let result = resolve_home_dir();
if let Some(h) = saved {
unsafe { std::env::set_var("HOME", h) };
}
if let Some(home) = result {
assert!(
home.is_absolute(),
"passwd-db home must be absolute; got: {}",
home.display()
);
}
}
#[test]
#[serial]
fn registry_path_is_cwd_independent() {
use crate::service::persistence::indexes_toml_path;
let tmp = tempfile::tempdir().unwrap();
let abs_dir = tmp.path().join("ts-718-cwd-test");
std::fs::create_dir_all(&abs_dir).unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", &abs_dir) };
let toml_path = indexes_toml_path();
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
let path = toml_path.expect("indexes_toml_path must succeed with TRUSTY_DATA_DIR set");
assert!(
path.is_absolute(),
"indexes.toml path must be absolute (cwd-independent); got: {}",
path.display()
);
assert!(
path.starts_with(&abs_dir),
"indexes.toml path {} must be under override dir {}",
path.display(),
abs_dir.display()
);
assert_eq!(
path.file_name().and_then(|n| n.to_str()),
Some("indexes.toml"),
"file name must be indexes.toml; got: {}",
path.display()
);
}
}