1use std::{cell::RefCell, collections::HashMap};
4
5const OPERATOR: char = '$';
6const REFERENCE_OPENER: char = '(';
7const REFERENCE_CLOSER: char = ')';
8
9fn syntax_wrap(input: &str) -> String {
10 format!(
11 "{}{}{}{}",
12 OPERATOR, REFERENCE_OPENER, input, REFERENCE_CLOSER
13 )
14}
15
16pub fn mapping_func_for<'a>(
17 context: &'a [&'a HashMap<String, RefCell<String>>],
18) -> impl Fn(&str) -> String + 'a {
19 move |input: &str| -> String {
20 for vars in context.iter() {
21 if let Some(val) = vars.get(input) {
22 return val.borrow().clone();
23 }
24 }
25 syntax_wrap(input)
26 }
27}
28
29pub fn expand(input: &str, mapping: impl Fn(&str) -> String) -> String {
30 let mut buf = String::with_capacity(16);
31 let mut checkpoint = 0;
32 let mut cursor = 0;
33 let mut chars = input.chars();
34 let mut chars_cursor = 0;
35
36 while cursor < input.len() {
37 if chars.nth(chars_cursor) == Some(OPERATOR) && cursor + 1 < input.len() {
38 buf.push_str(&input[checkpoint..cursor]);
39
40 let (read, is_var, advance) = try_read_variable_name(&input[cursor + 1..]);
41 if is_var {
42 buf.push_str(&mapping(&read));
43 } else {
44 buf.push_str(&read);
45 }
46
47 chars_cursor = advance;
48 cursor += advance;
49 checkpoint = cursor + 1;
50 } else {
51 chars_cursor = 0;
52 }
53 cursor += 1;
54 }
55
56 buf.push_str(&input[checkpoint..]);
57 buf
58}
59
60fn try_read_variable_name(input: &str) -> (String, bool, usize) {
61 let mut chars = input.chars();
62 let next = chars.next().unwrap();
63 match next {
64 OPERATOR => (OPERATOR.into(), false, 1),
65 REFERENCE_OPENER => {
66 for (i, c) in chars.enumerate() {
67 if c == REFERENCE_CLOSER {
68 return (input[1..i + 1].into(), true, i + 2);
69 }
70 }
71 (format!("{}{}", OPERATOR, REFERENCE_OPENER), false, 1)
72 }
73 _ => (format!("{}{}", OPERATOR, next), false, 1),
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use std::cell::RefCell;
80
81 use super::*;
82 use pretty_assertions::assert_eq;
83
84 struct NameValue<'a> {
85 name: &'a str,
86 value: &'a str,
87 }
88
89 #[test]
90 fn test_map_reference() {
91 let envs = vec![
92 NameValue {
93 name: "FOO",
94 value: "bar",
95 },
96 NameValue {
97 name: "ZOO",
98 value: "$(FOO)-1",
99 },
100 NameValue {
101 name: "BLU",
102 value: "$(ZOO)-2",
103 },
104 ];
105
106 let declared_env: HashMap<String, RefCell<String>> = HashMap::from_iter(
107 vec![("FOO", "bar"), ("ZOO", "$(FOO)-1"), ("BLU", "$(ZOO)-2")]
108 .into_iter()
109 .map(|(k, v)| (k.to_string(), v.to_string().into())),
110 );
111
112 let maps = vec![&declared_env];
113 let mapping = mapping_func_for(&maps);
114
115 for entry in envs {
116 let vrc = declared_env.get(entry.name).unwrap();
117 *vrc.borrow_mut() = expand(entry.value.into(), &mapping);
118 }
119
120 let expected_env: HashMap<String, String> = HashMap::from_iter(
121 vec![("FOO", "bar"), ("ZOO", "bar-1"), ("BLU", "bar-1-2")]
122 .into_iter()
123 .map(|(k, v)| (k.to_string(), v.to_string())),
124 );
125
126 for (k, v) in expected_env {
127 let expanded = declared_env.get(&k).unwrap().borrow();
128 assert_eq!(*expanded, v);
129 }
130 }
131
132 #[test]
133 fn test_mapping() {
134 let context: HashMap<String, RefCell<String>> = HashMap::from_iter(
135 vec![
136 ("VAR_A", "A"),
137 ("VAR_B", "B"),
138 ("VAR_C", "C"),
139 ("VAR_REF", "$(VAR_A)"),
140 ("VAR_EMPTY", ""),
141 ]
142 .into_iter()
143 .map(|(k, v)| (k.to_string(), v.to_string().into())),
144 );
145 let maps = vec![&context];
146 let mapping = mapping_func_for(&maps);
147
148 do_expansion_test(mapping)
149 }
150
151 #[test]
152 fn test_mapping_dual() {
153 let context: HashMap<String, RefCell<String>> = HashMap::from_iter(
154 vec![("VAR_A", "A"), ("VAR_EMPTY", "")]
155 .into_iter()
156 .map(|(k, v)| (k.to_string(), v.to_string().into())),
157 );
158 let context2: HashMap<String, RefCell<String>> = HashMap::from_iter(
159 vec![("VAR_B", "B"), ("VAR_C", "C"), ("VAR_REF", "$(VAR_A)")]
160 .into_iter()
161 .map(|(k, v)| (k.to_string(), v.to_string().into())),
162 );
163 let maps = vec![&context, &context2];
164 let mapping = mapping_func_for(&maps);
165
166 do_expansion_test(mapping)
167 }
168
169 fn do_expansion_test(mapping: impl Fn(&str) -> String) {
170 struct Case<'a> {
171 name: &'a str,
172 input: &'a str,
173 expected: &'a str,
174 }
175 let cases = vec![
176 Case {
177 name: "whole string",
178 input: "$(VAR_A)",
179 expected: "A",
180 },
181 Case {
182 name: "repeat",
183 input: "$(VAR_A)-$(VAR_A)",
184 expected: "A-A",
185 },
186 Case {
187 name: "beginning",
188 input: "$(VAR_A)-1",
189 expected: "A-1",
190 },
191 Case {
192 name: "middle",
193 input: "___$(VAR_B)___",
194 expected: "___B___",
195 },
196 Case {
197 name: "end",
198 input: "___$(VAR_C)",
199 expected: "___C",
200 },
201 Case {
202 name: "compound",
203 input: "$(VAR_A)_$(VAR_B)_$(VAR_C)",
204 expected: "A_B_C",
205 },
206 Case {
207 name: "escape & expand",
208 input: "$$(VAR_B)_$(VAR_A)",
209 expected: "$(VAR_B)_A",
210 },
211 Case {
212 name: "compound escape",
213 input: "$$(VAR_A)_$$(VAR_B)",
214 expected: "$(VAR_A)_$(VAR_B)",
215 },
216 Case {
217 name: "mixed in escapes",
218 input: "f000-$$VAR_A",
219 expected: "f000-$VAR_A",
220 },
221 Case {
222 name: "backslash escape ignored",
223 input: "foo\\$(VAR_C)bar",
224 expected: "foo\\Cbar",
225 },
226 Case {
227 name: "backslash escape ignored",
228 input: "foo\\\\$(VAR_C)bar",
229 expected: "foo\\\\Cbar",
230 },
231 Case {
232 name: "lots of backslashes",
233 input: "foo\\\\\\\\$(VAR_A)bar",
234 expected: "foo\\\\\\\\Abar",
235 },
236 Case {
237 name: "nested var references",
238 input: "$(VAR_A$(VAR_B))",
239 expected: "$(VAR_A$(VAR_B))",
240 },
241 Case {
242 name: "nested var references second type",
243 input: "$(VAR_A$(VAR_B)",
244 expected: "$(VAR_A$(VAR_B)",
245 },
246 Case {
247 name: "value is a reference",
248 input: "$(VAR_REF)",
249 expected: "$(VAR_A)",
250 },
251 Case {
252 name: "value is a reference x 2",
253 input: "%%$(VAR_REF)--$(VAR_REF)%%",
254 expected: "%%$(VAR_A)--$(VAR_A)%%",
255 },
256 Case {
257 name: "empty var",
258 input: "foo$(VAR_EMPTY)bar",
259 expected: "foobar",
260 },
261 Case {
262 name: "unterminated expression",
263 input: "foo$(VAR_Awhoops!",
264 expected: "foo$(VAR_Awhoops!",
265 },
266 Case {
267 name: "expression without operator",
268 input: "f00__(VAR_A)__",
269 expected: "f00__(VAR_A)__",
270 },
271 Case {
272 name: "shell special vars pass through",
273 input: "$?_boo_$!",
274 expected: "$?_boo_$!",
275 },
276 Case {
277 name: "bare operators are ignored",
278 input: "$VAR_A",
279 expected: "$VAR_A",
280 },
281 Case {
282 name: "undefined vars are passed through",
283 input: "$(VAR_DNE)",
284 expected: "$(VAR_DNE)",
285 },
286 Case {
287 name: "multiple (even) operators, var undefined",
288 input: "$$$$$$(BIG_MONEY)",
289 expected: "$$$(BIG_MONEY)",
290 },
291 Case {
292 name: "multiple (even) operators, var defined",
293 input: "$$$$$$(VAR_A)",
294 expected: "$$$(VAR_A)",
295 },
296 Case {
297 name: "multiple (odd) operators, var undefined",
298 input: "$$$$$$$(GOOD_ODDS)",
299 expected: "$$$$(GOOD_ODDS)",
300 },
301 Case {
302 name: "multiple (odd) operators, var defined",
303 input: "$$$$$$$(VAR_A)",
304 expected: "$$$A",
305 },
306 Case {
307 name: "missing open expression",
308 input: "$VAR_A)",
309 expected: "$VAR_A)",
310 },
311 Case {
312 name: "shell syntax ignored",
313 input: "${VAR_A}",
314 expected: "${VAR_A}",
315 },
316 Case {
317 name: "trailing incomplete expression not consumed",
318 input: "$(VAR_B)_______$(A",
319 expected: "B_______$(A",
320 },
321 Case {
322 name: "trailing incomplete expression, no content, is not consumed",
323 input: "$(VAR_C)_______$(",
324 expected: "C_______$(",
325 },
326 Case {
327 name: "operator at end of input string is preserved",
328 input: "$(VAR_A)foobarzab$",
329 expected: "Afoobarzab$",
330 },
331 Case {
332 name: "shell escaped incomplete expr",
333 input: "foo-\\$(VAR_A",
334 expected: "foo-\\$(VAR_A",
335 },
336 Case {
337 name: "lots of $( in middle",
338 input: "--$($($($($--",
339 expected: "--$($($($($--",
340 },
341 Case {
342 name: "lots of $( in beginning",
343 input: "$($($($($--foo$(",
344 expected: "$($($($($--foo$(",
345 },
346 Case {
347 name: "lots of $( at end",
348 input: "foo0--$($($($(",
349 expected: "foo0--$($($($(",
350 },
351 Case {
352 name: "escaped operators in variable names are not escaped",
353 input: "$(foo$$var)",
354 expected: "$(foo$$var)",
355 },
356 Case {
357 name: "newline not expanded",
358 input: "\n",
359 expected: "\n",
360 },
361 ];
362
363 for case in cases {
364 let expanded = expand(case.input.into(), &mapping);
365 assert_eq!(expanded, case.expected, "{}", case.name);
366 }
367 }
368}