1use crate::{Error, Result};
11use std::collections::BTreeMap;
12use std::ffi::OsString;
13use std::path::PathBuf;
14
15#[derive(Clone)]
16pub enum Environment {
17 Inherit(BTreeMap<OsString, OsString>),
19 Replace(BTreeMap<OsString, OsString>),
21}
22
23impl 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}