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