use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use super::{git, path_string, AsrError, AsrResult};
#[derive(Debug, Clone)]
pub struct AsrPaths {
pub home: PathBuf,
pub db_path: PathBuf,
pub repos_local: PathBuf,
pub repos_mirrors: PathBuf,
pub index: PathBuf,
pub exact_index: PathBuf,
pub cache: PathBuf,
}
impl AsrPaths {
pub fn resolve() -> AsrResult<Self> {
let home = match env::var_os("ASR_HOME") {
Some(value) if !value.is_empty() => absolutize_home_path(PathBuf::from(value))?,
_ => dirs::home_dir()
.map(|home| home.join(".asr"))
.ok_or_else(|| AsrError::new("home_not_found", "Unable to resolve ASR_HOME"))?,
};
Ok(Self::from_home(home))
}
fn from_home(home: PathBuf) -> Self {
Self {
db_path: home.join("asr.sqlite"),
repos_local: home.join("repos").join("local"),
repos_mirrors: home.join("repos").join("mirrors"),
index: home.join("index"),
exact_index: home.join("index").join("exact"),
cache: home.join("cache"),
home,
}
}
pub(crate) fn ensure_dirs(&self) -> AsrResult<Vec<String>> {
let dirs = [
self.home.as_path(),
self.repos_local.as_path(),
self.repos_mirrors.as_path(),
self.index.as_path(),
self.exact_index.as_path(),
self.cache.as_path(),
];
let mut created = Vec::new();
for dir in dirs {
if !dir.exists() {
created.push(path_string(dir));
}
fs::create_dir_all(dir).map_err(|err| {
AsrError::with_path(
"asr_home_create_failed",
format!("Failed to create ASR directory: {err}"),
path_string(dir),
)
})?;
}
created.sort();
Ok(created)
}
pub(crate) fn create_all(&self) -> AsrResult<Vec<String>> {
validate_asr_home_not_in_git_worktree(&self.home)?;
self.ensure_dirs()
}
}
fn absolutize_home_path(path: PathBuf) -> AsrResult<PathBuf> {
if path.is_absolute() {
return Ok(path);
}
let cwd = env::current_dir().map_err(|err| {
AsrError::new(
"asr_home_unreadable",
format!("Unable to resolve relative ASR_HOME from current directory: {err}"),
)
})?;
Ok(cwd.join(path))
}
fn validate_asr_home_not_in_git_worktree(home: &Path) -> AsrResult<()> {
let Some(existing_parent) = nearest_existing_parent(home) else {
return Ok(());
};
let Ok(git_root) = git::canonical_git_root(&existing_parent) else {
return Ok(());
};
let canonical_parent = existing_parent.canonicalize().map_err(|err| {
AsrError::with_path(
"asr_home_unreadable",
format!("ASR_HOME parent is unreadable: {err}"),
path_string(&existing_parent),
)
})?;
let suffix = home
.strip_prefix(&existing_parent)
.unwrap_or_else(|_| Path::new(""));
let projected_home = canonical_parent.join(suffix);
if projected_home.starts_with(&git_root) {
return Err(AsrError::with_path(
"asr_home_inside_git_repo",
"ASR_HOME must not be located inside any Git worktree",
path_string(&projected_home),
));
}
Ok(())
}
fn nearest_existing_parent(path: &Path) -> Option<PathBuf> {
let mut current = Some(path);
while let Some(candidate) = current {
if candidate.exists() {
return Some(candidate.to_path_buf());
}
current = candidate.parent();
}
None
}