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
31pub 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 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 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
76fn 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 #[cfg(unix)]
88 {
89 use std::os::unix::fs::PermissionsExt;
90 let mut perms = fs::metadata(path)?.permissions();
91 perms.set_mode(0o700); fs::set_permissions(path, perms)?;
93 }
94
95 Ok(())
96}
97
98fn 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 if path_str.contains("..") {
105 return Err(anyhow::anyhow!("Path traversal detected: {}", path_str));
106 }
107
108 if path_str.contains('\0') {
110 return Err(anyhow::anyhow!("Path contains null bytes: {}", path_str));
111 }
112
113 if path_str.len() > 4096 {
115 return Err(anyhow::anyhow!("Path is too long: {} characters", path_str.len()));
116 }
117
118 Ok(())
119}
120
121fn 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 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
146fn is_valid_project_name(name: &str) -> bool {
148 if name.trim().is_empty() {
150 return false;
151 }
152
153 if name.contains('\0') || name.contains('/') || name.contains('\\') {
155 return false;
156 }
157
158 if name == "." || name == ".." {
160 return false;
161 }
162
163 if name.len() > 255 {
165 return false;
166 }
167
168 true
169}
170
171pub fn validate_project_path(path: &Path) -> Result<PathBuf> {
173 validate_path_security(path)?;
174
175 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 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}