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    /// Authentication token for encrypted transports
76    pub auth_token: Option<String>,
77}
78
79impl TransportConfig {
80    /// Create a new transport configuration with defaults for the current platform
81    pub fn for_current_platform() -> Self {
82        #[cfg(unix)]
83        {
84            Self {
85                unix_socket_path: Some(
86                    crate::paths::default_ipc_socket_path()
87                        .to_string_lossy()
88                        .to_string(),
89                ),
90                ..Default::default()
91            }
92        }
93
94        #[cfg(windows)]
95        {
96            Self {
97                windows_pipe_path: Some(r"\\.\pipe\SentinelPass".to_string()),
98                ..Default::default()
99            }
100        }
101
102        #[cfg(not(any(unix, windows)))]
103        {
104            Self::default()
105        }
106    }
107
108    /// Set the authentication token
109    pub fn with_auth_token(mut self, token: String) -> Self {
110        self.auth_token = Some(token);
111        self
112    }
113
114    /// Set the Unix socket path
115    pub fn with_unix_socket(mut self, path: String) -> Self {
116        self.unix_socket_path = Some(path);
117        self
118    }
119
120    /// Set the Windows named pipe path
121    pub fn with_windows_pipe(mut self, path: String) -> Self {
122        self.windows_pipe_path = Some(path);
123        self
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_transport_config_defaults() {
133        let config = TransportConfig::default();
134        assert!(config.unix_socket_path.is_none());
135        assert!(config.windows_pipe_path.is_none());
136        assert!(config.auth_token.is_none());
137    }
138
139    #[test]
140    fn test_transport_config_builder() {
141        let config = TransportConfig::default()
142            .with_auth_token("test_token".to_string())
143            .with_unix_socket("/tmp/test.sock".to_string());
144
145        assert_eq!(config.auth_token, Some("test_token".to_string()));
146        assert_eq!(config.unix_socket_path, Some("/tmp/test.sock".to_string()));
147    }
148
149    #[test]
150    fn test_max_message_size() {
151        assert_eq!(MAX_MESSAGE_SIZE, 65536);
152    }
153
154    #[test]
155    fn test_transport_error_display() {
156        let err = TransportError::ConnectionFailed("test".to_string());
157        assert_eq!(err.to_string(), "Connection failed: test");
158
159        let err = TransportError::MessageTooLarge {
160            size: 100000,
161            max: 65536,
162        };
163        assert_eq!(
164            err.to_string(),
165            "Message too large: 100000 bytes (max: 65536 bytes)"
166        );
167    }
168
169    #[test]
170    fn test_transport_error_from_io() {
171        let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "test");
172        let transport_err: TransportError = io_err.into();
173        assert!(matches!(transport_err, TransportError::Io(_)));
174    }
175}