rpi-tools 0.1.28

Built-in agent tools (read/write/edit/bash/grep/find/ls) + ExecutionEnv, Rust port of pi harness tools
Documentation
//! Open URL in the default browser — mirrors `packages/coding-agent/src/utils/open-browser.ts`.
//!
//! Cross-platform utility to open a URL in the user's default browser.
//! Intentionally avoids invoking a shell to prevent command injection from
//! attacker-controlled URLs.
//!
//! # Platform Support
//!
//! - **macOS**: Uses `open` command
//! - **Windows**: Uses `rundll32 url.dll,FileProtocolHandler`
//! - **Linux/Unix**: Uses `xdg-open`
//!
//! # Security
//!
//! This implementation deliberately does NOT use a shell. On Windows, using
//! `cmd /c start` would allow attacker-controlled URLs to inject metacharacters
//! (&, |, ^, etc.) that cmd.exe re-parses before `start` runs.
//!
//! # Example
//!
//! ```rust
//! use rpi_tools::utils::open_browser::open_browser;
//!
//! // Open a URL in the default browser
//! if let Err(e) = open_browser("https://example.com") {
//!     eprintln!("Failed to open browser: {}", e);
//! }
//! ```

use std::process::Command;

/// Open a URL in the default browser. Returns `Ok(())` if the browser launch
/// command was successfully invoked (not if the browser actually opened the URL).
///
/// # Arguments
///
/// * `url` - The URL to open. Should be a valid URL string.
///
/// # Returns
///
/// * `Ok(())` - The browser launch command was invoked successfully.
/// * `Err(String)` - Failed to invoke the browser launch command.
///
/// # Platform-specific behavior
///
/// - **macOS**: Runs `open <url>`
/// - **Windows**: Runs `rundll32 url.dll,FileProtocolHandler <url>`
/// - **Linux/Unix**: Runs `xdg-open <url>`
///
/// # Security notes
///
/// This function intentionally does NOT invoke a shell. The URL is passed
/// directly as an argument to the platform-specific launcher, preventing
/// command injection from malicious URLs.
pub fn open_browser(url: &str) -> Result<(), String> {
    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
        ("open", vec![url])
    } else if cfg!(target_os = "windows") {
        ("rundll32", vec!["url.dll,FileProtocolHandler", url])
    } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") {
        ("xdg-open", vec![url])
    } else {
        return Err(format!("Unsupported platform for opening browser"));
    };

    // Spawn the command without waiting for it to complete.
    // Browser launch is best-effort: we don't want to block if the browser
    // takes time to start, and we don't want a launcher failure to crash
    // the application.
    match Command::new(cmd).args(&args).spawn() {
        Ok(_child) => {
            // Successfully spawned. We don't wait for it to complete.
            Ok(())
        }
        Err(e) => {
            // Failed to spawn. This could be because the command doesn't exist
            // (e.g., xdg-open not installed on a minimal Linux system).
            Err(format!("Failed to open browser: {}", e))
        }
    }
}

/// Open a file with the default application. Returns `Ok(())` if the launch
/// command was successfully invoked.
///
/// # Arguments
///
/// * `path` - The file path to open.
///
/// # Returns
///
/// * `Ok(())` - The launch command was invoked successfully.
/// * `Err(String)` - Failed to invoke the launch command.
pub fn open_file(path: &str) -> Result<(), String> {
    let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
        ("open", vec![path])
    } else if cfg!(target_os = "windows") {
        // On Windows, we can use `start` but need to be careful about shells.
        // Using rundll32 with FileProtocolHandler works for files too.
        ("rundll32", vec!["url.dll,FileProtocolHandler", path])
    } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") {
        ("xdg-open", vec![path])
    } else {
        return Err(format!("Unsupported platform for opening file"));
    };

    match Command::new(cmd).args(&args).spawn() {
        Ok(_) => Ok(()),
        Err(e) => Err(format!("Failed to open file: {}", e)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[ignore] // This test actually opens a browser, so it's ignored by default
    fn test_open_browser() {
        // This would open a browser in a real test
        let result = open_browser("https://example.com");
        assert!(result.is_ok());
    }

    #[test]
    fn test_platform_detection() {
        // Just verify the function doesn't panic on the current platform
        let _ = open_browser("https://example.com");
    }
}