op-loader 0.5.0

TUI for configuring 1password secrets for injection into your shell environment
use anyhow::{Context, Result};
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheRemoval {
    Removed,
    NotFound,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheKind {
    ResolvedVars,
}

pub fn cache_dir() -> Result<PathBuf> {
    if let Some(dir) = std::env::var_os("XDG_CACHE_HOME") {
        return Ok(PathBuf::from(dir).join("op_loader"));
    }

    let home = std::env::var_os("HOME").context("HOME environment variable not set")?;
    Ok(PathBuf::from(home).join(".cache").join("op_loader"))
}

pub fn ensure_cache_dir() -> Result<PathBuf> {
    let dir = cache_dir()?;
    std::fs::create_dir_all(&dir)
        .with_context(|| format!("Failed to create cache directory: {}", dir.display()))?;
    Ok(dir)
}

pub fn cache_path_for_account(
    cache_root: &std::path::Path,
    account_id: &str,
    kind: CacheKind,
) -> PathBuf {
    let prefix = match kind {
        CacheKind::ResolvedVars => "op_inject_vars",
    };
    let filename = format!("{}_{}.cache", prefix, sanitize_account_id(account_id));
    cache_root.join(filename)
}

pub fn cache_file_for_account(account_id: &str, kind: CacheKind) -> Result<PathBuf> {
    Ok(cache_path_for_account(&cache_dir()?, account_id, kind))
}

pub fn lock_path_for_account(account_id: &str) -> Result<PathBuf> {
    Ok(cache_dir()?.join(format!(
        "op_inject_{}.lock",
        sanitize_account_id(account_id)
    )))
}

pub fn remove_cache_for_account(account_id: &str) -> Result<CacheRemoval> {
    let mut removed_any = false;
    let path = cache_file_for_account(account_id, CacheKind::ResolvedVars)?;
    if path.exists() {
        std::fs::remove_file(&path)
            .with_context(|| format!("Failed to remove cache file: {}", path.display()))?;
        removed_any = true;
    }

    if removed_any {
        Ok(CacheRemoval::Removed)
    } else {
        Ok(CacheRemoval::NotFound)
    }
}

fn sanitize_account_id(account_id: &str) -> String {
    let mut sanitized = String::with_capacity(account_id.len());
    for ch in account_id.chars() {
        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' {
            sanitized.push(ch);
        } else {
            sanitized.push('_');
        }
    }

    if sanitized.is_empty() {
        "account".to_string()
    } else {
        sanitized
    }
}