Skip to main content

synth_ai_core/
utils.rs

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}