Skip to main content

secretspec_ipc/
launch.rs

1//! Child-process launch configuration for local endpoints and SSH (0.21+).
2//!
3//! The mandatory version 1 transport directly launches a child, so both the async
4//! [`crate::lifecycle`] transport and the synchronous [`crate::blocking`] one
5//! need the same executable, argument, environment, and capture rules. Keeping
6//! them here means a caller can move between transports without rewriting its
7//! launch configuration, and the trust rules below are stated once. Connected
8//! resolver streams (0.21+) have no child and do not use this configuration.
9
10use crate::{Error, Result};
11use std::collections::BTreeMap;
12use std::ffi::OsString;
13use std::path::PathBuf;
14
15#[derive(Clone)]
16pub enum Environment {
17    /// Inherit the caller environment and apply these overrides.
18    Inherit(BTreeMap<OsString, OsString>),
19    /// Clear the environment and install exactly these entries.
20    Replace(BTreeMap<OsString, OsString>),
21}
22
23// Environment entries often carry provider tokens, so only names are shown.
24impl std::fmt::Debug for Environment {
25    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        let (variant, entries) = match self {
27            Self::Inherit(entries) => ("Inherit", entries),
28            Self::Replace(entries) => ("Replace", entries),
29        };
30        formatter
31            .debug_tuple(variant)
32            .field(&entries.keys().collect::<Vec<_>>())
33            .finish()
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct LaunchOptions {
39    pub executable: PathBuf,
40    pub arguments: Vec<OsString>,
41    pub environment: Environment,
42    pub allow_path_discovery: bool,
43    pub max_stderr_bytes: usize,
44}
45
46impl LaunchOptions {
47    pub fn validate(&self) -> Result<()> {
48        if self.executable.as_os_str().is_empty() {
49            return Err(Error::Protocol("executable is empty"));
50        }
51        if !self.allow_path_discovery && !self.executable.is_absolute() {
52            return Err(Error::Protocol(
53                "executable must be absolute unless discovery is enabled",
54            ));
55        }
56        if self.max_stderr_bytes > 1_048_576 {
57            return Err(Error::Protocol(
58                "stderr capture exceeds the version 1 bound",
59            ));
60        }
61        Ok(())
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    fn options(executable: &str, allow_path_discovery: bool) -> LaunchOptions {
70        LaunchOptions {
71            executable: PathBuf::from(executable),
72            arguments: Vec::new(),
73            environment: Environment::Inherit(BTreeMap::new()),
74            allow_path_discovery,
75            max_stderr_bytes: 4096,
76        }
77    }
78
79    #[test]
80    fn relative_executables_require_explicit_discovery() {
81        assert!(options("secretspec", false).validate().is_err());
82        assert!(options("secretspec", true).validate().is_ok());
83        assert!(options("", true).validate().is_err());
84    }
85
86    #[test]
87    fn debug_shows_environment_names_but_not_values() {
88        let mut options = options("secretspec", true);
89        options.environment = Environment::Replace(BTreeMap::from([(
90            OsString::from("VAULT_TOKEN"),
91            OsString::from("hunter2"),
92        )]));
93        let debug = format!("{options:?}");
94        assert!(debug.contains("VAULT_TOKEN"), "{debug}");
95        assert!(!debug.contains("hunter2"), "{debug}");
96    }
97
98    #[test]
99    fn stderr_capture_is_bounded() {
100        let mut options = options("secretspec", true);
101        options.max_stderr_bytes = 1_048_577;
102        assert!(options.validate().is_err());
103    }
104}