cc_toolgate/eval/
context.rs1use crate::parse::Redirection;
4
5#[derive(Debug)]
7pub struct CommandContext<'a> {
8 pub raw: &'a str,
10 pub base_command: String,
12 pub words: Vec<String>,
14 pub env_vars: Vec<(String, String)>,
16 pub redirection: Option<Redirection>,
18 pub accumulated_env: std::collections::HashMap<String, String>,
21}
22
23impl<'a> CommandContext<'a> {
24 pub fn from_command(raw: &'a str) -> Self {
26 let base_command = crate::parse::base_command(raw);
27 let env_vars = crate::parse::env_vars(raw);
28 let words = crate::parse::tokenize(raw);
29 let redirection = crate::parse::has_output_redirection(raw);
30
31 Self {
32 raw,
33 base_command,
34 words,
35 env_vars,
36 redirection,
37 accumulated_env: std::collections::HashMap::new(),
38 }
39 }
40
41 pub fn env_satisfies(&self, required: &std::collections::HashMap<String, String>) -> bool {
51 required.iter().all(|(key, value)| {
52 let expanded = match shellexpand::full(value) {
53 Ok(v) => v,
54 Err(e) => {
55 log::warn!("shellexpand failed for config_env {key}={value}: {e}");
56 std::borrow::Cow::Borrowed(value.as_str())
57 }
58 };
59 if let Some((_, v)) = self.env_vars.iter().find(|(k, _)| k == key) {
61 return v == value || v == expanded.as_ref();
62 }
63 if let Some(v) = self.accumulated_env.get(key) {
65 return v == value || v == expanded.as_ref();
66 }
67 std::env::var(key).is_ok_and(|v| v == *value || v == expanded.as_ref())
69 })
70 }
71
72 pub fn args(&self) -> &[String] {
74 let skip = self.env_vars.len() + 1; if self.words.len() > skip {
77 &self.words[skip..]
78 } else {
79 &[]
80 }
81 }
82
83 pub fn has_flag(&self, flag: &str) -> bool {
85 self.words.iter().any(|w| w == flag)
86 }
87
88 pub fn has_any_flag(&self, flags: &[&str]) -> bool {
90 self.words.iter().any(|w| flags.contains(&w.as_str()))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::collections::HashMap;
98
99 fn require_nextest() {
105 assert!(
106 std::env::var("NEXTEST").is_ok(),
107 "this test mutates process env and requires nextest (cargo nextest run)"
108 );
109 }
110
111 #[test]
112 fn env_satisfies_inline_exact() {
113 let ctx = CommandContext::from_command("FOO=bar git push");
114 let req = HashMap::from([("FOO".into(), "bar".into())]);
115 assert!(ctx.env_satisfies(&req));
116 }
117
118 #[test]
119 fn env_satisfies_inline_wrong_value() {
120 let ctx = CommandContext::from_command("FOO=baz git push");
121 let req = HashMap::from([("FOO".into(), "bar".into())]);
122 assert!(!ctx.env_satisfies(&req));
123 }
124
125 #[test]
126 fn env_satisfies_inline_missing() {
127 let ctx = CommandContext::from_command("git push");
128 let req = HashMap::from([("FOO".into(), "bar".into())]);
129 assert!(!ctx.env_satisfies(&req));
131 }
132
133 #[test]
134 fn env_satisfies_process_env() {
135 require_nextest();
136 let key = "CC_TOOLGATE_TEST_PROCESS_ENV";
137 unsafe { std::env::set_var(key, "expected_value") };
139 let ctx = CommandContext::from_command("git push");
140 let req = HashMap::from([(key.into(), "expected_value".into())]);
141 assert!(ctx.env_satisfies(&req));
142 unsafe { std::env::remove_var(key) };
143 }
144
145 #[test]
146 fn env_satisfies_process_env_wrong_value() {
147 require_nextest();
148 let key = "CC_TOOLGATE_TEST_WRONG_VALUE";
149 unsafe { std::env::set_var(key, "actual") };
151 let ctx = CommandContext::from_command("git push");
152 let req = HashMap::from([(key.into(), "expected".into())]);
153 assert!(!ctx.env_satisfies(&req));
154 unsafe { std::env::remove_var(key) };
155 }
156
157 #[test]
158 fn env_satisfies_multi_source_one_inline_one_process() {
159 require_nextest();
160 let key_process = "CC_TOOLGATE_TEST_MULTI_PROC";
161 unsafe { std::env::set_var(key_process, "/correct/path") };
163 let ctx = CommandContext::from_command("INLINE_VAR=correct git push");
164 let req = HashMap::from([
165 ("INLINE_VAR".into(), "correct".into()),
166 (key_process.into(), "/correct/path".into()),
167 ]);
168 assert!(ctx.env_satisfies(&req));
169 unsafe { std::env::remove_var(key_process) };
170 }
171
172 #[test]
173 fn env_satisfies_multi_source_one_missing() {
174 let ctx = CommandContext::from_command("INLINE_VAR=correct git push");
176 let req = HashMap::from([
177 ("INLINE_VAR".into(), "correct".into()),
178 ("MISSING_VAR".into(), "value".into()),
179 ]);
180 assert!(!ctx.env_satisfies(&req));
181 }
182
183 #[test]
184 fn env_satisfies_multi_source_one_wrong() {
185 require_nextest();
186 let key_process = "CC_TOOLGATE_TEST_MULTI_WRONG";
187 unsafe { std::env::set_var(key_process, "/wrong/path") };
189 let ctx = CommandContext::from_command("INLINE_VAR=correct git push");
190 let req = HashMap::from([
191 ("INLINE_VAR".into(), "correct".into()),
192 (key_process.into(), "/correct/path".into()),
193 ]);
194 assert!(!ctx.env_satisfies(&req));
195 unsafe { std::env::remove_var(key_process) };
196 }
197
198 #[test]
199 fn env_satisfies_tilde_expansion() {
200 require_nextest();
201 let key = "CC_TOOLGATE_TEST_TILDE";
202 let home = std::env::var("HOME").unwrap();
203 unsafe { std::env::set_var(key, format!("{home}/foo")) };
205 let ctx = CommandContext::from_command("git push");
206 let req = HashMap::from([(key.into(), "~/foo".into())]);
207 assert!(ctx.env_satisfies(&req));
208 unsafe { std::env::remove_var(key) };
209 }
210
211 #[test]
212 fn env_satisfies_empty_map() {
213 let ctx = CommandContext::from_command("git push");
214 assert!(ctx.env_satisfies(&HashMap::new()));
215 }
216
217 const COLLISION_KEY: &str = "CC_TOOLGATE_TEST_COLLISION";
225
226 #[test]
227 fn env_collision_value_alpha() {
228 require_nextest();
229 unsafe { std::env::set_var(COLLISION_KEY, "alpha") };
231 std::thread::sleep(std::time::Duration::from_millis(5));
233 let ctx = CommandContext::from_command("git push");
234 let req = HashMap::from([(COLLISION_KEY.into(), "alpha".into())]);
235 assert!(
236 ctx.env_satisfies(&req),
237 "expected 'alpha', env was tampered"
238 );
239 unsafe { std::env::remove_var(COLLISION_KEY) };
240 }
241
242 #[test]
243 fn env_collision_value_beta() {
244 require_nextest();
245 unsafe { std::env::set_var(COLLISION_KEY, "beta") };
247 std::thread::sleep(std::time::Duration::from_millis(5));
248 let ctx = CommandContext::from_command("git push");
249 let req = HashMap::from([(COLLISION_KEY.into(), "beta".into())]);
250 assert!(ctx.env_satisfies(&req), "expected 'beta', env was tampered");
251 unsafe { std::env::remove_var(COLLISION_KEY) };
252 }
253}