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 = if reads_secret(raw) {
60        vec![(raw, AuthoredPath::root())]
61    } else {
62        Vec::new()
63    };
64    std::iter::from_fn(move || {
65        while let Some((value, at)) = stack.pop() {
66            match value {
67                Value::Object(map) => {
68                    if map.len() == 1
69                        && let Some(arg) = map.get(SECRET)
70                    {
71                        let literal = match arg {
72                            Value::String(s) => Some(s.as_str()),
73                            Value::Array(items) if items.len() == 1 => items[0].as_str(),
74                            _ => None,
75                        };
76                        stack.push((arg, at.join(SECRET)));
77                        return Some((literal, at));
78                    }
79                    for (key, child) in map.iter().rev() {
80                        stack.push((child, at.join(key.as_str())));
81                    }
82                }
83                Value::Array(items) => {
84                    for (i, child) in items.iter().enumerate().rev() {
85                        stack.push((child, at.join(i)));
86                    }
87                }
88                _ => {}
89            }
90        }
91        None
92    })
93}
94
95/// Whether `raw` reads a secret anywhere, a computed name included: whether
96/// [`secret_refs`] finds any, checked without allocating paths.
97pub(crate) fn reads_secret(raw: &Value) -> bool {
98    match raw {
99        Value::Object(map) => {
100            (map.len() == 1 && map.contains_key(SECRET)) || map.values().any(reads_secret)
101        }
102        Value::Array(items) => items.iter().any(reads_secret),
103        _ => false,
104    }
105}