tempo_cli/utils/
paths.rs

1use anyhow::{Result, Context};
2use std::path::{Path, PathBuf};
3use std::fs;
4
5pub fn get_data_dir() -> Result<PathBuf> {
6    let data_dir = dirs::data_dir()
7        .or_else(|| dirs::home_dir())
8        .ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))?;
9    
10    let tempo_dir = data_dir.join(".tempo");
11    create_secure_directory(&tempo_dir)
12        .context("Failed to create tempo data directory")?;
13    
14    Ok(tempo_dir)
15}
16
17pub fn get_log_dir() -> Result<PathBuf> {
18    let log_dir = get_data_dir()?.join("logs");
19    create_secure_directory(&log_dir)
20        .context("Failed to create log directory")?;
21    Ok(log_dir)
22}
23
24pub fn get_backup_dir() -> Result<PathBuf> {
25    let backup_dir = get_data_dir()?.join("backups");
26    create_secure_directory(&backup_dir)
27        .context("Failed to create backup directory")?;
28    Ok(backup_dir)
29}
30
31/// Securely canonicalize a path with validation
32pub fn canonicalize_path(path: &Path) -> Result<PathBuf> {
33    validate_path_security(path)?;
34    path.canonicalize()
35        .with_context(|| format!("Failed to canonicalize path: {}", path.display()))
36}
37
38pub fn is_git_repository(path: &Path) -> bool {
39    path.join(".git").exists()
40}
41
42pub fn has_tempo_marker(path: &Path) -> bool {
43    path.join(".tempo").exists()
44}
45
46pub fn detect_project_name(path: &Path) -> String {
47    path.file_name()
48        .and_then(|name| name.to_str())
49        .filter(|name| is_valid_project_name(name))
50        .unwrap_or("unknown")
51        .to_string()
52}
53
54pub fn get_git_hash(path: &Path) -> Option<String> {
55    if !is_git_repository(path) {
56        return None;
57    }
58    
59    // Try to read .git/HEAD and .git/config to create a unique hash
60    let git_dir = path.join(".git");
61    
62    let head_content = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
63    let config_content = std::fs::read_to_string(git_dir.join("config")).ok()?;
64    
65    // Create a simple hash from the combination
66    use std::collections::hash_map::DefaultHasher;
67    use std::hash::{Hash, Hasher};
68    
69    let mut hasher = DefaultHasher::new();
70    head_content.hash(&mut hasher);
71    config_content.hash(&mut hasher);
72    
73    Some(format!("{:x}", hasher.finish()))
74}
75
76/// Create a directory with secure permissions
77fn create_secure_directory(path: &Path) -> Result<()> {
78    if path.exists() {
79        validate_directory_permissions(path)?;
80        return Ok(());
81    }
82    
83    fs::create_dir_all(path)
84        .with_context(|| format!("Failed to create directory: {}", path.display()))?;
85        
86    // Set secure permissions on Unix systems
87    #[cfg(unix)]
88    {
89        use std::os::unix::fs::PermissionsExt;
90        let mut perms = fs::metadata(path)?.permissions();
91        perms.set_mode(0o700); // Owner read/write/execute only
92        fs::set_permissions(path, perms)?;
93    }
94    
95    Ok(())
96}
97
98/// Validate path for security issues
99fn validate_path_security(path: &Path) -> Result<()> {
100    let path_str = path.to_str()
101        .ok_or_else(|| anyhow::anyhow!("Path contains invalid Unicode"))?;
102    
103    // Check for path traversal attempts
104    if path_str.contains("..") {
105        return Err(anyhow::anyhow!("Path traversal detected: {}", path_str));
106    }
107    
108    // Check for null bytes
109    if path_str.contains('\0') {
110        return Err(anyhow::anyhow!("Path contains null bytes: {}", path_str));
111    }
112    
113    // Check for excessively long paths
114    if path_str.len() > 4096 {
115        return Err(anyhow::anyhow!("Path is too long: {} characters", path_str.len()));
116    }
117    
118    Ok(())
119}
120
121/// Validate directory permissions
122fn validate_directory_permissions(path: &Path) -> Result<()> {
123    let metadata = fs::metadata(path)
124        .with_context(|| format!("Failed to read metadata for: {}", path.display()))?;
125    
126    if !metadata.is_dir() {
127        return Err(anyhow::anyhow!("Path is not a directory: {}", path.display()));
128    }
129    
130    #[cfg(unix)]
131    {
132        use std::os::unix::fs::PermissionsExt;
133        let mode = metadata.permissions().mode();
134        // Check that directory is not world-writable
135        if mode & 0o002 != 0 {
136            return Err(anyhow::anyhow!(
137                "Directory is world-writable (insecure): {}", 
138                path.display()
139            ));
140        }
141    }
142    
143    Ok(())
144}
145
146/// Validate project name for security and sanity
147fn is_valid_project_name(name: &str) -> bool {
148    // Must not be empty or just whitespace
149    if name.trim().is_empty() {
150        return false;
151    }
152    
153    // Must not contain dangerous characters
154    if name.contains('\0') || name.contains('/') || name.contains('\\') {
155        return false;
156    }
157    
158    // Must not be relative path components
159    if name == "." || name == ".." {
160        return false;
161    }
162    
163    // Length check
164    if name.len() > 255 {
165        return false;
166    }
167    
168    true
169}
170
171/// Validate and sanitize project path for creation
172pub fn validate_project_path(path: &Path) -> Result<PathBuf> {
173    validate_path_security(path)?;
174    
175    // Ensure path is absolute for security
176    let canonical_path = if path.is_absolute() {
177        path.to_path_buf()
178    } else {
179        std::env::current_dir()
180            .context("Failed to get current directory")?
181            .join(path)
182    };
183    
184    // Additional validation for project paths
185    if !canonical_path.exists() {
186        return Err(anyhow::anyhow!("Project path does not exist: {}", canonical_path.display()));
187    }
188    
189    if !canonical_path.is_dir() {
190        return Err(anyhow::anyhow!("Project path is not a directory: {}", canonical_path.display()));
191    }
192    
193    Ok(canonical_path)
194}