Skip to main content

sentinelpass_protocol/transport/
mod.rs

1//! Wire framing and platform connection types for IPC clients.
2//!
3//! Server-side transports (listeners) live in `sentinelpass-core`; this
4//! module holds everything a client needs plus the shared connection types.
5
6#[cfg(unix)]
7pub mod unix;
8#[cfg(windows)]
9pub mod windows;
10
11use std::io;
12
13/// Maximum message size for IPC (64KB)
14pub const MAX_MESSAGE_SIZE: usize = 65536;
15
16/// Result type for transport operations
17pub type TransportResult<T> = Result<T, TransportError>;
18
19/// Transport-specific errors
20#[derive(Debug)]
21pub enum TransportError {
22    ConnectionFailed(String),
23    Io(io::Error),
24    MessageTooLarge { size: usize, max: usize },
25    InvalidFormat(String),
26    Encryption(String),
27    Decryption(String),
28    Closed,
29    Timeout,
30    Other(String),
31}
32
33impl std::fmt::Display for TransportError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
37            Self::Io(e) => write!(f, "IO error: {}", e),
38            Self::MessageTooLarge { size, max } => {
39                write!(f, "Message too large: {} bytes (max: {} bytes)", size, max)
40            }
41            Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
42            Self::Encryption(msg) => write!(f, "Encryption failed: {}", msg),
43            Self::Decryption(msg) => write!(f, "Decryption failed: {}", msg),
44            Self::Closed => write!(f, "Transport closed"),
45            Self::Timeout => write!(f, "Timeout"),
46            Self::Other(msg) => write!(f, "Other: {}", msg),
47        }
48    }
49}
50
51impl std::error::Error for TransportError {
52    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        match self {
54            Self::Io(e) => Some(e),
55            _ => None,
56        }
57    }
58}
59
60impl From<io::Error> for TransportError {
61    fn from(err: io::Error) -> Self {
62        Self::Io(err)
63    }
64}
65
66/// Transport configuration
67#[derive(Debug, Clone, Default)]
68pub struct TransportConfig {
69    /// Path for Unix domain socket
70    pub unix_socket_path: Option<String>,
71
72    /// Path for Windows named pipe
73    pub windows_pipe_path: Option<String>,
74
75    /// TCP address for fallback (Windows only)
76    pub tcp_fallback_addr: Option<String>,
77
78    /// Authentication token for encrypted transports
79    pub auth_token: Option<String>,
80}
81
82impl TransportConfig {
83    /// Create a new transport configuration with defaults for the current platform
84    pub fn for_current_platform() -> Self {
85        #[cfg(unix)]
86        {
87            let runtime_dir =
88                std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
89            Self {
90                unix_socket_path: Some(format!("{}/sentinelpass.sock", runtime_dir)),
91                ..Default::default()
92            }
93        }
94
95        #[cfg(windows)]
96        {
97            Self {
98                windows_pipe_path: Some(r"\\.\pipe\SentinelPass".to_string()),
99                tcp_fallback_addr: Some("127.0.0.1:35873".to_string()),
100                ..Default::default()
101            }
102        }
103
104        #[cfg(not(any(unix, windows)))]
105        {
106            Self::default()
107        }
108    }
109
110    /// Set the authentication token
111    pub fn with_auth_token(mut self, token: String) -> Self {
112        self.auth_token = Some(token);
113        self
114    }
115
116    /// Set the Unix socket path
117    pub fn with_unix_socket(mut self, path: String) -> Self {
118        self.unix_socket_path = Some(path);
119        self
120    }
121
122    /// Set the Windows named pipe path
123    pub fn with_windows_pipe(mut self, path: String) -> Self {
124        self.windows_pipe_path = Some(path);
125        self
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_transport_config_defaults() {
135        let config = TransportConfig::default();
136        assert!(config.unix_socket_path.is_none());
137        assert!(config.windows_pipe_path.is_none());
138        assert!(config.auth_token.is_none());
139    }
140
141    #[test]
142    fn test_transport_config_builder() {
143        let config = TransportConfig::default()
144            .with_auth_token("test_token".to_string())
145            .with_unix_socket("/tmp/test.sock".to_string());
146
147        assert_eq!(config.auth_token, Some("test_token".to_string()));
148        assert_eq!(config.unix_socket_path, Some("/tmp/test.sock".to_string()));
149    }
150
151    #[test]
152    fn test_max_message_size() {
153        assert_eq!(MAX_MESSAGE_SIZE, 65536);
154    }
155
156    #[test]
157    fn test_transport_error_display() {
158        let err = TransportError::ConnectionFailed("test".to_string());
159        assert_eq!(err.to_string(), "Connection failed: test");
160
161        let err = TransportError::MessageTooLarge {
162            size: 100000,
163            max: 65536,
164        };
165        assert_eq!(
166            err.to_string(),
167            "Message too large: 100000 bytes (max: 65536 bytes)"
168        );
169    }
170
171    #[test]
172    fn test_transport_error_from_io() {
173        let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "test");
174        let transport_err: TransportError = io_err.into();
175        assert!(matches!(transport_err, TransportError::Io(_)));
176    }
177}