1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
4
5use crate::Element;
6
7#[derive(Clone, Debug, Default, PartialEq)]
12pub struct Macros(Arc<Layer>);
13
14#[derive(Debug, Default, PartialEq)]
15struct Layer {
16 parent: Option<Macros>,
17 base: Option<Macros>,
19 own: Vec<(String, String)>,
20 index: HashMap<String, usize>,
22 depth: u32,
24}
25
26const MAX_LAYER_BYTES: usize = 1 << 20;
28
29pub const MAX_LOAD_BYTES: usize = 4 << 20;
34
35const MAX_LAYER_MACROS: usize = 512;
37const MAX_LAYERS: u32 = 32;
38
39impl Macros {
40 pub fn from_element(el: &Element) -> Self {
41 Self::from_pairs(el.children.iter().map(|c| (c.name.clone(), c.text.clone())))
42 }
43
44 pub fn from_pairs<K: Into<String>, V: Into<String>>(
45 pairs: impl IntoIterator<Item = (K, V)>,
46 ) -> Self {
47 let own: Vec<(String, String)> = pairs
48 .into_iter()
49 .map(|(k, v)| (k.into(), v.into()))
50 .collect();
51 Self::layer(None, None, own)
52 }
53
54 fn layer(parent: Option<Macros>, base: Option<Macros>, own: Vec<(String, String)>) -> Self {
55 let index = own
56 .iter()
57 .enumerate()
58 .map(|(i, (k, _))| (k.clone(), i))
59 .collect();
60 let depth = parent.as_ref().map_or(0, |p| p.0.depth + 1);
61 Self(Arc::new(Layer {
62 parent,
63 base,
64 own,
65 index,
66 depth,
67 }))
68 }
69
70 pub fn get(&self, name: &str) -> Option<&str> {
71 let own = self.0.index.get(name).map(|&i| self.0.own[i].1.as_str());
72 own.or_else(|| self.0.parent.as_ref()?.get(name))
73 .or_else(|| self.0.base.as_ref()?.get(name))
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.0.own.is_empty()
78 && self.0.parent.as_ref().is_none_or(|p| p.is_empty())
79 && self.0.base.as_ref().is_none_or(|b| b.is_empty())
80 }
81
82 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
86 let mut out = Vec::new();
87 self.collect_into(&mut out);
88 out.into_iter()
89 }
90
91 fn collect_into<'a>(&'a self, out: &mut Vec<(&'a str, &'a str)>) {
92 if let Some(b) = &self.0.base {
93 b.collect_into(out);
94 }
95 if let Some(p) = &self.0.parent {
96 p.collect_into(out);
97 }
98 out.extend(self.0.own.iter().map(|(k, v)| (k.as_str(), v.as_str())));
99 }
100
101 pub fn with(&self, inner: &Macros) -> Macros {
106 self.with_budget(inner, &AtomicUsize::new(MAX_LAYER_BYTES))
107 }
108
109 pub fn with_budget(&self, inner: &Macros, left: &AtomicUsize) -> Macros {
117 if inner.0.own.is_empty() {
118 return self.clone();
119 }
120 if self.0.depth >= MAX_LAYERS {
121 log::warn!("macros nested over {MAX_LAYERS} deep are ignored");
122 return self.clone();
123 }
124 if inner.0.own.len() > MAX_LAYER_MACROS {
125 log::warn!("over {MAX_LAYER_MACROS} macros in a widget; the rest are ignored");
126 }
127 let own = inner
128 .0
129 .own
130 .iter()
131 .take(MAX_LAYER_MACROS)
132 .map(|(k, v)| {
133 let v = if left.load(Ordering::Relaxed) > 0 {
134 let out = expand(v, &|name| self.get(name).map(str::to_string));
135 spend(left, v.len() + out.len());
136 out
137 } else {
138 warn_spent();
139 v.clone()
140 };
141 (k.clone(), v)
142 })
143 .collect();
144 Self::layer(Some(self.clone()), None, own)
145 }
146
147 pub fn over(defaults: &Macros, over: &Macros, left: &AtomicUsize) -> Macros {
151 if over.0.parent.is_none() && over.0.base.is_none() {
152 return defaults.with_budget(over, left);
153 }
154 let own = over
157 .iter()
158 .filter_map(|(k, v)| {
159 let default = defaults.get(k)?;
160 let fixed = v
161 .replace(&format!("$({k})"), default)
162 .replace(&format!("${{{k}}}"), default);
163 (fixed != v && over.get(k) == Some(v)).then(|| (k.to_string(), fixed))
164 })
165 .collect();
166 Self::layer(Some(over.clone()), Some(defaults.clone()), own)
167 }
168
169 pub fn own_bytes(&self) -> usize {
171 self.0.own.iter().map(|(k, v)| k.len() + v.len()).sum()
172 }
173}
174
175pub fn spend(left: &AtomicUsize, n: usize) {
177 let mut l = left.load(Ordering::Relaxed);
178 while let Err(now) =
179 left.compare_exchange(l, l.saturating_sub(n), Ordering::Relaxed, Ordering::Relaxed)
180 {
181 l = now;
182 }
183}
184
185static WARNED: AtomicBool = AtomicBool::new(false);
187
188pub fn rearm_warning() {
190 WARNED.store(false, Ordering::Relaxed);
191 SPENT_WARNED.store(false, Ordering::Relaxed);
192}
193
194static SPENT_WARNED: AtomicBool = AtomicBool::new(false);
195
196pub fn warn_spent() {
198 if !SPENT_WARNED.swap(true, Ordering::Relaxed) {
199 log::warn!("macro expansion budget spent; the rest of the load is left as written");
200 }
201}
202
203pub fn expand(text: &str, lookup: &dyn Fn(&str) -> Option<String>) -> String {
209 let mut work = 0;
210 expand_depth(text, lookup, 0, &mut work).unwrap_or_else(|| {
211 if !WARNED.swap(true, Ordering::Relaxed) {
212 log::warn!("macro expansion over {MAX_WORK} bytes left as written: {text:.80}");
213 }
214 text.to_string()
215 })
216}
217
218const MAX_WORK: usize = 16 << 10;
222
223fn expand_depth(
226 text: &str,
227 lookup: &dyn Fn(&str) -> Option<String>,
228 depth: u32,
229 work: &mut usize,
230) -> Option<String> {
231 *work += text.len();
232 if *work > MAX_WORK {
233 return None;
234 }
235 if depth > 10 || !text.contains('$') {
236 return Some(text.to_string());
237 }
238 let mut out = String::with_capacity(text.len());
239 let mut rest = text;
240 while let Some(pos) = rest.find('$') {
241 out.push_str(&rest[..pos]);
242 let after = &rest[pos + 1..];
243 let close = match after.chars().next() {
244 Some('(') => ')',
245 Some('{') => '}',
246 _ => {
247 out.push('$');
248 rest = after;
249 continue;
250 }
251 };
252 let Some(end) = matching(after, close) else {
253 out.push_str(&rest[pos..]);
254 return Some(out);
255 };
256 let body = expand_depth(&after[1..end], lookup, depth + 1, work)?;
257 let (name, default) = match body.split_once('=') {
258 Some((n, d)) => (n, Some(d)),
259 None => (body.as_str(), None),
260 };
261 match lookup(name).or(default.map(str::to_string)) {
262 Some(value) => out.push_str(&expand_depth(&value, lookup, depth + 1, work)?),
263 None => out.push_str(&rest[pos..pos + end + 2]),
264 }
265 rest = &after[end + 1..];
266 }
267 out.push_str(rest);
268 Some(out)
269}
270
271fn matching(s: &str, close: char) -> Option<usize> {
273 let open = s.chars().next()?;
274 let mut level = 0;
275 for (i, c) in s.char_indices() {
276 if c == open {
277 level += 1;
278 } else if c == close {
279 level -= 1;
280 if level == 0 {
281 return Some(i);
282 }
283 }
284 }
285 None
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn m() -> Macros {
293 Macros::from_pairs([("P", "BL01T"), ("R", ":DET:"), ("N", "$(P)")])
294 }
295
296 fn ex(s: &str) -> String {
297 let m = m();
298 expand(s, &|n| m.get(n).map(str::to_string))
299 }
300
301 #[test]
302 fn expands_both_bracket_styles() {
303 assert_eq!(ex("$(P)$(R)Acquire"), "BL01T:DET:Acquire");
304 assert_eq!(ex("${P}:X"), "BL01T:X");
305 }
306
307 #[test]
308 fn keeps_unknown_and_uses_defaults() {
309 assert_eq!(ex("$(Q):X"), "$(Q):X");
310 assert_eq!(ex("$(Q=dflt)"), "dflt");
311 assert_eq!(ex("cost $5"), "cost $5");
312 assert_eq!(ex("$(P"), "$(P");
313 }
314
315 #[test]
316 fn nested_expansion_is_bounded() {
317 let mut pairs = vec![("M0".to_string(), "x".to_string())];
319 for i in 1..=10 {
320 pairs.push((format!("M{i}"), format!("$(M{})", i - 1).repeat(6)));
321 }
322 let m = Macros::from_pairs(pairs);
323 let start = std::time::Instant::now();
324 let text = "$(M10)";
325 let out = expand(text, &|n| m.get(n).map(str::to_string));
326 assert_eq!(out, text);
327 assert!(start.elapsed().as_secs() < 2);
328 let m = Macros::from_pairs([("L", "y".repeat(10_000))]);
330 assert_eq!(
331 expand("$(L)", &|n| m.get(n).map(str::to_string)).len(),
332 10_000
333 );
334 }
335
336 #[test]
337 fn layers_share_a_budget() {
338 let root = Macros::from_pairs([("V", "x".repeat(1000))]);
339 let left = AtomicUsize::new(5000);
340 let inner = Macros::from_pairs((0..100).map(|i| (format!("N{i}"), "$(V)")));
341 let set = root.with_budget(&inner, &left);
342 assert_eq!(set.get("N0").map(str::len), Some(1000));
344 assert_eq!(set.get("N99"), Some("$(V)"));
345 assert!(set.own_bytes() < 10_000);
346 let more = set.with_budget(&inner, &left);
347 assert_eq!(more.get("N0"), Some("$(V)"));
348 }
349
350 #[test]
351 fn iter_lists_every_layer_as_get_sees_them() {
352 let defaults = Macros::from_pairs([("D", "dflt"), ("P", "D")]);
355 let caller = Macros::from_pairs([("P", "CALL")]).with(&Macros::from_pairs([("id", "01")]));
356 let m = Macros::over(&defaults, &caller, &AtomicUsize::new(1 << 20));
357 let last: std::collections::BTreeMap<_, _> = m.iter().collect();
358 for (k, v) in &last {
359 assert_eq!(m.get(k), Some(*v), "{k}");
360 }
361 assert_eq!(
362 last.into_iter().collect::<Vec<_>>(),
363 [("D", "dflt"), ("P", "CALL"), ("id", "01")]
364 );
365 }
366
367 #[test]
368 fn caller_self_references_use_the_displays_defaults() {
369 let ex = |m: &Macros, t: &str| expand(t, &|n| m.get(n).map(str::to_string));
370 let defaults = Macros::from_pairs([("P", "D")]);
371 let own = Macros::from_pairs([("P", "$(P):SUB")]);
372 let none = Macros::from_pairs([("X", "1")]);
373 let caller = none.with(&own);
374 let m = Macros::over(&defaults, &caller, &AtomicUsize::new(1 << 20));
375 assert_eq!(ex(&m, "$(P)"), "D:SUB");
376 let caller = Macros::from_pairs([("P", "CALL")]).with(&own);
377 let m = Macros::over(&defaults, &caller, &AtomicUsize::new(1 << 20));
378 assert_eq!(ex(&m, "$(P)"), "CALL:SUB");
379 }
380
381 #[test]
382 fn expands_recursively_and_layers() {
383 assert_eq!(ex("$(N)"), "BL01T");
384 let inner = Macros::from_pairs([("P", "$(P):SUB")]);
385 assert_eq!(m().with(&inner).get("P"), Some("BL01T:SUB"));
386 }
387}