tempo_cli/utils/
paths.rs

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