use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub fn get_data_dir() -> Result<PathBuf> {
let data_dir = dirs::data_dir()
.or_else(dirs::home_dir)
.ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))?;
let tempo_dir = data_dir.join(".tempo");
create_secure_directory(&tempo_dir).context("Failed to create tempo data directory")?;
Ok(tempo_dir)
}
pub fn get_log_dir() -> Result<PathBuf> {
let log_dir = get_data_dir()?.join("logs");
create_secure_directory(&log_dir).context("Failed to create log directory")?;
Ok(log_dir)
}
pub fn get_backup_dir() -> Result<PathBuf> {
let backup_dir = get_data_dir()?.join("backups");
create_secure_directory(&backup_dir).context("Failed to create backup directory")?;
Ok(backup_dir)
}
pub fn canonicalize_path(path: &Path) -> Result<PathBuf> {
validate_path_security(path)?;
path.canonicalize()
.with_context(|| format!("Failed to canonicalize path: {}", path.display()))
}
pub fn is_git_repository(path: &Path) -> bool {
path.join(".git").exists()
}
pub fn has_tempo_marker(path: &Path) -> bool {
path.join(".tempo").exists()
}
pub fn detect_project_name(path: &Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.filter(|name| is_valid_project_name(name))
.unwrap_or("unknown")
.to_string()
}
pub fn get_git_hash(path: &Path) -> Option<String> {
if !is_git_repository(path) {
return None;
}
let git_dir = path.join(".git");
let head_content = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
let config_content = std::fs::read_to_string(git_dir.join("config")).ok()?;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
head_content.hash(&mut hasher);
config_content.hash(&mut hasher);
Some(format!("{:x}", hasher.finish()))
}
fn create_secure_directory(path: &Path) -> Result<()> {
if path.exists() {
validate_directory_permissions(path)?;
return Ok(());
}
fs::create_dir_all(path)
.with_context(|| format!("Failed to create directory: {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o700); fs::set_permissions(path, perms)?;
}
Ok(())
}
fn validate_path_security(path: &Path) -> Result<()> {
let path_str = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid Unicode"))?;
if path_str.contains("..") {
return Err(anyhow::anyhow!("Path traversal detected: {}", path_str));
}
if path_str.contains('\0') {
return Err(anyhow::anyhow!("Path contains null bytes: {}", path_str));
}
if path_str.len() > 4096 {
return Err(anyhow::anyhow!(
"Path is too long: {} characters",
path_str.len()
));
}
Ok(())
}
fn validate_directory_permissions(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read metadata for: {}", path.display()))?;
if !metadata.is_dir() {
return Err(anyhow::anyhow!(
"Path is not a directory: {}",
path.display()
));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode();
if mode & 0o002 != 0 {
return Err(anyhow::anyhow!(
"Directory is world-writable (insecure): {}",
path.display()
));
}
}
Ok(())
}
fn is_valid_project_name(name: &str) -> bool {
if name.trim().is_empty() {
return false;
}
if name.contains('\0') || name.contains('/') || name.contains('\\') {
return false;
}
if name == "." || name == ".." {
return false;
}
if name.len() > 255 {
return false;
}
true
}
pub fn validate_project_path(path: &Path) -> Result<PathBuf> {
validate_path_security(path)?;
let canonical_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.context("Failed to get current directory")?
.join(path)
};
if !canonical_path.exists() {
return Err(anyhow::anyhow!(
"Project path does not exist: {}",
canonical_path.display()
));
}
if !canonical_path.is_dir() {
return Err(anyhow::anyhow!(
"Project path is not a directory: {}",
canonical_path.display()
));
}
Ok(canonical_path)
}