Skip to main content

rvaultlib/json/
mod.rs

1use serde_json::Value;
2
3use crate::types::{new_key_path, ProcessHandling, TypedValue, ValueType};
4
5fn typed_value_from_json(val: &Value) -> Option<(TypedValue, ValueType)> {
6    match val {
7        Value::Bool(b) => Some((TypedValue::Bool(*b), ValueType::Bool)),
8        Value::String(s) => Some((TypedValue::String(s.clone()), ValueType::String)),
9        Value::Number(n) => {
10            if let Some(i) = n.as_i64() {
11                // serde_json Number is i64 if it was parsed from an integer literal
12                if n.is_i64() {
13                    return Some((TypedValue::Integer(i), ValueType::Integer));
14                }
15            }
16            n.as_f64()
17                .map(|f| (TypedValue::Number(f), ValueType::Number))
18        }
19        Value::Null => Some((TypedValue::Null, ValueType::Null)),
20        _ => None, // Object/Array – handled by recursion
21    }
22}
23
24fn typed_value_to_json(typed: TypedValue) -> Value {
25    match typed {
26        TypedValue::Bool(b) => Value::Bool(b),
27        TypedValue::Integer(i) => Value::Number(i.into()),
28        TypedValue::Number(f) => Value::Number(
29            serde_json::Number::from_f64(f).unwrap_or_else(|| 0i64.into()),
30        ),
31        TypedValue::String(s) => Value::String(s),
32        TypedValue::Null => Value::Null,
33    }
34}
35
36type Processor<'a> = &'a mut dyn FnMut(
37    TypedValue,
38    ValueType,
39    &str,
40) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>;
41
42fn traverse(
43    val: &mut Value,
44    key_path: &str,
45    processor: Processor<'_>,
46    filter_keys: &[String],
47) -> Result<bool, Box<dyn std::error::Error>> {
48    // Sub-node path: if this node matches a filter key and is Object/Array, offer the
49    // whole node to the processor as a serialized JSON string.
50    // - Process: replace node with the processor's result (encrypt case).
51    // - Skip: fall through to normal per-leaf recursion (decrypt on a node whose items
52    //   are individually encrypted, not as a single blob).
53    // - Cancel: abort.
54    if !filter_keys.is_empty()
55        && filter_keys.iter().any(|k| k.as_str() == key_path)
56        && matches!(val, Value::Object(_) | Value::Array(_))
57    {
58        let serialized = format!("__subnode__\n{}", serde_json::to_string(val)?);
59        match processor(TypedValue::String(serialized), ValueType::String, key_path) {
60            Ok((new_typed, _, ProcessHandling::Process)) => {
61                *val = typed_value_to_json(new_typed);
62                return Ok(true);
63            }
64            Ok((_, _, ProcessHandling::Cancel)) => return Ok(false),
65            Ok((_, _, ProcessHandling::Skip)) => {
66                // fall through to normal recursion below
67            }
68            Err(e) => {
69                eprintln!("error processing key '{}': {}", key_path, e);
70                return Ok(false);
71            }
72        }
73    }
74
75    match val {
76        Value::Object(map) => {
77            // Collect keys first so we can iterate with mutable access to values.
78            let keys: Vec<String> = map.keys().cloned().collect();
79            for key in &keys {
80                let new_path = new_key_path(key_path, key);
81                let child = map.get_mut(key).unwrap();
82                if !traverse(child, &new_path, processor, filter_keys)? {
83                    return Ok(false);
84                }
85            }
86        }
87        Value::Array(arr) => {
88            for item in arr.iter_mut() {
89                if !traverse(item, key_path, processor, filter_keys)? {
90                    return Ok(false);
91                }
92            }
93        }
94        scalar => {
95            if !filter_keys.is_empty()
96                && !filter_keys.iter().any(|k| k.as_str() == key_path)
97            {
98                return Ok(true);
99            }
100            if let Some((typed, vt)) = typed_value_from_json(scalar) {
101                match processor(typed, vt, key_path) {
102                    Ok((new_typed, _, ProcessHandling::Process)) => {
103                        // Sub-node decryption: the marker prefix "__subnode__\n" identifies
104                        // values that were encrypted as a whole JSON structure. Strip the
105                        // prefix and restore the structured node.
106                        let mut restored = false;
107                        if let TypedValue::String(ref s) = new_typed {
108                            if let Some(inner) = s.strip_prefix("__subnode__\n") {
109                                if let Ok(parsed) = serde_json::from_str::<Value>(inner) {
110                                    if matches!(parsed, Value::Object(_) | Value::Array(_)) {
111                                        *scalar = parsed;
112                                        restored = true;
113                                    }
114                                }
115                            }
116                        }
117                        if !restored {
118                            *scalar = typed_value_to_json(new_typed);
119                        }
120                    }
121                    Ok((_, _, ProcessHandling::Cancel)) => return Ok(false),
122                    Ok((_, _, ProcessHandling::Skip)) => {}
123                    Err(e) => {
124                        eprintln!("error processing key '{}': {}", key_path, e);
125                        return Ok(false);
126                    }
127                }
128            }
129        }
130    }
131    Ok(true)
132}
133
134/// Read a JSON file, apply `processor` to every matching scalar value, and return the result in memory.
135/// `filter_keys` is a list of dot-notation key paths to restrict processing to; empty means all values.
136pub fn process_in_memory(
137    input: &str,
138    filter_keys: &[String],
139    processor: Processor<'_>,
140) -> Result<Value, Box<dyn std::error::Error>> {
141    let content = std::fs::read_to_string(input)
142        .map_err(|e| format!("error reading '{}': {}", input, e))?;
143    let mut root: Value = serde_json::from_str(&content)
144        .map_err(|e| format!("error parsing JSON from '{}': {}", input, e))?;
145    if !traverse(&mut root, "", processor, filter_keys)? {
146        return Err("processing canceled".into());
147    }
148    Ok(root)
149}
150
151/// Read a JSON file, apply `processor` to every matching scalar value, and write the result.
152/// `filter_keys` is a list of dot-notation key paths to restrict processing to; empty means all values.
153/// Key insertion order is preserved via serde_json's `preserve_order` feature.
154pub fn process_file(
155    input: &str,
156    output: &str,
157    filter_keys: &[String],
158    processor: Processor<'_>,
159) -> Result<(), Box<dyn std::error::Error>> {
160    let content = std::fs::read_to_string(input)
161        .map_err(|e| format!("error reading '{}': {}", input, e))?;
162    let mut root: Value = serde_json::from_str(&content)
163        .map_err(|e| format!("error parsing JSON from '{}': {}", input, e))?;
164
165    if !traverse(&mut root, "", processor, filter_keys)? {
166        return Err("processing canceled".into());
167    }
168
169    let output_str = serde_json::to_string_pretty(&root)?;
170    if output == "stdout" {
171        println!("{}", output_str);
172    } else {
173        std::fs::write(output, &output_str)
174            .map_err(|e| format!("error writing '{}': {}", output, e))?;
175    }
176    Ok(())
177}
178
179/// Return the dot-notation paths of all vault-encrypted values in a JSON file.
180pub fn get_encrypted_keys(input: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
181    let content = std::fs::read_to_string(input)?;
182    let root: Value = serde_json::from_str(&content)?;
183    let mut encrypted = Vec::new();
184    collect_encrypted(&root, "", &mut encrypted);
185    Ok(encrypted)
186}
187
188fn collect_encrypted(val: &Value, key_path: &str, out: &mut Vec<String>) {
189    match val {
190        Value::Object(map) => {
191            for (key, child) in map.iter() {
192                collect_encrypted(child, &new_key_path(key_path, key), out);
193            }
194        }
195        Value::Array(arr) => {
196            for item in arr.iter() {
197                collect_encrypted(item, key_path, out);
198            }
199        }
200        Value::String(s) if s.contains("$ANSIBLE_VAULT;") => {
201            out.push(key_path.to_string());
202        }
203        _ => {}
204    }
205}
206
207/// Collect dot-notation key paths of all nodes exactly at `level` depth.
208/// Nodes in branches that don't reach `level` are omitted.
209pub fn collect_paths_at_level(input: &str, level: u32) -> Result<Vec<String>, Box<dyn std::error::Error>> {
210    let content = std::fs::read_to_string(input)
211        .map_err(|e| format!("error reading '{}': {}", input, e))?;
212    let root: Value = serde_json::from_str(&content)
213        .map_err(|e| format!("error parsing JSON from '{}': {}", input, e))?;
214    let mut paths = Vec::new();
215    collect_at_level(&root, "", level, &mut paths);
216    Ok(paths)
217}
218
219fn collect_at_level(val: &Value, key_path: &str, remaining: u32, out: &mut Vec<String>) {
220    if remaining == 0 {
221        out.push(key_path.to_string());
222        return;
223    }
224    if let Value::Object(map) = val {
225        for (key, child) in map.iter() {
226            collect_at_level(child, &new_key_path(key_path, key), remaining - 1, out);
227        }
228    }
229    // Arrays and scalars above the target level are not at the target depth: skip.
230}