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
28pub 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 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 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
73fn 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 #[cfg(unix)]
85 {
86 use std::os::unix::fs::PermissionsExt;
87 let mut perms = fs::metadata(path)?.permissions();
88 perms.set_mode(0o700); fs::set_permissions(path, perms)?;
90 }
91
92 Ok(())
93}
94
95fn 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 if path_str.contains("..") {
103 return Err(anyhow::anyhow!("Path traversal detected: {}", path_str));
104 }
105
106 if path_str.contains('\0') {
108 return Err(anyhow::anyhow!("Path contains null bytes: {}", path_str));
109 }
110
111 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
122fn 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 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
150fn is_valid_project_name(name: &str) -> bool {
152 if name.trim().is_empty() {
154 return false;
155 }
156
157 if name.contains('\0') || name.contains('/') || name.contains('\\') {
159 return false;
160 }
161
162 if name == "." || name == ".." {
164 return false;
165 }
166
167 if name.len() > 255 {
169 return false;
170 }
171
172 true
173}
174
175pub fn validate_project_path(path: &Path) -> Result<PathBuf> {
177 validate_path_security(path)?;
178
179 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 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}