brlapi 0.4.1

Safe Rust bindings for the BrlAPI library
// SPDX-License-Identifier: LGPL-2.1

//! BrlAPI Suspend Mode - Complete driver disconnection for hardware access
//!
//! # WARNING: EXTREMELY DANGEROUS
//!
//! Suspend mode completely disconnects the BrlAPI server from the braille device,
//! allowing a client to directly open and control the hardware. This is much more
//! dangerous than raw mode and should be avoided whenever possible.
//!
//! **CRITICAL WARNINGS:**
//! - Can leave braille device completely inaccessible if client crashes
//! - May cause system instability or device damage if misused
//! - Renders screen readers (Orca, NVDA) completely unable to access braille
//! - Only one client can be in suspend mode at a time
//! - BrlAPI documentation states "This mode is not recommended"
//! - Official guidance: "Raw mode should be sufficient for any use"
//!
//! # When to Use Suspend Mode
//!
//! Suspend mode should ONLY be used when:
//! - Device firmware updates require exclusive hardware access
//! - Raw mode is insufficient for driver development/debugging
//! - Emergency device recovery when driver is stuck/corrupted
//! - You have no other choice and understand the risks
//!
//! **Consider using [`crate::raw`] instead** - it provides direct hardware
//! communication while keeping the driver active and system stable.
//!
//! # Safety Requirements
//!
//! 1. **Feature Flag Required**: Must compile with `--features dangerous-suspend-mode`
//! 2. **RAII Usage**: Always use the `SuspendMode` wrapper for automatic cleanup
//! 3. **Error Handling**: Always handle errors - failed resume leaves device unusable
//! 4. **Testing**: Test thoroughly with non-critical devices first
//! 5. **Backup Plan**: Have alternative access method if suspend mode fails
//!
//! # Example Usage
//!
//! ```no_run
//! # #[cfg(feature = "dangerous-suspend-mode")]
//! # {
//! use brlapi::{Connection, suspend::SuspendMode};
//!
//! fn emergency_device_recovery() -> Result<(), brlapi::BrlApiError> {
//!     let connection = Connection::open()?;
//!     let driver_name = connection.display_driver()?;
//!
//!     // WARNING: This completely disconnects the driver from hardware
//!     let suspend_mode = SuspendMode::enter(&connection, &driver_name)?;
//!
//!     // Perform critical hardware operations
//!     // Your code here - you now have exclusive hardware access
//!
//!     // Driver automatically resumed when suspend_mode is dropped
//!     Ok(())
//! }
//! # }
//! ```

use crate::{BrlApiError, Connection, Result, brlapi_call};
use std::ffi::CString;

/// Complete driver suspension for exclusive hardware access
///
/// **WARNING: EXTREMELY DANGEROUS** - This completely disconnects the BrlAPI server
/// from the braille device. Use only when raw mode is insufficient and you understand
/// the severe risks.
///
/// # Safety Features
///
/// - **RAII Cleanup**: Driver automatically resumed when dropped
/// - **Error Recovery**: Comprehensive error handling for suspend/resume failures
/// - **Validation**: Driver existence checked before suspension
/// - **Thread Safety**: Uses handle-based BrlAPI functions
///
/// # Risks
///
/// - **Device Inaccessibility**: If client crashes, device may be left unusable
/// - **System Instability**: Complete driver disconnection can cause issues
/// - **Screen Reader Disruption**: Orca/NVDA completely lose braille access
/// - **Hardware Damage**: Direct hardware access without driver safety checks
#[derive(Debug)]
pub struct SuspendMode<'a> {
    connection: &'a Connection,
    driver_name: String,
    suspended: bool,
}

impl<'a> SuspendMode<'a> {
    /// Enter suspend mode by completely disconnecting the specified driver
    ///
    /// **WARNING: EXTREMELY DANGEROUS** - This completely removes the driver's
    /// access to the hardware. The device becomes exclusively available to your
    /// application until the driver is resumed.
    ///
    /// # Arguments
    ///
    /// * `connection` - Active BrlAPI connection
    /// * `driver` - Name of the driver to suspend (e.g., "baum", "hims", "alva")
    ///
    /// # Safety Requirements
    ///
    /// 1. **Must have exclusive access** - No other applications using the device
    /// 2. **Must handle errors** - Failed resume can leave device inaccessible
    /// 3. **Must test safely** - Use non-critical devices for development
    /// 4. **Must have backup plan** - Alternative access if suspend fails
    ///
    /// # Errors
    ///
    /// - `BrlApiError::SuspendFailed` - Driver suspension failed
    /// - `BrlApiError::DriverNotFound` - Specified driver doesn't exist
    /// - `BrlApiError::DeviceBusy` - Another client already has exclusive access
    /// - `BrlApiError::PermissionDenied` - Insufficient privileges for suspend
    ///
    /// # Example
    ///
    /// ```no_run
    /// # #[cfg(feature = "dangerous-suspend-mode")]
    /// # {
    /// use brlapi::{Connection, suspend::SuspendMode};
    ///
    /// fn firmware_update() -> Result<(), brlapi::BrlApiError> {
    ///     let connection = Connection::open()?;
    ///     let driver = connection.display_driver()?;
    ///
    ///     // Suspend driver for exclusive hardware access
    ///     let _suspend_mode = SuspendMode::enter(&connection, &driver)?;
    ///
    ///     // Your firmware update code here
    ///     // Driver automatically resumed when _suspend_mode is dropped
    ///
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn enter(connection: &'a Connection, driver: &str) -> Result<Self> {
        // Validate driver name
        if driver.is_empty() {
            return Err(BrlApiError::custom("Driver name cannot be empty"));
        }

        // Check if driver exists by querying current driver
        let current_driver = connection.display_driver().map_err(|e| {
            BrlApiError::custom(&format!("Cannot verify driver '{}' exists: {}", driver, e))
        })?;

        if current_driver != driver {
            return Err(BrlApiError::custom(&format!(
                "Driver '{}' is not currently active (current: '{}')",
                driver, current_driver
            )));
        }

        let driver_cstring = CString::new(driver)
            .map_err(|_| BrlApiError::custom("Driver name contains null bytes"))?;

        // Attempt to suspend the driver
        crate::brlapi_call!(unsafe {
            brlapi_sys::brlapi__suspendDriver(connection.handle_ptr(), driver_cstring.as_ptr())
        }).map_err(|e| BrlApiError::suspend_failed(driver, &e.to_string()))?;

        Ok(Self {
            connection,
            driver_name: driver.to_string(),
            suspended: true,
        })
    }

    /// Check if the driver is currently suspended
    ///
    /// This returns `true` if the driver is suspended and hardware access
    /// is available to the client. When this returns `false`, the driver
    /// has been resumed and normal BrlAPI operations are available.
    pub fn is_suspended(&self) -> bool {
        self.suspended
    }

    /// Get the name of the suspended driver
    ///
    /// Returns the driver name that was passed to `enter()` and is currently
    /// suspended for exclusive hardware access.
    pub fn driver_name(&self) -> &str {
        &self.driver_name
    }

    /// Manually resume the driver (automatic via Drop trait)
    ///
    /// This explicitly resumes the suspended driver, restoring normal BrlAPI
    /// operation. This is also called automatically when the `SuspendMode`
    /// is dropped, so manual calls are usually unnecessary.
    ///
    /// # Errors
    ///
    /// - `BrlApiError::ResumeFailed` - Driver resume failed
    /// - `BrlApiError::NotSuspended` - No driver is currently suspended
    ///
    /// # Example
    ///
    /// ```no_run
    /// # #[cfg(feature = "dangerous-suspend-mode")]
    /// # {
    /// use brlapi::{Connection, suspend::SuspendMode};
    ///
    /// fn manual_resume() -> Result<(), brlapi::BrlApiError> {
    ///     let connection = Connection::open()?;
    ///     let driver = connection.display_driver()?;
    ///
    ///     let mut suspend_mode = SuspendMode::enter(&connection, &driver)?;
    ///
    ///     // Do hardware operations...
    ///
    ///     // Explicitly resume (optional - happens automatically on drop)
    ///     suspend_mode.resume()?;
    ///
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn resume(&mut self) -> Result<()> {
        if !self.suspended {
            return Err(BrlApiError::custom("No driver is currently suspended"));
        }

        match crate::brlapi_call!(unsafe { brlapi_sys::brlapi__resumeDriver(self.connection.handle_ptr()) })
            .map_err(|e| BrlApiError::resume_failed(&self.driver_name, &e.to_string()))
        {
            Ok(_) => {
                self.suspended = false;
                Ok(())
            }
            Err(e) => {
                // Even if resume failed, mark as not suspended to prevent double resume
                self.suspended = false;
                Err(e)
            }
        }
    }

    /// Check if a driver supports suspend mode
    ///
    /// This provides a basic heuristic for whether a driver might support
    /// suspend mode. Note that not all drivers support suspension, and some
    /// may support it but not recommend it.
    ///
    /// # Known Driver Support
    ///
    /// - **Usually Supported**: Most hardware drivers (baum, hims, alva, etc.)
    /// - **Not Supported**: NoBraille driver (used for testing)
    /// - **Varies**: Virtual/emulated drivers may not support suspension
    ///
    /// **Note**: This is a heuristic only. The definitive test is attempting
    /// to enter suspend mode and handling any errors appropriately.
    pub fn is_driver_supported(driver: &str) -> bool {
        match driver {
            // Drivers that definitely don't support suspend mode
            "nb" | "NoBraille" => false,
            // Virtual/test drivers that may not support suspend
            "fake" | "test" | "dummy" => false,
            // Most hardware drivers should support suspend mode
            _ => true,
        }
    }
}

impl Drop for SuspendMode<'_> {
    /// Automatically resume the driver when SuspendMode is dropped
    ///
    /// This is the primary safety mechanism - even if your code panics or
    /// exits unexpectedly, the driver will be resumed and normal braille
    /// access restored.
    ///
    /// **Note**: If resume fails during drop, the error is logged but not
    /// propagated (since Drop cannot return errors). This is why explicit
    /// error handling with `resume()` is recommended for critical applications.
    fn drop(&mut self) {
        if self.suspended {
            if let Err(e) = self.resume() {
                eprintln!(
                    "WARNING: Failed to resume driver '{}' during cleanup: {}",
                    self.driver_name, e
                );
                eprintln!("The braille device may be left in an inaccessible state!");
                eprintln!("You may need to restart the BrlAPI daemon or reboot the system.");
            }
        }
    }
}

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

    #[test]
    fn test_driver_support_detection() {
        // Test drivers that don't support suspend
        assert!(!SuspendMode::is_driver_supported("nb"));
        assert!(!SuspendMode::is_driver_supported("NoBraille"));
        assert!(!SuspendMode::is_driver_supported("fake"));
        assert!(!SuspendMode::is_driver_supported("test"));
        assert!(!SuspendMode::is_driver_supported("dummy"));

        // Test drivers that should support suspend
        assert!(SuspendMode::is_driver_supported("baum"));
        assert!(SuspendMode::is_driver_supported("hims"));
        assert!(SuspendMode::is_driver_supported("alva"));
        assert!(SuspendMode::is_driver_supported("papenmeier"));
        assert!(SuspendMode::is_driver_supported("freedom"));
        assert!(SuspendMode::is_driver_supported("unknown_driver"));
    }

    #[test]
    fn test_driver_name_validation() {
        // These tests check input validation without requiring BrlAPI daemon
        let connection = Connection::open();

        // Skip if no daemon available
        if connection.is_err() {
            println!("Skipping suspend mode driver validation tests - no BrlAPI daemon");
            return;
        }

        let connection = connection
            .expect("Connection should be available for suspend mode driver validation tests");

        // Test empty driver name
        let result = SuspendMode::enter(&connection, "");
        assert!(result.is_err());
        if let Err(e) = result {
            assert!(e.to_string().contains("empty"));
        }

        // Test driver name with null bytes
        let result = SuspendMode::enter(&connection, "driver\0name");
        assert!(result.is_err());
        if let Err(e) = result {
            assert!(e.to_string().contains("null bytes"));
        }
    }
}