Skip to main content

running_process_protocol/
lib.rs

1//! Generated protobuf types used by the optional running-process broker client.
2//!
3//! This is an implementation-detail package.  Consumers should use the
4//! client-gated compatibility paths re-exported by `running-process` rather
5//! than depending on this crate directly.
6// #1101: environment reads go through declared variables; see the
7// `running_process_env_direct` Dylint lint.
8#![cfg_attr(
9    dylint_lib = "running_process_env_literal",
10    deny(running_process_env_direct)
11)]
12
13/// Explicit pre-existing broker launch and lifetime-control protocol.
14#[allow(missing_docs)]
15pub mod independent_spawn {
16    include!(concat!(
17        env!("OUT_DIR"),
18        "/running_process.independent_spawn.v1.rs"
19    ));
20}
21
22/// Generated daemon control protocol types.
23#[allow(missing_docs)]
24pub mod daemon {
25    include!(concat!(env!("OUT_DIR"), "/running_process.daemon.v1.rs"));
26}
27
28/// Generated broker protocol types, grouped by frozen wire version.
29pub mod broker {
30    /// Generated v1 broker protocol types.
31    #[allow(missing_docs)]
32    pub mod v1 {
33        include!(concat!(env!("OUT_DIR"), "/running_process.broker.v1.rs"));
34    }
35
36    /// Generated v2 broker protocol types.
37    #[allow(missing_docs)]
38    pub mod v2 {
39        include!(concat!(env!("OUT_DIR"), "/running_process.broker.v2.rs"));
40    }
41}
42
43/// Errors from the broker v1 [`broker::v1::Endpoint`] smart constructors.
44#[derive(Debug, thiserror::Error, PartialEq, Eq)]
45pub enum EndpointNameError {
46    /// The endpoint name or path was empty.
47    #[error("endpoint name must not be empty")]
48    Empty,
49    /// A Windows pipe name carried the `\\\\.\\pipe\\` prefix; endpoint
50    /// paths must be bare because running-process adds the prefix while
51    /// resolving the endpoint.
52    #[error(
53        "windows pipe name must be bare (no \\\\.\\pipe\\ prefix), got {got:?}: \\
54         running-process prepends the prefix when resolving the endpoint"
55    )]
56    PrefixedPipeName {
57        /// The rejected, already-prefixed name.
58        got: String,
59    },
60}
61
62/// Converts a caller's environment policy into the two frozen fields carried
63/// by a v2 [`broker::v2::SessionStart`].
64///
65/// The root crate implements this for its public `EnvironmentPolicy`, keeping
66/// [`broker::v2::SessionStart::with_environment_policy`] an inherent method
67/// for downstream callers.  Keeping the conversion boundary here avoids a
68/// dependency from this optional protocol crate back to the core facade.
69pub trait SessionStartEnvironmentPolicy {
70    /// Return `(environment_policy, clear_inherited_env)` for the wire frame.
71    fn session_start_wire_fields(self) -> (i32, bool);
72}
73
74impl broker::v1::Frame {
75    /// Build a v1 request frame with the frozen envelope defaults.
76    pub fn request(payload_protocol: u32, payload: Vec<u8>) -> Self {
77        Self {
78            envelope_version: 1,
79            kind: broker::v1::FrameKind::Request as i32,
80            payload_protocol,
81            payload,
82            request_id: 0,
83            payload_encoding: broker::v1::PayloadEncoding::None as i32,
84            deadline_unix_ms: 0,
85            traceparent: String::new(),
86            tracestate: String::new(),
87        }
88    }
89
90    /// Build the v1 response frame for `request`.
91    pub fn response_to(request: &Self, payload: Vec<u8>) -> Self {
92        Self {
93            envelope_version: 1,
94            kind: broker::v1::FrameKind::Response as i32,
95            payload_protocol: request.payload_protocol,
96            payload,
97            request_id: request.request_id,
98            payload_encoding: broker::v1::PayloadEncoding::None as i32,
99            deadline_unix_ms: 0,
100            traceparent: request.traceparent.clone(),
101            tracestate: request.tracestate.clone(),
102        }
103    }
104
105    /// Set the correlation request id.
106    #[must_use]
107    pub fn with_request_id(mut self, request_id: u64) -> Self {
108        self.request_id = request_id;
109        self
110    }
111}
112
113impl broker::v1::Endpoint {
114    /// Build a Windows named-pipe endpoint from a bare pipe name.
115    pub fn windows_pipe(
116        namespace_id: impl Into<String>,
117        pipe_name: impl Into<String>,
118    ) -> Result<Self, EndpointNameError> {
119        let pipe_name = pipe_name.into();
120        if pipe_name.is_empty() {
121            return Err(EndpointNameError::Empty);
122        }
123        let lowered = pipe_name.to_ascii_lowercase().replace('/', "\\");
124        if lowered.starts_with("\\\\.\\pipe\\") {
125            return Err(EndpointNameError::PrefixedPipeName { got: pipe_name });
126        }
127        Ok(Self {
128            namespace_id: namespace_id.into(),
129            path: pipe_name,
130        })
131    }
132
133    /// Build a Unix-domain-socket endpoint from a filesystem path.
134    pub fn unix_socket(
135        namespace_id: impl Into<String>,
136        socket_path: impl Into<String>,
137    ) -> Result<Self, EndpointNameError> {
138        let socket_path = socket_path.into();
139        if socket_path.is_empty() {
140            return Err(EndpointNameError::Empty);
141        }
142        Ok(Self {
143            namespace_id: namespace_id.into(),
144            path: socket_path,
145        })
146    }
147}
148
149impl broker::v2::SessionStart {
150    /// Build a contained SESSION request from the caller's current process
151    /// context. Only Unicode environment entries are representable by the
152    /// protobuf string vocabulary.
153    pub fn from_current_process(
154        program: impl Into<String>,
155        args: impl IntoIterator<Item = impl Into<String>>,
156        cwd: impl Into<String>,
157    ) -> Self {
158        Self {
159            program: program.into(),
160            args: args.into_iter().map(Into::into).collect(),
161            cwd: cwd.into(),
162            env: std::env::vars()
163                .map(|(key, value)| broker::v2::SessionEnvVar { key, value })
164                .collect(),
165            clear_inherited_env: true,
166            environment_policy: 3,
167        }
168    }
169
170    /// Select the base environment for this contained session.
171    ///
172    /// This stays inherent on the generated protocol type so existing
173    /// downstream calls do not need to import an extension trait after the
174    /// generated definitions moved into this crate.
175    #[must_use]
176    pub fn with_environment_policy(mut self, policy: impl SessionStartEnvironmentPolicy) -> Self {
177        (self.environment_policy, self.clear_inherited_env) = policy.session_start_wire_fields();
178        self
179    }
180}
181
182#[cfg(test)]
183mod compatibility_tests {
184    use super::broker::v1::{Endpoint, Frame, FrameKind, PayloadEncoding};
185    use super::EndpointNameError;
186
187    #[test]
188    fn frame_constructors_keep_the_frozen_v1_defaults() {
189        let mut request = Frame::request(0x7A63, b"ping".to_vec()).with_request_id(42);
190        assert_eq!(request.envelope_version, 1);
191        assert_eq!(request.kind, FrameKind::Request as i32);
192        assert_eq!(request.payload_encoding, PayloadEncoding::None as i32);
193        assert_eq!(request.request_id, 42);
194
195        request.traceparent = "00-abc-def-01".to_owned();
196        request.tracestate = "vendor=1".to_owned();
197        let response = Frame::response_to(&request, b"pong".to_vec());
198        assert_eq!(response.kind, FrameKind::Response as i32);
199        assert_eq!(response.payload_protocol, request.payload_protocol);
200        assert_eq!(response.request_id, request.request_id);
201        assert_eq!(response.traceparent, request.traceparent);
202        assert_eq!(response.tracestate, request.tracestate);
203    }
204
205    #[test]
206    fn endpoint_constructors_keep_the_public_validation_contract() {
207        let pipe = Endpoint::windows_pipe("svc", "svc-pipe").expect("bare pipe name");
208        assert_eq!(pipe.namespace_id, "svc");
209        assert_eq!(pipe.path, "svc-pipe");
210        assert_eq!(
211            Endpoint::windows_pipe("svc", r"\\.\pipe\svc-pipe"),
212            Err(EndpointNameError::PrefixedPipeName {
213                got: r"\\.\pipe\svc-pipe".to_owned(),
214            })
215        );
216        // soldr#1178: `windows_pipe` lowercases and folds `/` to `\\` before
217        // testing the prefix, so a caller who spells the prefix in either
218        // style or in mixed case must still be rejected. The backslash form
219        // above is the only one the surviving assertions covered; these two
220        // branches had no test after #1151.
221        assert_eq!(
222            Endpoint::windows_pipe("svc", "//./pipe/svc-pipe"),
223            Err(EndpointNameError::PrefixedPipeName {
224                got: "//./pipe/svc-pipe".to_owned(),
225            }),
226            "forward-slash spelling of the prefix must be rejected too"
227        );
228        assert_eq!(
229            Endpoint::windows_pipe("svc", r"\\.\PIPE\svc-pipe"),
230            Err(EndpointNameError::PrefixedPipeName {
231                got: r"\\.\PIPE\svc-pipe".to_owned(),
232            }),
233            "the prefix check is case-insensitive"
234        );
235        assert_eq!(
236            Endpoint::windows_pipe("svc", ""),
237            Err(EndpointNameError::Empty)
238        );
239        assert_eq!(
240            Endpoint::unix_socket("svc", ""),
241            Err(EndpointNameError::Empty)
242        );
243    }
244}