use std::path::{Component, PathBuf};
const JARVY_DIR: &str = ".jarvy";
#[derive(Debug, thiserror::Error)]
#[error("cannot determine home directory")]
pub struct NoHomeDir;
pub fn jarvy_home() -> Result<PathBuf, NoHomeDir> {
if let Ok(custom) = std::env::var("JARVY_HOME") {
let trimmed = custom.trim();
if !trimmed.is_empty() {
let p = PathBuf::from(trimmed);
if !is_safe_jarvy_home(&p) {
return Err(NoHomeDir);
}
return Ok(p);
}
}
dirs::home_dir().map(|h| h.join(JARVY_DIR)).ok_or(NoHomeDir)
}
fn is_safe_jarvy_home(p: &std::path::Path) -> bool {
if !p.is_absolute() {
return false;
}
if p.components().any(|c| matches!(c, Component::ParentDir)) {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let Ok(meta) = std::fs::symlink_metadata(p)
&& meta.uid() != current_uid()
{
return false;
}
}
true
}
#[cfg(unix)]
fn current_uid() -> u32 {
#[allow(unsafe_code)]
unsafe {
unsafe extern "C" {
fn getuid() -> u32;
}
getuid()
}
}
pub fn config_toml() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("config.toml"))
}
pub fn logs_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("logs"))
}
pub fn tickets_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("tickets"))
}
pub fn remote_config_cache_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("cache").join("configs"))
}
pub fn staging_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("staging"))
}
pub fn backup_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("backup"))
}
pub fn plugins_dir() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("tools.d"))
}
pub fn registry_remote_cache_dir() -> Result<PathBuf, NoHomeDir> {
Ok(plugins_dir()?.join(".remote"))
}
pub fn team_sources_toml() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("team-sources.toml"))
}
pub fn mcp_config_toml() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("mcp-config.toml"))
}
pub fn update_state_json() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("update-state.json"))
}
pub fn install_method_json() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("install-method.json"))
}
pub fn rollback_info_json() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("rollback-info.json"))
}
pub fn ensure_stamp() -> Result<PathBuf, NoHomeDir> {
Ok(jarvy_home()?.join("ensure.stamp"))
}
pub fn state_json(project: &std::path::Path) -> PathBuf {
project.join(JARVY_DIR).join("state.json")
}
pub fn ensure_dir_0700(dir: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[serial_test::serial(jarvy_home_env)]
fn jarvy_home_honors_env_override() {
let override_path = std::env::temp_dir().join("jarvy-test-override");
let prev = std::env::var("JARVY_HOME").ok();
#[allow(unsafe_code)]
unsafe {
std::env::set_var("JARVY_HOME", &override_path);
}
let p = jarvy_home().unwrap();
assert_eq!(p, override_path);
#[allow(unsafe_code)]
unsafe {
match prev {
Some(v) => std::env::set_var("JARVY_HOME", v),
None => std::env::remove_var("JARVY_HOME"),
}
}
}
#[test]
#[serial_test::serial(jarvy_home_env)]
fn derived_paths_share_jarvy_home() {
if std::env::var("JARVY_HOME").is_ok() {
return;
}
let home = jarvy_home().unwrap();
assert!(logs_dir().unwrap().starts_with(&home));
assert!(tickets_dir().unwrap().starts_with(&home));
assert!(remote_config_cache_dir().unwrap().starts_with(&home));
assert!(staging_dir().unwrap().starts_with(&home));
assert!(backup_dir().unwrap().starts_with(&home));
assert!(config_toml().unwrap().starts_with(&home));
}
}