1use crate::errors::{CoreError, CoreResult};
2use serde_json::Value;
3use std::env;
4use std::fs;
5use std::io::{self, Write};
6use std::path::{Path, PathBuf};
7
8pub const PRIVATE_DIR_MODE: u32 = 0o700;
9pub const PRIVATE_FILE_MODE: u32 = 0o600;
10
11pub fn strip_json_comments(raw: &str) -> String {
12 let mut result = String::with_capacity(raw.len());
13 let mut in_string = false;
14 let mut in_line_comment = false;
15 let mut in_block_comment = false;
16 let mut escape = false;
17 let chars: Vec<char> = raw.chars().collect();
18 let mut i = 0;
19 while i < chars.len() {
20 let c = chars[i];
21 let next = if i + 1 < chars.len() {
22 chars[i + 1]
23 } else {
24 '\0'
25 };
26
27 if in_line_comment {
28 if c == '\n' {
29 in_line_comment = false;
30 result.push(c);
31 }
32 i += 1;
33 continue;
34 }
35
36 if in_block_comment {
37 if c == '*' && next == '/' {
38 in_block_comment = false;
39 i += 2;
40 } else {
41 i += 1;
42 }
43 continue;
44 }
45
46 if in_string {
47 result.push(c);
48 if c == '"' && !escape {
49 in_string = false;
50 }
51 escape = c == '\\' && !escape;
52 i += 1;
53 continue;
54 }
55
56 if c == '/' && next == '/' {
57 in_line_comment = true;
58 i += 2;
59 continue;
60 }
61
62 if c == '/' && next == '*' {
63 in_block_comment = true;
64 i += 2;
65 continue;
66 }
67
68 if c == '"' {
69 in_string = true;
70 escape = false;
71 }
72
73 result.push(c);
74 i += 1;
75 }
76
77 result
78}
79
80pub fn create_and_write_json(path: &Path, content: &Value) -> io::Result<()> {
81 if let Some(parent) = path.parent() {
82 fs::create_dir_all(parent)?;
83 }
84 let payload = serde_json::to_string_pretty(content)
85 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
86 + "\n";
87 fs::write(path, payload)
88}
89
90pub fn load_json_to_value(path: &Path) -> Value {
91 if !path.exists() {
92 return Value::Object(Default::default());
93 }
94 let raw = match fs::read_to_string(path) {
95 Ok(value) => value,
96 Err(_) => return Value::Object(Default::default()),
97 };
98 let stripped = strip_json_comments(&raw);
99 serde_json::from_str(&stripped).unwrap_or_else(|_| Value::Object(Default::default()))
100}
101
102pub fn repo_root() -> Option<PathBuf> {
103 let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
104 manifest
105 .parent()
106 .and_then(|p| p.parent())
107 .map(|p| p.to_path_buf())
108}
109
110pub fn synth_home_dir() -> PathBuf {
111 dirs::home_dir()
112 .unwrap_or_else(|| PathBuf::from("."))
113 .join(".synth-ai")
114}
115
116pub fn synth_user_config_path() -> PathBuf {
117 synth_home_dir().join("user_config.json")
118}
119
120pub fn synth_container_config_path() -> PathBuf {
121 synth_home_dir().join("container_config.json")
122}
123
124pub fn synth_bin_dir() -> PathBuf {
125 synth_home_dir().join("bin")
126}
127
128pub fn is_file_type(path: &Path, ext: &str) -> bool {
129 let mut ext = ext.to_string();
130 if !ext.starts_with('.') {
131 ext.insert(0, '.');
132 }
133 path.is_file()
134 && path
135 .extension()
136 .map(|e| format!(".{}", e.to_string_lossy()))
137 == Some(ext)
138}
139
140pub fn validate_file_type(path: &Path, ext: &str) -> CoreResult<()> {
141 if !is_file_type(path, ext) {
142 return Err(CoreError::InvalidInput(format!(
143 "{} is not a {} file",
144 path.display(),
145 ext
146 )));
147 }
148 Ok(())
149}
150
151pub fn is_hidden_path(path: &Path, root: &Path) -> bool {
152 let rel = path.strip_prefix(root).unwrap_or(path);
153 rel.components()
154 .any(|c| c.as_os_str().to_string_lossy().starts_with('.'))
155}
156
157pub fn get_bin_path(name: &str) -> Option<PathBuf> {
158 let path_var = env::var("PATH").ok()?;
159 for dir in env::split_paths(&path_var) {
160 let candidate = dir.join(name);
161 if candidate.is_file() {
162 return Some(candidate);
163 }
164 #[cfg(windows)]
165 {
166 let exe = candidate.with_extension("exe");
167 if exe.is_file() {
168 return Some(exe);
169 }
170 }
171 }
172 None
173}
174
175pub fn get_home_config_file_paths(dir_name: &str, file_extension: &str) -> Vec<PathBuf> {
176 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
177 let dir = home.join(dir_name);
178 let mut results = Vec::new();
179 if let Ok(entries) = fs::read_dir(dir) {
180 for entry in entries.flatten() {
181 let path = entry.path();
182 if path.is_file() {
183 if let Some(ext) = path.extension() {
184 if ext == file_extension {
185 results.push(path);
186 }
187 }
188 }
189 }
190 }
191 results
192}
193
194pub fn find_config_path(bin: &Path, home_subdir: &str, filename: &str) -> Option<PathBuf> {
195 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
196 let home_candidate = home.join(home_subdir).join(filename);
197 if home_candidate.exists() {
198 return Some(home_candidate);
199 }
200 let local_candidate = bin
201 .parent()
202 .unwrap_or(Path::new("."))
203 .join(home_subdir)
204 .join(filename);
205 if local_candidate.exists() {
206 return Some(local_candidate);
207 }
208 None
209}
210
211pub fn compute_import_paths(app: &Path, repo_root: Option<&Path>) -> Vec<String> {
212 let app_dir = app.parent().unwrap_or(Path::new(".")).to_path_buf();
213 let mut initial_dirs: Vec<PathBuf> = vec![app_dir.clone()];
214 if app_dir.join("__init__.py").exists() {
215 if let Some(parent) = app_dir.parent() {
216 initial_dirs.push(parent.to_path_buf());
217 }
218 }
219 if let Some(root) = repo_root {
220 initial_dirs.push(root.to_path_buf());
221 }
222
223 let mut unique_dirs: Vec<String> = Vec::new();
224 for dir in initial_dirs {
225 let dir_str = dir.to_string_lossy().to_string();
226 if !dir_str.is_empty() && !unique_dirs.contains(&dir_str) {
227 unique_dirs.push(dir_str);
228 }
229 }
230
231 if let Ok(existing) = env::var("PYTHONPATH") {
232 for segment in env::split_paths(&existing) {
233 let segment_str = segment.to_string_lossy().to_string();
234 if !segment_str.is_empty() && !unique_dirs.contains(&segment_str) {
235 unique_dirs.push(segment_str);
236 }
237 }
238 }
239
240 unique_dirs
241}
242
243pub fn cleanup_paths(file: &Path, dir: &Path) -> CoreResult<()> {
244 if !file.starts_with(dir) {
245 return Err(CoreError::InvalidInput(format!(
246 "{} is not inside {}",
247 file.display(),
248 dir.display()
249 )));
250 }
251 let _ = fs::remove_file(file);
252 let _ = fs::remove_dir_all(dir);
253 Ok(())
254}
255
256fn set_permissions(path: &Path, mode: u32) -> io::Result<()> {
257 #[cfg(unix)]
258 {
259 use std::os::unix::fs::PermissionsExt;
260 let perms = fs::Permissions::from_mode(mode);
261 fs::set_permissions(path, perms)?;
262 }
263 #[cfg(not(unix))]
264 {
265 let _ = mode;
266 }
267 Ok(())
268}
269
270pub fn ensure_private_dir(path: &Path) -> io::Result<()> {
271 fs::create_dir_all(path)?;
272 let _ = set_permissions(path, PRIVATE_DIR_MODE);
273 Ok(())
274}
275
276pub fn write_private_text(path: &Path, content: &str, mode: u32) -> io::Result<()> {
277 if let Some(parent) = path.parent() {
278 ensure_private_dir(parent)?;
279 let mut tmp = tempfile::Builder::new()
280 .prefix(&format!(
281 "{}.",
282 path.file_name().unwrap_or_default().to_string_lossy()
283 ))
284 .tempfile_in(parent)?;
285 let _ = set_permissions(tmp.path(), mode);
286 tmp.write_all(content.as_bytes())?;
287 tmp.flush()?;
288 let _ = tmp.as_file().sync_all();
289 tmp.persist(path).map_err(|e| e.error)?;
290 let _ = set_permissions(path, mode);
291 }
292 Ok(())
293}
294
295pub fn write_private_json(path: &Path, data: &Value) -> io::Result<()> {
296 let payload = serde_json::to_string_pretty(data)
297 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
298 + "\n";
299 write_private_text(path, &payload, PRIVATE_FILE_MODE)
300}
301
302pub fn should_filter_log_line(line: &str) -> bool {
303 let trimmed = line.trim();
304 if trimmed.is_empty() {
305 return false;
306 }
307 let lowered = trimmed.to_lowercase();
308 let substrings = [
309 "codex_otel::otel_event_manager",
310 "event.kind=response.reasoning_summary_text.delta",
311 "event.name=\"codex.sse_event\"",
312 "codex_otel",
313 ];
314 for substr in substrings {
315 if lowered.contains(substr) {
316 return true;
317 }
318 }
319 let patterns = [
320 r"(?i).*codex_otel::otel_event_manager.*",
321 r"(?i).*event\.kind=response\.reasoning_summary_text\.delta.*",
322 r#"(?i).*event\.name="codex\.sse_event".*"#,
323 r"(?i)^\d{4}-\d{2}-\d{2}t.*codex_otel.*",
324 ];
325 for pat in patterns {
326 if let Ok(re) = regex::Regex::new(pat) {
327 if re.is_match(trimmed) {
328 return true;
329 }
330 }
331 }
332 false
333}