brlapi 0.4.1

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

//! Connection settings and configuration

use crate::{BrlApiError, Result};
use brlapi_sys::brlapi_connectionSettings_t;
use std::ffi::CString;
use std::ptr;
use std::time::Duration;

/// BrlAPI connection settings
#[derive(Debug, Clone)]
pub struct ConnectionSettings {
    /// The location of the authentication file
    auth: Option<String>,
    /// The host on which the BRLTTY daemon is listening  
    host: Option<String>,
    /// Connection timeout (defaults to 10 seconds)
    timeout: Duration,
}

impl Default for ConnectionSettings {
    fn default() -> Self {
        Self {
            auth: None,
            host: None,
            timeout: Duration::from_secs(10), // 10 second default timeout
        }
    }
}

impl ConnectionSettings {
    /// Create new connection settings with default values
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Create connection settings for localhost
    #[must_use]
    pub fn localhost() -> Self {
        Self {
            host: Some("localhost".to_string()),
            auth: None,
            timeout: Duration::from_secs(10),
        }
    }

    /// Create connection settings for a specific host
    #[must_use]
    pub fn for_host(host: impl Into<String>) -> Self {
        Self {
            host: Some(host.into()),
            auth: None,
            timeout: Duration::from_secs(10),
        }
    }

    /// Create connection settings with a specific auth file
    #[must_use]
    pub fn with_auth_file(auth_path: impl Into<String>) -> Self {
        Self {
            auth: Some(auth_path.into()),
            host: None,
            timeout: Duration::from_secs(10),
        }
    }

    /// Set the authentication file path
    pub fn set_auth(&mut self, auth_path: impl Into<String>) -> &mut Self {
        self.auth = Some(auth_path.into());
        self
    }

    /// Set the host
    pub fn set_host(&mut self, host: impl Into<String>) -> &mut Self {
        self.host = Some(host.into());
        self
    }

    /// Set the connection timeout
    pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
        self.timeout = timeout;
        self
    }

    /// Create connection settings with a specific timeout
    #[must_use]
    pub fn with_timeout(timeout: Duration) -> Self {
        Self {
            auth: None,
            host: None,
            timeout,
        }
    }

    /// Get the authentication file path
    pub fn auth(&self) -> Option<&str> {
        self.auth.as_deref()
    }

    /// Get the host
    pub fn host(&self) -> Option<&str> {
        self.host.as_deref()
    }

    /// Get the connection timeout
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// Convert to C-compatible settings structure
    ///
    /// Returns the C struct along with owned CStrings that must be kept alive
    /// for the duration of the C API call.
    ///
    /// # Errors
    ///
    /// Returns `BrlApiError::InvalidParameter` if the auth path or host contain null bytes.
    pub fn to_c_settings(
        &self,
    ) -> Result<(
        brlapi_connectionSettings_t,
        Option<CString>,
        Option<CString>,
    )> {
        let auth_cstr = match &self.auth {
            Some(s) => Some(CString::new(s.as_str()).map_err(|_| BrlApiError::InvalidParameter)?),
            None => None,
        };

        let host_cstr = match &self.host {
            Some(s) => Some(CString::new(s.as_str()).map_err(|_| BrlApiError::InvalidParameter)?),
            None => None,
        };

        let c_settings = brlapi_connectionSettings_t {
            auth: auth_cstr.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
            host: host_cstr.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
        };

        Ok((c_settings, auth_cstr, host_cstr))
    }
}

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

    #[test]
    fn test_default_settings() {
        let settings = ConnectionSettings::default();
        assert!(settings.auth.is_none());
        assert!(settings.host.is_none());
    }

    #[test]
    fn test_localhost_settings() {
        let settings = ConnectionSettings::localhost();
        assert!(settings.auth.is_none());
        assert_eq!(settings.host, Some("localhost".to_string()));
    }

    #[test]
    fn test_builder_pattern() {
        let mut settings = ConnectionSettings::new();
        settings.set_host("example.com").set_auth("/etc/brlapi.key");

        assert_eq!(settings.host, Some("example.com".to_string()));
        assert_eq!(settings.auth, Some("/etc/brlapi.key".to_string()));
    }

    #[test]
    fn test_c_conversion() {
        let settings = ConnectionSettings {
            auth: Some("/etc/brlapi.key".to_string()),
            host: Some("localhost".to_string()),
            timeout: Duration::from_secs(5),
        };

        let (c_settings, auth_str, host_str) = settings
            .to_c_settings()
            .expect("Valid settings should convert to C settings");

        assert!(auth_str.is_some());
        assert!(host_str.is_some());
        assert!(!c_settings.auth.is_null());
        assert!(!c_settings.host.is_null());
    }

    #[test]
    fn test_timeout_settings() {
        let settings = ConnectionSettings::with_timeout(Duration::from_secs(30));
        assert_eq!(settings.timeout(), Duration::from_secs(30));

        let mut settings = ConnectionSettings::new();
        settings.set_timeout(Duration::from_millis(5000));
        assert_eq!(settings.timeout(), Duration::from_millis(5000));
    }

    #[test]
    #[should_panic]
    fn test_connection_settings_null_byte_in_host() {
        let settings = ConnectionSettings::for_host("example.com\0malicious");
        let _ = settings
            .to_c_settings()
            .expect("Settings conversion should succeed for valid settings");
    }

    #[test]
    #[should_panic]
    fn test_connection_settings_null_byte_in_auth() {
        let settings = ConnectionSettings::with_auth_file("/path/to\0malicious/auth");
        let _ = settings
            .to_c_settings()
            .expect("Settings conversion should succeed for valid settings");
    }

    #[test]
    fn test_connection_settings_validation() {
        let settings = ConnectionSettings::for_host("example.com\0malicious");
        assert!(settings.to_c_settings().is_err());

        let settings = ConnectionSettings::with_auth_file("/path/to\0malicious/auth");
        assert!(settings.to_c_settings().is_err());

        // Valid settings should work
        let settings = ConnectionSettings::for_host("example.com");
        assert!(settings.to_c_settings().is_ok());
    }
}