Skip to main content

rvaultlib/edit/
mod.rs

1use std::io::{self, Write};
2
3use inquire::InquireError;
4
5use crate::{
6    keys::{check_key_conflicts, is_encrypted},
7    typedetect::{FileFormat, detect_format},
8    types::{ProcessHandling, TypedValue, ValueType},
9    vaultfunc,
10};
11
12/// Interactively edit vault-encrypted values in a JSON or YAML file.
13///
14/// For each encrypted value that matches `keys` (or all encrypted values when `keys` is empty),
15/// the current decrypted value is shown and the user is prompted for a replacement.
16/// An empty input keeps the existing encrypted value; `q` aborts processing.
17///
18/// - `input`:    path to the input file
19/// - `output`:   path to write the result, or `"stdout"` to print to stdout
20/// - `password`: vault password
21/// - `keys`:     dot-notation key paths to edit; empty means all encrypted values
22pub fn edit(
23    input: &str,
24    output: &str,
25    password: &str,
26    keys: &[String],
27) -> Result<(), Box<dyn std::error::Error>> {
28    check_key_conflicts(keys)?;
29    println!("Editing: {input}");
30    println!();
31    let mut processor = make_edit_processor(password);
32    match detect_format(input)? {
33        FileFormat::Json => crate::json::process_file(input, output, keys, &mut processor),
34        FileFormat::Yaml => crate::yaml::process_file(input, output, keys, &mut processor),
35        FileFormat::Vault => Err("cannot edit a raw vault-encrypted file (level-0)".into()),
36    }
37}
38
39/// Fuzzy-select which encrypted keys to edit when none are specified explicitly.
40///
41/// Shows an `inquire::MultiSelect` over all encrypted key paths in the file,
42/// then calls `edit` with the selected keys.
43pub fn edit_fuzzy(
44    input: &str,
45    output: &str,
46    password: &str,
47) -> Result<(), Box<dyn std::error::Error>> {
48    let keys = match detect_format(input)? {
49        FileFormat::Json => crate::json::get_encrypted_keys(input)?,
50        FileFormat::Yaml => crate::yaml::get_encrypted_keys(input)?,
51        FileFormat::Vault => {
52            return Err("cannot edit a raw vault-encrypted file (level-0)".into())
53        }
54    };
55
56    if keys.is_empty() {
57        println!("No encrypted values found in '{}'.", input);
58        return Ok(());
59    }
60
61    let selected = match inquire::MultiSelect::new("Select keys to edit (Space to select, Enter to confirm):", keys).prompt() {
62        Ok(s) => s,
63        Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {
64            println!("Cancelled.");
65            return Ok(());
66        }
67        Err(e) => return Err(e.into()),
68    };
69
70    if selected.is_empty() {
71        println!("No keys selected.");
72        return Ok(());
73    }
74
75    edit(input, output, password, &selected)
76}
77
78fn display_value(v: &TypedValue) -> String {
79    match v {
80        TypedValue::String(s) => s.clone(),
81        TypedValue::Integer(i) => i.to_string(),
82        TypedValue::Bool(b) => b.to_string(),
83        TypedValue::Number(f) => f.to_string(),
84        TypedValue::Null => "null".to_string(),
85    }
86}
87
88pub(crate) fn make_edit_processor(
89    password: &str,
90) -> impl FnMut(
91    TypedValue,
92    ValueType,
93    &str,
94) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>
95+ '_ {
96    move |typed, vt, key_path| {
97        let (encrypted, vault_str) = is_encrypted(&typed);
98        if !encrypted {
99            return Ok((typed, vt, ProcessHandling::Skip));
100        }
101        let vault_str = vault_str.unwrap();
102
103        let (decrypted_val, _) = match vaultfunc::decrypt(&vault_str, password) {
104            Ok(result) => result,
105            Err(e) => {
106                eprintln!("[!] could not decrypt '{key_path}': {e}");
107                return Ok((typed, vt, ProcessHandling::Skip));
108            }
109        };
110
111        println!("key:     {key_path}");
112        println!("current: {}", display_value(&decrypted_val));
113        print!("new value (empty = keep, q = quit): ");
114        io::stdout().flush()?;
115
116        let mut line = String::new();
117        io::stdin().read_line(&mut line)?;
118        let trimmed = line.trim();
119
120        println!();
121
122        if trimmed.is_empty() {
123            return Ok((typed, vt, ProcessHandling::Skip));
124        }
125        if trimmed.eq_ignore_ascii_case("q") {
126            println!("Cancelled.");
127            return Ok((typed, vt, ProcessHandling::Cancel));
128        }
129
130        let (new_typed, _) = vaultfunc::input_type(trimmed);
131        match vaultfunc::encrypt(&new_typed, password) {
132            Ok(enc) => Ok((TypedValue::String(enc), ValueType::String, ProcessHandling::Process)),
133            Err(e) => {
134                eprintln!("[!] could not encrypt new value for '{key_path}': {e}");
135                Ok((typed, vt, ProcessHandling::Skip))
136            }
137        }
138    }
139}