1use std::io::{self, Write};
7use std::path::Path;
8use std::process::{Command, Stdio};
9
10use crate::env_diff::{self, EnvSnapshot};
11use crate::state;
12
13const ENV_MARKER: &str = "\0__REEF_ENV__\0";
16const CWD_MARKER: &str = "\0__REEF_CWD__\0";
17
18#[must_use]
43pub fn bash_exec(command: &str) -> i32 {
44 let before = EnvSnapshot::capture_current();
45
46 let script = build_script(&shell_escape_for_bash(command), " >&2", true);
49
50 let output = match Command::new("bash")
51 .args(["-c", &script])
52 .stdin(Stdio::inherit())
53 .stdout(Stdio::piped())
54 .stderr(Stdio::inherit())
55 .output()
56 {
57 Ok(o) => o,
58 Err(e) => {
59 eprintln!("reef: failed to run bash: {e}");
60 return 1;
61 }
62 };
63
64 #[cfg(unix)]
67 let exit_code = {
68 use std::os::unix::process::ExitStatusExt;
69 output.status.code().unwrap_or_else(|| {
70 output.status.signal().map_or(1, |sig| 128 + sig)
71 })
72 };
73 #[cfg(not(unix))]
74 let exit_code = output.status.code().unwrap_or(1);
75 diff_and_print_env(&before, &output.stdout);
76 exit_code
77}
78
79#[must_use]
96pub fn bash_exec_env_diff(command: &str) -> i32 {
97 let before = EnvSnapshot::capture_current();
98
99 let script = build_script(&shell_escape_for_bash(command), " >/dev/null 2>&1", false);
102
103 let output = match Command::new("bash").args(["-c", &script]).output() {
104 Ok(o) => o,
105 Err(e) => {
106 eprintln!("reef: failed to run bash: {e}");
107 return 1;
108 }
109 };
110
111 diff_and_print_env(&before, &output.stdout);
112
113 if output.status.success() {
114 0
115 } else {
116 #[cfg(unix)]
117 {
118 use std::os::unix::process::ExitStatusExt;
119 output.status.code().unwrap_or_else(|| {
120 output.status.signal().map_or(1, |sig| 128 + sig)
121 })
122 }
123 #[cfg(not(unix))]
124 {
125 output.status.code().unwrap_or(1)
126 }
127 }
128}
129
130#[must_use]
149pub fn bash_exec_with_state(command: &str, state_path: &Path) -> i32 {
150 let before = EnvSnapshot::capture_current();
151
152 let prefix = state::state_prefix(state_path);
153 let escaped = shell_escape_for_bash(command);
154 let body = build_script(&escaped, " >&2", true);
155
156 let mut script = String::with_capacity(prefix.len() + body.len());
157 script.push_str(&prefix);
158 script.push_str(&body);
159
160 let output = match Command::new("bash")
161 .args(["-c", &script])
162 .stdin(Stdio::inherit())
163 .stdout(Stdio::piped())
164 .stderr(Stdio::inherit())
165 .output()
166 {
167 Ok(o) => o,
168 Err(e) => {
169 eprintln!("reef: failed to run bash: {e}");
170 return 1;
171 }
172 };
173
174 #[cfg(unix)]
175 let exit_code = {
176 use std::os::unix::process::ExitStatusExt;
177 output.status.code().unwrap_or_else(|| {
178 output.status.signal().map_or(1, |sig| 128 + sig)
179 })
180 };
181 #[cfg(not(unix))]
182 let exit_code = output.status.code().unwrap_or(1);
183 diff_and_print_env_save_state(&before, &output.stdout, state_path);
184 exit_code
185}
186
187fn extract_env_sections(raw_stdout: &[u8]) -> Option<(String, String)> {
189 let stdout = String::from_utf8_lossy(raw_stdout);
190 let env_pos = stdout.find(ENV_MARKER)?;
191 let cwd_pos = stdout.find(CWD_MARKER)?;
192 let env_section = stdout[env_pos + ENV_MARKER.len()..cwd_pos].to_string();
193 let cwd_section = stdout[cwd_pos + CWD_MARKER.len()..].trim().to_string();
194 Some((env_section, cwd_section))
195}
196
197fn diff_and_print_env(before: &EnvSnapshot, raw_stdout: &[u8]) {
200 if let Some((env_section, cwd_section)) = extract_env_sections(raw_stdout) {
201 let after = EnvSnapshot::new(
202 env_diff::parse_null_separated_env(&env_section),
203 cwd_section,
204 );
205 let mut buf = String::new();
206 before.diff_into(&after, &mut buf);
207 if !buf.is_empty() {
208 let _ = io::stdout().lock().write_all(buf.as_bytes());
209 }
210 }
211}
212
213fn diff_and_print_env_save_state(before: &EnvSnapshot, raw_stdout: &[u8], state_path: &Path) {
216 if let Some((env_section, cwd_section)) = extract_env_sections(raw_stdout) {
217 let _ = state::save_state(state_path, &env_section);
218 let after = EnvSnapshot::new(
219 env_diff::parse_null_separated_env(&env_section),
220 cwd_section,
221 );
222 let mut buf = String::new();
223 before.diff_into(&after, &mut buf);
224 if !buf.is_empty() {
225 let _ = io::stdout().lock().write_all(buf.as_bytes());
226 }
227 }
228}
229
230fn build_script(escaped_cmd: &str, redirect: &str, track_exit: bool) -> String {
233 let mut s = String::with_capacity(escaped_cmd.len() + 100);
234 s.push_str("eval ");
235 s.push_str(escaped_cmd);
236 s.push_str(redirect);
237 s.push('\n');
238 if track_exit {
239 s.push_str("__reef_exit=$?\n");
240 }
241 s.push_str("printf '\\0__REEF_ENV__\\0'\nenv -0\nprintf '\\0__REEF_CWD__\\0'\npwd");
243 if track_exit {
244 s.push_str("\nexit $__reef_exit");
245 }
246 s
247}
248
249fn shell_escape_for_bash(s: &str) -> String {
252 let mut result = String::with_capacity(s.len() + 2);
253 result.push('\'');
254 for &b in s.as_bytes() {
255 if b == b'\'' {
256 result.push_str("'\\''");
257 } else {
258 result.push(b as char);
259 }
260 }
261 result.push('\'');
262 result
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn shell_escape_simple() {
271 assert_eq!(shell_escape_for_bash("echo hello"), "'echo hello'");
272 }
273
274 #[test]
275 fn shell_escape_with_quotes() {
276 assert_eq!(
277 shell_escape_for_bash("echo 'it'\"s\""),
278 "'echo '\\''it'\\''\"s\"'"
279 );
280 }
281
282 #[test]
283 fn bash_exec_sets_var() {
284 let code = bash_exec("export __REEF_TEST_VAR_xyzzy=hello_reef");
286 assert_eq!(code, 0);
288 }
289
290 #[test]
291 fn bash_exec_env_diff_captures_var() {
292 let code = bash_exec_env_diff("export __REEF_TEST_ED_VAR=test_val");
294 assert_eq!(code, 0);
295 }
296
297 #[test]
298 fn bash_exec_preserves_exit_code() {
299 let code = bash_exec("exit 42");
300 assert_eq!(code, 42);
301 }
302
303 #[test]
304 fn bash_exec_exit_code_zero() {
305 let code = bash_exec("true");
306 assert_eq!(code, 0);
307 }
308
309 #[test]
310 fn sentinel_uses_null_bytes() {
311 assert!(ENV_MARKER.contains('\0'));
313 assert!(CWD_MARKER.contains('\0'));
314 }
315}