1use crate::{Error, Result};
4use std::path::{Path, PathBuf};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use ::url::Url as UrlType;
7
8pub mod fs {
10 use super::*;
11 use std::fs;
12
13 pub fn safe_canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
15 let path = path.as_ref();
16
17 if let Ok(canonical) = fs::canonicalize(path) {
19 return Ok(canonical);
20 }
21
22 if let Some(parent) = path.parent() {
24 if let Some(filename) = path.file_name() {
25 if let Ok(parent_canonical) = fs::canonicalize(parent) {
26 return Ok(parent_canonical.join(filename));
27 }
28 }
29 }
30
31 let current_dir = std::env::current_dir()?;
33 Ok(current_dir.join(path))
34 }
35
36 pub fn file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
38 let metadata = fs::metadata(path)?;
39 Ok(metadata.len())
40 }
41
42 pub fn is_safe_path<P: AsRef<Path>>(base: P, target: P) -> Result<bool> {
44 let base = safe_canonicalize(base)?;
45 let target = safe_canonicalize(target)?;
46
47 Ok(target.starts_with(base))
48 }
49
50 pub fn ensure_dir<P: AsRef<Path>>(path: P) -> Result<()> {
52 fs::create_dir_all(path)?;
53 Ok(())
54 }
55
56 pub fn file_extension<P: AsRef<Path>>(path: P) -> Option<String> {
58 path.as_ref()
59 .extension()
60 .and_then(|ext| ext.to_str())
61 .map(|ext| ext.to_lowercase())
62 }
63
64 pub fn is_binary_file<P: AsRef<Path>>(path: P) -> Result<bool> {
66 let mut buffer = [0; 1024];
67 let file = std::fs::File::open(path)?;
68 let bytes_read = std::io::Read::read(&mut std::io::BufReader::new(file), &mut buffer)?;
69
70 Ok(buffer[..bytes_read].contains(&0))
72 }
73}
74
75pub mod string {
77 use super::*;
78
79 pub fn truncate(s: &str, max_len: usize) -> String {
81 if s.len() <= max_len {
82 s.to_string()
83 } else if max_len <= 3 {
84 "...".to_string()
85 } else {
86 format!("{}...", &s[..max_len - 3])
87 }
88 }
89
90 pub fn escape_json(s: &str) -> String {
92 s.replace('\\', "\\\\")
93 .replace('"', "\\\"")
94 .replace('\n', "\\n")
95 .replace('\r', "\\r")
96 .replace('\t', "\\t")
97 }
98
99 pub fn sanitize_filename(s: &str) -> String {
101 s.chars()
102 .map(|c| match c {
103 '/' | '\\' | '?' | '%' | '*' | ':' | '|' | '"' | '<' | '>' => '_',
104 c if c.is_control() => '_',
105 c => c,
106 })
107 .collect()
108 }
109
110 pub fn camel_to_snake(s: &str) -> String {
112 let mut result = String::new();
113 let mut prev_lowercase = false;
114
115 for c in s.chars() {
116 if c.is_uppercase() && prev_lowercase {
117 result.push('_');
118 }
119 result.push(c.to_lowercase().next().unwrap_or(c));
120 prev_lowercase = c.is_lowercase();
121 }
122
123 result
124 }
125
126 pub fn extract_context(content: &str, line_number: usize, context: usize) -> Vec<(usize, &str)> {
128 let lines: Vec<&str> = content.lines().collect();
129 let start = line_number.saturating_sub(context);
130 let end = (line_number + context + 1).min(lines.len());
131
132 lines[start..end]
133 .iter()
134 .enumerate()
135 .map(|(i, line)| (start + i + 1, *line))
136 .collect()
137 }
138}
139
140pub mod time {
142 use super::*;
143
144 pub fn now_timestamp() -> Result<u64> {
146 SystemTime::now()
147 .duration_since(UNIX_EPOCH)
148 .map(|d| d.as_secs())
149 .map_err(|e| Error::Other(anyhow::anyhow!("Time error: {}", e)))
150 }
151
152 pub fn format_duration(duration: Duration) -> String {
154 let total_seconds = duration.as_secs();
155
156 if total_seconds < 60 {
157 format!("{}s", total_seconds)
158 } else if total_seconds < 3600 {
159 format!("{}m {}s", total_seconds / 60, total_seconds % 60)
160 } else if total_seconds < 86400 {
161 format!(
162 "{}h {}m",
163 total_seconds / 3600,
164 (total_seconds % 3600) / 60
165 )
166 } else {
167 format!(
168 "{}d {}h",
169 total_seconds / 86400,
170 (total_seconds % 86400) / 3600
171 )
172 }
173 }
174
175 pub fn parse_duration(s: &str) -> Result<Duration> {
177 let s = s.trim().to_lowercase();
178
179 if let Ok(seconds) = s.parse::<u64>() {
180 return Ok(Duration::from_secs(seconds));
181 }
182
183 if s.ends_with("ms") {
184 let ms = s[..s.len() - 2].parse::<u64>()?;
185 return Ok(Duration::from_millis(ms));
186 }
187
188 if s.ends_with('s') {
189 let secs = s[..s.len() - 1].parse::<u64>()?;
190 return Ok(Duration::from_secs(secs));
191 }
192
193 if s.ends_with('m') {
194 let mins = s[..s.len() - 1].parse::<u64>()?;
195 return Ok(Duration::from_secs(mins * 60));
196 }
197
198 if s.ends_with('h') {
199 let hours = s[..s.len() - 1].parse::<u64>()?;
200 return Ok(Duration::from_secs(hours * 3600));
201 }
202
203 if s.ends_with('d') {
204 let days = s[..s.len() - 1].parse::<u64>()?;
205 return Ok(Duration::from_secs(days * 86400));
206 }
207
208 Err(Error::Other(anyhow::anyhow!("Invalid duration format: {}", s)))
209 }
210}
211
212pub mod hash {
214 use super::*;
215 use sha2::{Sha256, Digest};
216
217 pub fn sha256(content: &[u8]) -> String {
219 let mut hasher = Sha256::new();
220 hasher.update(content);
221 hex::encode(hasher.finalize())
222 }
223
224 pub fn sha256_string(content: &str) -> String {
226 sha256(content.as_bytes())
227 }
228
229 pub fn content_hash(content: &str) -> String {
231 use std::collections::hash_map::DefaultHasher;
232 use std::hash::{Hash, Hasher};
233
234 let mut hasher = DefaultHasher::new();
235 content.hash(&mut hasher);
236 format!("{:x}", hasher.finish())
237 }
238}
239
240pub mod url {
242 use super::*;
243
244 pub fn is_valid_url(s: &str) -> bool {
246 UrlType::parse(s).is_ok()
247 }
248
249 pub fn extract_domain(url_str: &str) -> Result<String> {
251 let url = UrlType::parse(url_str)
252 .map_err(|e| Error::Other(anyhow::anyhow!("Invalid URL: {}", e)))?;
253
254 url.host_str()
255 .map(|host| host.to_string())
256 .ok_or_else(|| Error::Other(anyhow::anyhow!("No host in URL")))
257 }
258
259 pub fn join_path(base: &str, path: &str) -> Result<String> {
261 let mut url = UrlType::parse(base)
262 .map_err(|e| Error::Other(anyhow::anyhow!("Invalid base URL: {}", e)))?;
263
264 url = url.join(path)
265 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to join path: {}", e)))?;
266
267 Ok(url.to_string())
268 }
269}
270
271pub mod process {
273 use super::*;
274
275 pub fn command_exists(command: &str) -> bool {
277 which::which(command).is_ok()
278 }
279
280 pub fn get_shell() -> String {
282 if cfg!(windows) {
283 std::env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string())
284 } else {
285 std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
286 }
287 }
288
289 pub fn escape_shell_arg(arg: &str) -> String {
291 if cfg!(windows) {
292 if arg.contains(' ') || arg.contains('"') {
294 format!("\"{}\"", arg.replace('"', "\\\""))
295 } else {
296 arg.to_string()
297 }
298 } else {
299 if arg.chars().any(|c| " \t\n\r\"'\\|&;<>()$`".contains(c)) {
301 format!("'{}'", arg.replace('\'', "'\"'\"'"))
302 } else {
303 arg.to_string()
304 }
305 }
306 }
307}
308
309pub mod memory {
311 use super::*;
312
313 pub fn format_bytes(bytes: u64) -> String {
315 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
316 const THRESHOLD: u64 = 1024;
317
318 if bytes < THRESHOLD {
319 return format!("{} B", bytes);
320 }
321
322 let mut size = bytes as f64;
323 let mut unit_index = 0;
324
325 while size >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 {
326 size /= THRESHOLD as f64;
327 unit_index += 1;
328 }
329
330 format!("{:.1} {}", size, UNITS[unit_index])
331 }
332
333 pub fn parse_bytes(s: &str) -> Result<u64> {
335 let s = s.trim().to_uppercase();
336
337 if let Ok(bytes) = s.parse::<u64>() {
338 return Ok(bytes);
339 }
340
341 let (number_part, unit_part) = if s.ends_with('B') {
342 let unit_start = s.len() - if s.ends_with("KB") || s.ends_with("MB") || s.ends_with("GB") || s.ends_with("TB") { 2 } else { 1 };
343 (s[..unit_start].trim(), &s[unit_start..])
344 } else {
345 (s.as_str(), "B")
346 };
347
348 let number: f64 = number_part.parse()
349 .map_err(|_| Error::Other(anyhow::anyhow!("Invalid number: {}", number_part)))?;
350
351 let multiplier = match unit_part {
352 "B" => 1,
353 "KB" => 1024,
354 "MB" => 1024 * 1024,
355 "GB" => 1024 * 1024 * 1024,
356 "TB" => 1024_u64.pow(4),
357 _ => return Err(Error::Other(anyhow::anyhow!("Invalid unit: {}", unit_part))),
358 };
359
360 Ok((number * multiplier as f64) as u64)
361 }
362}
363
364pub mod validation {
366 use super::*;
367
368 pub fn is_valid_email(email: &str) -> bool {
370 let email_regex = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
371 email_regex.is_match(email)
372 }
373
374 pub fn is_valid_api_key(key: &str) -> bool {
376 !key.is_empty() && key.len() >= 10 && key.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')
377 }
378
379 pub fn is_valid_session_id(id: &str) -> bool {
381 !id.is_empty() && id.len() <= 256 && id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')
382 }
383
384 pub fn is_valid_model_name(name: &str) -> bool {
386 !name.is_empty() && name.len() <= 100 && name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '/' || c == '.')
387 }
388}
389
390pub mod config {
392 use super::*;
393
394 pub fn config_dir() -> Result<PathBuf> {
396 dirs::config_dir()
397 .map(|dir| dir.join("code-mesh"))
398 .ok_or_else(|| Error::Other(anyhow::anyhow!("Could not find config directory")))
399 }
400
401 pub fn data_dir() -> Result<PathBuf> {
403 dirs::data_dir()
404 .map(|dir| dir.join("code-mesh"))
405 .ok_or_else(|| Error::Other(anyhow::anyhow!("Could not find data directory")))
406 }
407
408 pub fn cache_dir() -> Result<PathBuf> {
410 dirs::cache_dir()
411 .map(|dir| dir.join("code-mesh"))
412 .ok_or_else(|| Error::Other(anyhow::anyhow!("Could not find cache directory")))
413 }
414
415 pub fn ensure_app_dirs() -> Result<()> {
417 fs::ensure_dir(config_dir()?)?;
418 fs::ensure_dir(data_dir()?)?;
419 fs::ensure_dir(cache_dir()?)?;
420 Ok(())
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn test_string_truncate() {
430 assert_eq!(string::truncate("hello", 10), "hello");
431 assert_eq!(string::truncate("hello world", 8), "hello...");
432 assert_eq!(string::truncate("hi", 1), "...");
433 }
434
435 #[test]
436 fn test_string_sanitize_filename() {
437 assert_eq!(string::sanitize_filename("hello/world"), "hello_world");
438 assert_eq!(string::sanitize_filename("file?.txt"), "file_.txt");
439 }
440
441 #[test]
442 fn test_camel_to_snake() {
443 assert_eq!(string::camel_to_snake("camelCase"), "camel_case");
444 assert_eq!(string::camel_to_snake("HTTPSConnection"), "h_t_t_p_s_connection");
445 assert_eq!(string::camel_to_snake("simple"), "simple");
446 }
447
448 #[test]
449 fn test_format_bytes() {
450 assert_eq!(memory::format_bytes(512), "512 B");
451 assert_eq!(memory::format_bytes(1024), "1.0 KB");
452 assert_eq!(memory::format_bytes(1536), "1.5 KB");
453 assert_eq!(memory::format_bytes(1024 * 1024), "1.0 MB");
454 }
455
456 #[test]
457 fn test_parse_duration() -> Result<()> {
458 assert_eq!(time::parse_duration("30s")?, Duration::from_secs(30));
459 assert_eq!(time::parse_duration("5m")?, Duration::from_secs(300));
460 assert_eq!(time::parse_duration("2h")?, Duration::from_secs(7200));
461 assert_eq!(time::parse_duration("1d")?, Duration::from_secs(86400));
462 Ok(())
463 }
464
465 #[test]
466 fn test_validation() {
467 assert!(validation::is_valid_email("test@example.com"));
468 assert!(!validation::is_valid_email("invalid-email"));
469
470 assert!(validation::is_valid_api_key("sk-1234567890abcdef"));
471 assert!(!validation::is_valid_api_key("short"));
472
473 assert!(validation::is_valid_model_name("anthropic/claude-3-opus"));
474 assert!(!validation::is_valid_model_name(""));
475 }
476}