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