code_mesh_core/
utils.rs

1//! Utility functions and helpers for Code Mesh Core
2
3use crate::{Error, Result};
4use std::path::{Path, PathBuf};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use ::url::Url as UrlType;
7
8/// File system utilities
9pub mod fs {
10    use super::*;
11    use std::fs;
12    
13    /// Safely canonicalize a path, handling cases where the path doesn't exist
14    pub fn safe_canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
15        let path = path.as_ref();
16        
17        // Try direct canonicalization first
18        if let Ok(canonical) = fs::canonicalize(path) {
19            return Ok(canonical);
20        }
21        
22        // If that fails, try to canonicalize the parent and append the filename
23        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        // Fall back to absolute path
32        let current_dir = std::env::current_dir()?;
33        Ok(current_dir.join(path))
34    }
35    
36    /// Get file size safely
37    pub fn file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
38        let metadata = fs::metadata(path)?;
39        Ok(metadata.len())
40    }
41    
42    /// Check if a path is safe to access (no directory traversal)
43    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    /// Create directories recursively with proper error handling
51    pub fn ensure_dir<P: AsRef<Path>>(path: P) -> Result<()> {
52        fs::create_dir_all(path)?;
53        Ok(())
54    }
55    
56    /// Get file extension as lowercase string
57    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    /// Check if file is binary based on content
65    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        // Check for null bytes, which typically indicate binary content
71        Ok(buffer[..bytes_read].contains(&0))
72    }
73}
74
75/// String utilities
76pub mod string {
77    use super::*;
78    
79    /// Truncate string to specified length with ellipsis
80    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    /// Escape string for safe inclusion in JSON
91    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    /// Clean string for use as filename
100    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    /// Convert camelCase to snake_case
111    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    /// Extract lines around a specific line number
127    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
140/// Time utilities
141pub mod time {
142    use super::*;
143    
144    /// Get current timestamp as seconds since Unix epoch
145    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    /// Format duration in human-readable form
153    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    /// Parse duration from human-readable string
176    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
212/// Hash utilities
213pub mod hash {
214    use super::*;
215    use sha2::{Sha256, Digest};
216    
217    /// Generate SHA-256 hash of content
218    pub fn sha256(content: &[u8]) -> String {
219        let mut hasher = Sha256::new();
220        hasher.update(content);
221        hex::encode(hasher.finalize())
222    }
223    
224    /// Generate SHA-256 hash of string
225    pub fn sha256_string(content: &str) -> String {
226        sha256(content.as_bytes())
227    }
228    
229    /// Generate content hash for caching
230    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
240/// URL utilities
241pub mod url {
242    use super::*;
243    
244    /// Validate URL format
245    pub fn is_valid_url(s: &str) -> bool {
246        UrlType::parse(s).is_ok()
247    }
248    
249    /// Extract domain from URL
250    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    /// Join URL paths safely
260    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
271/// Process utilities
272pub mod process {
273    use super::*;
274    
275    /// Check if a command exists in PATH
276    pub fn command_exists(command: &str) -> bool {
277        which::which(command).is_ok()
278    }
279    
280    /// Get available shell command
281    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    /// Escape shell argument
290    pub fn escape_shell_arg(arg: &str) -> String {
291        if cfg!(windows) {
292            // Windows shell escaping
293            if arg.contains(' ') || arg.contains('"') {
294                format!("\"{}\"", arg.replace('"', "\\\""))
295            } else {
296                arg.to_string()
297            }
298        } else {
299            // Unix shell escaping
300            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
309/// Memory utilities
310pub mod memory {
311    use super::*;
312    
313    /// Format bytes in human-readable form
314    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    /// Parse bytes from human-readable string
334    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
364/// Validation utilities
365pub mod validation {
366    use super::*;
367    
368    /// Validate email format
369    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    /// Validate API key format (basic check)
375    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    /// Validate session ID format
380    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    /// Validate model name format
385    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
390/// Configuration utilities
391pub mod config {
392    use super::*;
393    
394    /// Get configuration directory for the application
395    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    /// Get data directory for the application
402    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    /// Get cache directory for the application
409    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    /// Ensure all application directories exist
416    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}