1use std::collections::BTreeMap;
3
4pub(crate) struct ResolutionValues<'a> {
7 source: &'a dyn ValueSource,
8 environment: std::sync::Mutex<BTreeMap<String, Option<String>>>,
9 commands: std::sync::Mutex<BTreeMap<String, Option<String>>>,
10 now: i64,
11}
12impl<'a> ResolutionValues<'a> {
13 pub(crate) fn new(source: &'a dyn ValueSource) -> Self {
14 Self {
15 source,
16 environment: Default::default(),
17 commands: Default::default(),
18 now: source.now_unix_seconds(),
19 }
20 }
21}
22impl ValueSource for ResolutionValues<'_> {
23 fn env(&self, name: &str) -> Option<String> {
24 self.environment
25 .lock()
26 .unwrap()
27 .entry(name.into())
28 .or_insert_with(|| self.source.env(name))
29 .clone()
30 }
31 fn command(&self, command: &str, cached: bool) -> Option<String> {
32 if !cached {
33 return self.source.command(command, false);
34 }
35 self.commands
36 .lock()
37 .unwrap()
38 .entry(command.into())
39 .or_insert_with(|| self.source.command(command, true))
40 .clone()
41 }
42 fn now_unix_seconds(&self) -> i64 {
43 self.now
44 }
45}
46
47pub trait ValueSource: Send + Sync {
50 fn env(&self, name: &str) -> Option<String>;
51 fn command(&self, _command: &str, _cached: bool) -> Option<String> {
52 None
53 }
54 fn now_unix_seconds(&self) -> i64 {
55 std::time::SystemTime::now()
56 .duration_since(std::time::UNIX_EPOCH)
57 .map(|duration| duration.as_secs() as i64)
58 .unwrap_or(i64::MAX)
59 }
60}
61impl ValueSource for BTreeMap<String, String> {
62 fn env(&self, name: &str) -> Option<String> {
63 self.get(name).cloned()
64 }
65}
66pub fn resolve_config_value(
67 raw: &str,
68 overlay: Option<&BTreeMap<String, String>>,
69 values: &dyn ValueSource,
70 cached: bool,
71) -> Option<String> {
72 match raw.strip_prefix('!') {
73 Some(command) => values.command(command, cached),
74 None => resolve_template(raw, overlay, values),
75 }
76}
77pub fn resolve_headers(
78 headers: &BTreeMap<String, String>,
79 overlay: Option<&BTreeMap<String, String>>,
80 values: &dyn ValueSource,
81) -> BTreeMap<String, String> {
82 headers
83 .iter()
84 .filter_map(|(name, value)| {
85 resolve_config_value(value, overlay, values, false).map(|value| (name.clone(), value))
86 })
87 .collect()
88}
89enum TemplatePart {
91 Literal(String),
92 Env(String),
93}
94
95fn parse_template(config: &str) -> Vec<TemplatePart> {
100 let mut parts: Vec<TemplatePart> = Vec::new();
101 let bytes = config.as_bytes();
102 let mut i = 0usize;
103 while i < bytes.len() {
104 match config[i..].find('$') {
106 None => {
107 push_literal(&mut parts, &config[i..]);
108 break;
109 }
110 Some(offset) => {
111 let dollar = i + offset;
112 push_literal(&mut parts, &config[i..dollar]);
113 let after = dollar + 1;
114 let next = bytes.get(after).copied();
115 if next == Some(b'$') || next == Some(b'!') {
116 push_literal(&mut parts, &config[after..after + 1]);
117 i = after + 1;
118 continue;
119 }
120 if next == Some(b'{') {
121 if let Some(end_rel) = config[after + 1..].find('}') {
123 let end = after + 1 + end_rel;
124 let name = &config[after + 1..end];
125 if is_env_name(name) {
126 parts.push(TemplatePart::Env(name.to_string()));
127 } else {
128 push_literal(&mut parts, &config[dollar..=end]);
130 }
131 i = end + 1;
132 continue;
133 }
134 push_literal(&mut parts, "$");
136 i = after;
137 continue;
138 }
139 if let Some(name) = env_name_prefix(&config[after..]) {
141 parts.push(TemplatePart::Env(name.to_string()));
142 i = after + name.len();
143 } else {
144 push_literal(&mut parts, "$");
145 i = after;
146 }
147 }
148 }
149 }
150 parts
151}
152
153fn push_literal(parts: &mut Vec<TemplatePart>, value: &str) {
154 if value.is_empty() {
155 return;
156 }
157 if let Some(TemplatePart::Literal(s)) = parts.last_mut() {
158 s.push_str(value);
159 } else {
160 parts.push(TemplatePart::Literal(value.to_string()));
161 }
162}
163
164fn is_env_name(s: &str) -> bool {
165 let mut chars = s.chars();
166 match chars.next() {
167 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
168 _ => return false,
169 }
170 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
171}
172
173fn env_name_prefix(s: &str) -> Option<&str> {
176 let mut chars = s.char_indices();
177 match chars.next() {
178 Some((_, c)) if c.is_ascii_alphabetic() || c == '_' => {}
179 _ => return None,
180 }
181 let end = chars
182 .find(|(_, c)| !(c.is_ascii_alphanumeric() || *c == '_'))
183 .map(|(idx, _)| idx)
184 .unwrap_or(s.len());
185 Some(&s[..end])
186}
187
188pub fn resolve_template(
191 config: &str,
192 env_overlay: Option<&BTreeMap<String, String>>,
193 values: &dyn ValueSource,
194) -> Option<String> {
195 let parts = parse_template(config);
196 let mut out = String::with_capacity(config.len());
197 for part in parts {
198 match part {
199 TemplatePart::Literal(s) => out.push_str(&s),
200 TemplatePart::Env(name) => {
201 let v = env_overlay
202 .and_then(|overlay| overlay.get(&name).cloned())
203 .or_else(|| values.env(&name))?;
204 out.push_str(&v)
205 }
206 }
207 }
208 Some(out)
209}