Skip to main content

kode_bridge/transport/
mod.rs

1use crate::errors::{KodeBridgeError, Result};
2use std::io::IoSlice;
3use std::path::{Path, PathBuf};
4use std::pin::Pin;
5use std::task::{Context, Poll};
6use std::time::Duration;
7use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
8
9#[cfg(unix)]
10mod unix;
11#[cfg(windows)]
12mod windows;
13
14#[cfg(unix)]
15use unix as platform;
16#[cfg(windows)]
17use windows as platform;
18
19#[cfg(windows)]
20pub(crate) type WindowsServerPidVerifier = fn(u32) -> std::io::Result<()>;
21
22#[cfg(windows)]
23#[derive(Clone, Copy, Debug, Default)]
24pub(crate) struct WindowsServerVerification {
25    pub(crate) require_system: bool,
26    pub(crate) verifier: Option<WindowsServerPidVerifier>,
27}
28
29#[cfg(windows)]
30impl WindowsServerVerification {
31    pub(crate) const fn new(require_system: bool, verifier: Option<WindowsServerPidVerifier>) -> Self {
32        Self {
33            require_system,
34            verifier,
35        }
36    }
37}
38
39/// A validated cross-platform IPC endpoint.
40#[derive(Clone, Debug, Eq, Hash, PartialEq)]
41pub struct Endpoint(PathBuf);
42
43impl Endpoint {
44    /// Validate and own an IPC endpoint path.
45    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
46        platform::validate_endpoint(path.as_ref())
47            .map_err(|error| KodeBridgeError::configuration(format!("Invalid IPC endpoint: {error}")))?;
48        Ok(Self(path.as_ref().to_path_buf()))
49    }
50
51    /// Return the platform endpoint path.
52    pub fn as_path(&self) -> &Path {
53        &self.0
54    }
55}
56
57impl AsRef<Path> for Endpoint {
58    fn as_ref(&self) -> &Path {
59        self.as_path()
60    }
61}
62
63/// Cross-platform IPC listener configuration.
64#[derive(Clone, Debug)]
65pub struct ListenerOptions {
66    reclaim_name: bool,
67    try_overwrite: bool,
68    #[cfg(unix)]
69    max_spin_time: Option<Duration>,
70    #[cfg(unix)]
71    mode: Option<libc::mode_t>,
72    #[cfg(windows)]
73    security_descriptor: Option<std::sync::Arc<windows::SecurityDescriptor>>,
74}
75
76impl ListenerOptions {
77    /// Return the default listener configuration.
78    pub const fn new() -> Self {
79        Self {
80            reclaim_name: true,
81            try_overwrite: false,
82            #[cfg(unix)]
83            max_spin_time: None,
84            #[cfg(unix)]
85            mode: None,
86            #[cfg(windows)]
87            security_descriptor: None,
88        }
89    }
90
91    /// Configure whether the endpoint name is removed when the listener drops.
92    #[must_use]
93    pub const fn reclaim_name(mut self, reclaim_name: bool) -> Self {
94        self.reclaim_name = reclaim_name;
95        self
96    }
97
98    /// Configure whether a proven-stale Unix socket may be replaced while binding.
99    #[must_use]
100    pub const fn try_overwrite(mut self, try_overwrite: bool) -> Self {
101        self.try_overwrite = try_overwrite;
102        self
103    }
104
105    /// Bound retry time for Unix stale-socket replacement contention.
106    #[must_use]
107    #[cfg_attr(not(unix), allow(unused_mut))]
108    pub const fn max_spin_time(mut self, max_spin_time: Duration) -> Self {
109        #[cfg(unix)]
110        {
111            self.max_spin_time = Some(max_spin_time);
112        }
113        let _ = max_spin_time;
114        self
115    }
116
117    /// Set Unix socket permissions before the listener begins accepting clients.
118    #[cfg(unix)]
119    #[must_use]
120    pub const fn mode(mut self, mode: libc::mode_t) -> Self {
121        self.mode = Some(mode);
122        self
123    }
124
125    /// Set a Windows named-pipe security descriptor from SDDL.
126    ///
127    /// # Panics
128    /// Panics with `Invalid SDDL string` for an interior NUL, or with
129    /// `Failed to parse SDDL` when Windows rejects the descriptor.
130    #[cfg(windows)]
131    #[must_use]
132    #[allow(clippy::panic)]
133    pub fn security_descriptor(mut self, sddl: &str) -> Self {
134        if sddl.encode_utf16().any(|unit| unit == 0) {
135            panic!("Invalid SDDL string");
136        }
137        let descriptor = match windows::SecurityDescriptor::from_sddl(sddl) {
138            Ok(descriptor) => descriptor,
139            Err(error) => panic!("Failed to parse SDDL: {error}"),
140        };
141        self.security_descriptor = Some(std::sync::Arc::new(descriptor));
142        self
143    }
144}
145
146#[cfg(all(test, windows))]
147mod windows_tests {
148    use super::ListenerOptions;
149
150    #[test]
151    #[should_panic(expected = "Invalid SDDL string")]
152    fn interior_nul_keeps_legacy_panic_classification() {
153        let _options = ListenerOptions::new().security_descriptor("D:\0(A;;GA;;;WD)");
154    }
155
156    #[test]
157    #[should_panic(expected = "Failed to parse SDDL")]
158    fn malformed_sddl_keeps_legacy_panic_classification() {
159        let _options = ListenerOptions::new().security_descriptor("not-valid-sddl");
160    }
161}
162
163impl Default for ListenerOptions {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169/// Client-side stream returned by kode-bridge IPC connections.
170#[derive(Debug)]
171pub struct IpcStream(platform::ClientStream);
172
173impl IpcStream {
174    pub(crate) async fn connect(endpoint: &Endpoint) -> std::io::Result<Self> {
175        platform::connect(endpoint.as_path()).await.map(Self)
176    }
177
178    #[cfg(windows)]
179    pub(crate) async fn connect_with_windows_server_verification(
180        endpoint: &Endpoint,
181        verification: WindowsServerVerification,
182    ) -> std::io::Result<Self> {
183        platform::connect_with_server_verification(endpoint.as_path(), verification)
184            .await
185            .map(Self)
186    }
187}
188
189impl AsyncRead for IpcStream {
190    fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buffer: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
191        Pin::new(&mut self.get_mut().0).poll_read(cx, buffer)
192    }
193}
194
195impl AsyncWrite for IpcStream {
196    fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buffer: &[u8]) -> Poll<std::io::Result<usize>> {
197        Pin::new(&mut self.get_mut().0).poll_write(cx, buffer)
198    }
199
200    fn poll_write_vectored(
201        self: Pin<&mut Self>,
202        cx: &mut Context<'_>,
203        buffers: &[IoSlice<'_>],
204    ) -> Poll<std::io::Result<usize>> {
205        Pin::new(&mut self.get_mut().0).poll_write_vectored(cx, buffers)
206    }
207
208    fn is_write_vectored(&self) -> bool {
209        self.0.is_write_vectored()
210    }
211
212    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
213        Pin::new(&mut self.get_mut().0).poll_flush(cx)
214    }
215
216    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
217        Pin::new(&mut self.get_mut().0).poll_shutdown(cx)
218    }
219}
220
221#[cfg(feature = "server")]
222pub(crate) type ServerStream = platform::ServerStream;
223
224#[cfg(feature = "server")]
225pub(crate) struct Listener(platform::Listener);
226
227#[cfg(feature = "server")]
228impl Listener {
229    pub(crate) fn bind(endpoint: &Endpoint, options: &ListenerOptions) -> std::io::Result<Self> {
230        platform::Listener::bind(endpoint.as_path(), options).map(Self)
231    }
232
233    pub(crate) async fn accept(&mut self) -> std::io::Result<ServerStream> {
234        self.0.accept().await
235    }
236}
237
238#[cfg(all(feature = "server", unix))]
239pub(crate) fn peer_credentials(stream: &ServerStream) -> std::io::Result<(u32, u32)> {
240    platform::peer_credentials(stream)
241}