//! Platform abstraction layer for cross-platform compatibility. //! //! This module provides unified interfaces for platform-specific operations, //! making it easy to add support for new platforms by modifying only this file. use std::env; use std::fs; use std::io; use std::path::{Path, PathBuf}; /// Supported platform types. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Platform { Windows, Unix, } impl Platform { /// Detect the current platform. pub fn current() -> Self { if cfg!(target_os = "windows") { Platform::Windows } else { Platform::Unix } } /// Check if running on Windows. pub fn is_windows() -> bool { cfg!(target_os = "windows") } } /// Shell information for command execution. #[derive(Debug, Clone)] pub struct ShellInfo { /// Tool name exposed to LLM. pub name: &'static str, /// Shell executable name. pub executable: &'static str, /// Arguments to pass before the command. pub args: &'static [&'static str], } impl ShellInfo { /// Get the default shell for the current platform. pub fn default() -> Self { Self::for_platform(Platform::current()) } /// Get shell info for a specific platform. pub fn for_platform(platform: Platform) -> Self { match platform { Platform::Windows => ShellInfo { name: "shell", executable: "powershell", args: &["-Command"], }, Platform::Unix => ShellInfo { name: "bash", executable: "bash", args: &["-c"], }, } } /// Alternative shells available on the platform. pub fn available_shells(platform: Platform) -> Vec { match platform { Platform::Windows => vec![ ShellInfo { name: "shell", executable: "powershell", args: &["-Command"], }, ShellInfo { name: "shell", executable: "cmd", args: &["/C"], }, ], Platform::Unix => vec![ ShellInfo { name: "bash", executable: "bash", args: &["-c"], }, // Future: could add zsh, fish, sh // ShellInfo { name: "zsh", executable: "zsh", args: &["-c"] }, ], } } } /// Dangerous command patterns for safety guards. pub fn dangerous_command_patterns() -> Vec { vec![ // Unix dangerous commands r"\brm\s+-[rf]{1,2}\b".to_string(), r"\bchmod\s+-[Rr]".to_string(), r"\bchown\s+-[Rr]".to_string(), // Windows dangerous commands r"\bdel\s+/[fq]\b".to_string(), r"\brmdir\s+/s\b".to_string(), r"\bformat\s+".to_string(), // PowerShell dangerous commands r"\bRemove-Item\s+.*-Recurse".to_string(), r"\bRemove-Item\s+.*-Force".to_string(), // Fork bomb (cross-platform) r":\(\)\s*\{.*\};\s*:".to_string(), ] } /// Get the user's home directory. /// /// Supports environment variable overrides for testing: /// - `HOME` (Unix-style, works on all platforms for testing) /// - `USERPROFILE` (Windows-specific) pub fn home_dir() -> Option { // Test scenario: support HOME variable override on all platforms env::var_os("HOME") .map(PathBuf::from) .or_else(|| { // Windows: support USERPROFILE env::var_os("USERPROFILE").map(PathBuf::from) }) .or_else(|| dirs::home_dir()) } /// Atomically rename a file, handling platform differences. /// /// On Windows, `fs::rename` fails if the destination exists, so we need to /// remove it first. On Unix, rename is atomic and replaces the destination. pub fn atomic_rename(src: &Path, dst: &Path) -> io::Result<()> { if Platform::is_windows() && dst.exists() { fs::remove_file(dst)?; } fs::rename(src, dst) } /// Convert a filesystem path to a file:// URI. /// /// Handles platform-specific path formats: /// - Unix: `/path/to/file` -> `file:///path/to/file` /// - Windows: `C:\path\to\file` -> `file:///C:/path/to/file` pub fn path_to_uri(path: &Path) -> String { let path_str = path.display().to_string(); if Platform::is_windows() { // Windows paths use backslashes which must be converted to forward slashes let normalized = path_str.replace('\\', "/"); format!("file:///{}", normalized) } else { format!("file://{}", path_str) } } /// XML escape utility. pub fn xml_escape(value: &str) -> String { value .replace('&', "&") .replace('<', "<") .replace('>', ">") } #[cfg(test)] mod tests { use super::*; #[test] fn test_platform_detect() { let platform = Platform::current(); if cfg!(target_os = "windows") { assert_eq!(platform, Platform::Windows); } else { assert_eq!(platform, Platform::Unix); } } #[test] fn test_shell_info_default() { let shell = ShellInfo::default(); if cfg!(target_os = "windows") { assert_eq!(shell.executable, "powershell"); assert_eq!(shell.args, &["-Command"]); } else { assert_eq!(shell.executable, "bash"); assert_eq!(shell.args, &["-c"]); } } #[test] fn test_shell_info_for_platform() { let win_shell = ShellInfo::for_platform(Platform::Windows); assert_eq!(win_shell.executable, "powershell"); let unix_shell = ShellInfo::for_platform(Platform::Unix); assert_eq!(unix_shell.executable, "bash"); } #[test] fn test_path_to_uri() { let temp_dir = tempfile::tempdir().unwrap(); let test_path = temp_dir.path().join("test.txt"); let uri = path_to_uri(&test_path); assert!(uri.starts_with("file://")); assert!(uri.contains("test.txt")); assert!(!uri.contains('\\')); // No backslashes } #[test] fn test_path_to_uri_windows_format() { if cfg!(target_os = "windows") { let win_path = PathBuf::from("C:\\Users\\test\\file.txt"); let uri = path_to_uri(&win_path); assert!(uri.starts_with("file:///C:/")); assert_eq!(uri, "file:///C:/Users/test/file.txt"); } } #[test] fn test_atomic_rename() { let temp_dir = tempfile::tempdir().unwrap(); let src = temp_dir.path().join("source.txt"); let dst = temp_dir.path().join("dest.txt"); fs::write(&src, "content").unwrap(); fs::write(&dst, "old content").unwrap(); atomic_rename(&src, &dst).unwrap(); assert!(!src.exists()); assert!(dst.exists()); assert_eq!(fs::read_to_string(&dst).unwrap(), "content"); } #[test] fn test_dangerous_patterns() { let patterns = dangerous_command_patterns(); assert!(!patterns.is_empty()); // Should contain patterns for both platforms assert!(patterns.iter().any(|p| p.contains("rm"))); assert!(patterns.iter().any(|p| p.contains("del"))); } #[test] fn test_xml_escape() { assert_eq!(xml_escape("a & b"), "a & b"); assert_eq!(xml_escape(""), "<tag>"); } }