Skip to main content

dataflow_rs/expr/
walk.rs

1//! Two pure walks over authored JSON, for the checks that need them.
2
3use super::engine::TEMPLATE_KEY_ESCAPE;
4use super::secrets::SECRET_OPERATOR as SECRET;
5use crate::base::AuthoredPath;
6use serde_json::Value;
7
8/// Every object key that starts with exactly one `$`, at any depth.
9///
10/// Each object's own keys come before the keys of the values below it. The
11/// engine strips one `$` from every template key, so these are the keys that
12/// emit something other than what was written.
13pub fn escaped_keys(raw: &Value) -> impl Iterator<Item = AuthoredPath> + '_ {
14    let mut out = Vec::new();
15    let mut at = AuthoredPath::root();
16    walk_escaped(raw, &mut at, &mut out);
17    out.into_iter()
18}
19
20fn walk_escaped(value: &Value, at: &mut AuthoredPath, out: &mut Vec<AuthoredPath>) {
21    match value {
22        Value::Array(items) => {
23            for (i, item) in items.iter().enumerate() {
24                at.push(i);
25                walk_escaped(item, at, out);
26                at.pop();
27            }
28        }
29        Value::Object(map) => {
30            for key in map.keys() {
31                let mut chars = key.chars();
32                if chars.next() == Some(TEMPLATE_KEY_ESCAPE)
33                    && chars.next() != Some(TEMPLATE_KEY_ESCAPE)
34                {
35                    out.push(at.join(key.as_str()));
36                }
37            }
38            for (key, child) in map {
39                at.push(key.as_str());
40                walk_escaped(child, at, out);
41                at.pop();
42            }
43        }
44        _ => {}
45    }
46}
47
48/// Every `{"secret": …}` reference, as the literal name (`None` when the name
49/// is computed) and where the reference sits. Lazy, so a caller that only
50/// asks whether one exists (via `next()`) stops at the first.
51///
52/// The walk goes into the argument too, since a computed name may itself read
53/// a secret.
54pub fn secret_refs(raw: &Value) -> impl Iterator<Item = (Option<&str>, AuthoredPath)> + '_ {
55    // An explicit stack, not recursion: `next()` must stop the walk where the
56    // caller stops asking, so a caller that only needs the first reference
57    // does not walk the whole expression. Children are pushed in reverse, so
58    // they pop in v3's order.
59    let mut stack = vec![(raw, AuthoredPath::root())];
60    std::iter::from_fn(move || {
61        while let Some((value, at)) = stack.pop() {
62            match value {
63                Value::Object(map) => {
64                    if map.len() == 1
65                        && let Some(arg) = map.get(SECRET)
66                    {
67                        let literal = match arg {
68                            Value::String(s) => Some(s.as_str()),
69                            Value::Array(items) if items.len() == 1 => items[0].as_str(),
70                            _ => None,
71                        };
72                        stack.push((arg, at.join(SECRET)));
73                        return Some((literal, at));
74                    }
75                    for (key, child) in map.iter().rev() {
76                        stack.push((child, at.join(key.as_str())));
77                    }
78                }
79                Value::Array(items) => {
80                    for (i, child) in items.iter().enumerate().rev() {
81                        stack.push((child, at.join(i)));
82                    }
83                }
84                _ => {}
85            }
86        }
87        None
88    })
89}
90
91/// Whether `raw` reads a secret anywhere, a computed name included: whether
92/// [`secret_refs`] finds any, checked without allocating paths.
93pub(crate) fn reads_secret(raw: &Value) -> bool {
94    match raw {
95        Value::Object(map) => {
96            (map.len() == 1 && map.contains_key(SECRET)) || map.values().any(reads_secret)
97        }
98        Value::Array(items) => items.iter().any(reads_secret),
99        _ => false,
100    }
101}