1use std::fs;
7use std::path::Path;
8
9use crate::env_diff;
10
11pub fn save_state(path: &Path, env_data: &str) -> std::io::Result<()> {
21 let mut output = String::with_capacity(env_data.len());
22
23 for entry in env_data.split('\0') {
24 let entry = entry.trim_start_matches('\n');
25 if entry.is_empty() {
26 continue;
27 }
28 if let Some(eq_pos) = entry.find('=') {
29 let key = &entry[..eq_pos];
30 let value = &entry[eq_pos + 1..];
31
32 if key.is_empty()
33 || !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
34 {
35 continue;
36 }
37 if env_diff::should_skip_var(key) {
38 continue;
39 }
40
41 output.push_str("export ");
42 output.push_str(key);
43 output.push_str("='");
44 for &b in value.as_bytes() {
45 if b == b'\'' {
46 output.push_str("'\\''");
47 } else {
48 output.push(b as char);
49 }
50 }
51 output.push_str("'\n");
52 }
53 }
54
55 fs::write(path, output)
56}
57
58#[must_use]
60pub fn state_prefix(path: &Path) -> String {
61 let p = path.display();
62 format!("[ -f '{p}' ] && source '{p}'\n")
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use std::fs;
69
70 #[test]
71 fn save_and_read_state() {
72 let dir = std::env::temp_dir();
73 let path = dir.join("reef-test-state");
74
75 let env_data = "FOO=bar\0MY_VAR=hello world\0";
76 save_state(&path, env_data).unwrap();
77
78 let content = fs::read_to_string(&path).unwrap();
79 assert!(content.contains("export FOO='bar'"));
80 assert!(content.contains("export MY_VAR='hello world'"));
81
82 fs::remove_file(&path).ok();
83 }
84
85 #[test]
86 fn save_state_escapes_quotes() {
87 let dir = std::env::temp_dir();
88 let path = dir.join("reef-test-state-quotes");
89
90 let env_data = "QUOTED=it's a test\0";
91 save_state(&path, env_data).unwrap();
92
93 let content = fs::read_to_string(&path).unwrap();
94 assert!(content.contains("export QUOTED='it'\\''s a test'"));
95
96 fs::remove_file(&path).ok();
97 }
98
99 #[test]
100 fn save_state_skips_bash_internals() {
101 let dir = std::env::temp_dir();
102 let path = dir.join("reef-test-state-skip");
103
104 let env_data = "BASH_VERSION=5.2\0REAL_VAR=keep\0SHLVL=1\0";
105 save_state(&path, env_data).unwrap();
106
107 let content = fs::read_to_string(&path).unwrap();
108 assert!(!content.contains("BASH_VERSION"));
109 assert!(!content.contains("SHLVL"));
110 assert!(content.contains("export REAL_VAR='keep'"));
111
112 fs::remove_file(&path).ok();
113 }
114
115 #[test]
116 fn state_prefix_format() {
117 let path = Path::new("/tmp/reef-state-12345");
118 let prefix = state_prefix(path);
119 assert_eq!(
120 prefix,
121 "[ -f '/tmp/reef-state-12345' ] && source '/tmp/reef-state-12345'\n"
122 );
123 }
124}