coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! File utilities for CoderLib

use std::path::{Path, PathBuf};
use std::fs;

/// Check if a file exists and is readable
pub fn is_readable_file<P: AsRef<Path>>(path: P) -> bool {
    let path = path.as_ref();
    path.exists() && path.is_file() && fs::metadata(path).map(|m| !m.permissions().readonly()).unwrap_or(false)
}

/// Check if a directory exists and is accessible
pub fn is_accessible_directory<P: AsRef<Path>>(path: P) -> bool {
    let path = path.as_ref();
    path.exists() && path.is_dir()
}

/// Get file extension in lowercase
pub fn get_file_extension<P: AsRef<Path>>(path: P) -> Option<String> {
    path.as_ref()
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_lowercase())
}

/// Check if a file has one of the allowed extensions
pub fn has_allowed_extension<P: AsRef<Path>>(path: P, allowed: &[String]) -> bool {
    if let Some(ext) = get_file_extension(path) {
        allowed.iter().any(|allowed_ext| {
            allowed_ext.trim_start_matches('.').eq_ignore_ascii_case(&ext)
        })
    } else {
        false
    }
}

/// Get the size of a file in bytes
pub fn get_file_size<P: AsRef<Path>>(path: P) -> Result<u64, std::io::Error> {
    fs::metadata(path).map(|m| m.len())
}

/// Check if a file is within size limits
pub fn is_within_size_limit<P: AsRef<Path>>(path: P, max_size: u64) -> bool {
    get_file_size(path).map(|size| size <= max_size).unwrap_or(false)
}

/// Create a backup of a file
pub fn create_backup<P: AsRef<Path>>(path: P) -> Result<PathBuf, std::io::Error> {
    let path = path.as_ref();
    let backup_path = path.with_extension(format!("{}.bak", 
        path.extension().and_then(|s| s.to_str()).unwrap_or("")));
    
    fs::copy(path, &backup_path)?;
    Ok(backup_path)
}

/// Find files matching a pattern in a directory
pub fn find_files_by_pattern<P: AsRef<Path>>(
    dir: P,
    pattern: &str,
    recursive: bool,
) -> Result<Vec<PathBuf>, std::io::Error> {
    let mut results = Vec::new();
    find_files_recursive(dir.as_ref(), pattern, recursive, &mut results)?;
    Ok(results)
}

fn find_files_recursive(
    dir: &Path,
    pattern: &str,
    recursive: bool,
    results: &mut Vec<PathBuf>,
) -> Result<(), std::io::Error> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        
        if path.is_file() {
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                if name.contains(pattern) {
                    results.push(path);
                }
            }
        } else if path.is_dir() && recursive {
            find_files_recursive(&path, pattern, recursive, results)?;
        }
    }
    Ok(())
}

/// Get the relative path from one path to another
pub fn get_relative_path<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Option<PathBuf> {
    pathdiff::diff_paths(to, from)
}

/// Normalize a path by resolving . and .. components
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
    let path = path.as_ref();
    let mut components = Vec::new();
    
    for component in path.components() {
        match component {
            std::path::Component::CurDir => {
                // Skip current directory
            }
            std::path::Component::ParentDir => {
                // Go up one directory
                components.pop();
            }
            _ => {
                components.push(component);
            }
        }
    }
    
    components.iter().collect()
}

/// Check if a path is safe (doesn't contain dangerous patterns)
pub fn is_safe_path<P: AsRef<Path>>(path: P) -> bool {
    let path_str = path.as_ref().to_string_lossy();
    
    // Check for path traversal
    if path_str.contains("..") {
        return false;
    }
    
    // Check for absolute paths to system directories
    let dangerous_prefixes = [
        "/etc", "/sys", "/proc", "/dev", "/boot",
        "C:\\Windows", "C:\\System32", "C:\\Program Files",
        "/System", "/Library/System",
    ];
    
    for prefix in &dangerous_prefixes {
        if path_str.starts_with(prefix) {
            return false;
        }
    }
    
    true
}

/// Get the MIME type of a file based on its extension
pub fn get_mime_type<P: AsRef<Path>>(path: P) -> String {
    match get_file_extension(path).as_deref() {
        Some("txt") => "text/plain",
        Some("md") => "text/markdown",
        Some("rs") => "text/x-rust",
        Some("py") => "text/x-python",
        Some("js") => "text/javascript",
        Some("ts") => "text/typescript",
        Some("html") => "text/html",
        Some("css") => "text/css",
        Some("json") => "application/json",
        Some("xml") => "application/xml",
        Some("yaml") | Some("yml") => "application/x-yaml",
        Some("toml") => "application/toml",
        Some("png") => "image/png",
        Some("jpg") | Some("jpeg") => "image/jpeg",
        Some("gif") => "image/gif",
        Some("svg") => "image/svg+xml",
        Some("pdf") => "application/pdf",
        Some("zip") => "application/zip",
        _ => "application/octet-stream",
    }.to_string()
}

/// Check if a file is a text file based on its extension
pub fn is_text_file<P: AsRef<Path>>(path: P) -> bool {
    let mime_type = get_mime_type(path);
    mime_type.starts_with("text/") || 
    mime_type == "application/json" ||
    mime_type == "application/xml" ||
    mime_type == "application/x-yaml" ||
    mime_type == "application/toml"
}

/// Get a temporary file path with the given extension
pub fn get_temp_file_path(extension: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    path.push(format!("coderlib_{}.{}", 
        uuid::Uuid::new_v4().to_string()[..8].to_string(),
        extension.trim_start_matches('.')
    ));
    path
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn test_get_file_extension() {
        assert_eq!(get_file_extension("test.txt"), Some("txt".to_string()));
        assert_eq!(get_file_extension("test.RS"), Some("rs".to_string()));
        assert_eq!(get_file_extension("test"), None);
    }

    #[test]
    fn test_has_allowed_extension() {
        let allowed = vec![".txt".to_string(), ".rs".to_string()];
        assert!(has_allowed_extension("test.txt", &allowed));
        assert!(has_allowed_extension("test.RS", &allowed));
        assert!(!has_allowed_extension("test.py", &allowed));
    }

    #[test]
    fn test_is_safe_path() {
        assert!(is_safe_path("safe/path/file.txt"));
        assert!(!is_safe_path("../dangerous"));
        assert!(!is_safe_path("/etc/passwd"));
        assert!(!is_safe_path("C:\\Windows\\system32"));
    }

    #[test]
    fn test_get_mime_type() {
        assert_eq!(get_mime_type("test.txt"), "text/plain");
        assert_eq!(get_mime_type("test.rs"), "text/x-rust");
        assert_eq!(get_mime_type("test.json"), "application/json");
        assert_eq!(get_mime_type("test.unknown"), "application/octet-stream");
    }

    #[test]
    fn test_is_text_file() {
        assert!(is_text_file("test.txt"));
        assert!(is_text_file("test.rs"));
        assert!(is_text_file("test.json"));
        assert!(!is_text_file("test.png"));
        assert!(!is_text_file("test.exe"));
    }

    #[test]
    fn test_normalize_path() {
        assert_eq!(normalize_path("./test/../file.txt"), PathBuf::from("file.txt"));
        assert_eq!(normalize_path("dir/./file.txt"), PathBuf::from("dir/file.txt"));
    }

    #[test]
    fn test_file_operations() {
        let temp_dir = tempdir().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        
        // Create a test file
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "test content").unwrap();
        
        assert!(is_readable_file(&file_path));
        assert!(get_file_size(&file_path).unwrap() > 0);
        assert!(is_within_size_limit(&file_path, 1000));
        
        // Test backup creation
        let backup_path = create_backup(&file_path).unwrap();
        assert!(backup_path.exists());
    }

    #[test]
    fn test_get_temp_file_path() {
        let temp_path = get_temp_file_path("txt");
        assert!(temp_path.to_string_lossy().contains("coderlib_"));
        assert!(temp_path.to_string_lossy().ends_with(".txt"));
    }
}