use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::Command;
use clap::Args;
use serde_yaml as sy;
use tempfile::tempdir;
use treediff::tools::{ChangeType, Recorder};
use treediff::value::Key;
use treediff::Mutable;
use crate::cli::ENV_PATH_SEP;
use crate::error::{IOResultExt, Result, YageError};
use crate::{
decrypt_yaml, encrypt_yaml, get_yaml_recipients, load_identities, read_yaml, write_yaml,
};
#[derive(Args, Debug)]
pub struct EditArgs {
#[clap(short, long, default_value = "vim", env = "EDITOR")]
pub editor: String,
#[clap(short, long = "key", value_name = "KEY", env = "YAGE_KEY", value_delimiter = ',')]
pub keys: Vec<String>,
#[clap(
short = 'K',
long = "key-file",
value_name = "FILE",
env = "YAGE_KEY_FILE",
value_delimiter = ENV_PATH_SEP,
)]
pub key_files: Vec<PathBuf>,
#[arg()]
pub file: PathBuf,
}
pub fn edit(args: &EditArgs) -> Result<i32> {
if args.file == PathBuf::from("-") {
return Err(YageError::InPlaceStdin);
}
let identities = load_identities(&args.keys, &args.key_files)?;
let input_data = read_yaml(&args.file)?;
let recipients = get_yaml_recipients(&input_data)?;
if recipients.is_empty() {
return Err(YageError::NoRecipients);
}
let previous_data = decrypt_yaml(&input_data, &identities)?;
let dir = tempdir()?;
#[cfg(unix)]
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?;
let filename =
args.file.file_name().ok_or(YageError::InvalidFileName { path: args.file.clone() })?;
let temp_file = dir.path().join(filename);
write_yaml(&temp_file, &previous_data)?;
run_editor(&args.editor, &temp_file)?;
let edited_data = read_yaml(&temp_file)?;
let mut d = Recorder::default();
treediff::diff(&previous_data, &edited_data, &mut d);
let mut to_encrypt_data = edited_data.clone();
for d in d.calls {
if let ChangeType::Unchanged(keys, _) = d {
debug!("keeping unchanged key: {:?}", keys);
let v = yaml_get(&input_data, &keys)?;
to_encrypt_data.set(&keys, v);
}
}
let output_data = encrypt_yaml(&to_encrypt_data, &recipients)?;
write_yaml(&args.file, &output_data)?;
Ok(0)
}
fn yaml_get<'a>(data: &'a sy::Value, keys: &[Key]) -> Result<&'a sy::Value> {
if keys.is_empty() {
return Ok(data);
}
match &keys[0] {
Key::String(k) => {
let k: sy::Value = sy::from_str(k)?;
let value = data.get(k).ok_or(YageError::KeyNotFound)?;
yaml_get(value, &keys[1..])
}
Key::Index(i) => {
let value = data.get(i).ok_or(YageError::KeyNotFound)?;
yaml_get(value, &keys[1..])
}
}
}
fn run_editor(editor: &str, temp_file: &std::path::Path) -> Result<()> {
let editor_process_res = Command::new(editor).arg(temp_file).spawn();
let mut editor_process = match editor_process_res {
Ok(ep) => ep,
Err(err) => {
if let Some(ref editor_args) = shlex::split(editor) {
if editor_args.is_empty() {
return Err(err).path_ctx(editor);
}
Command::new(&editor_args[0])
.args(&editor_args[1..])
.arg(temp_file)
.spawn()
.path_ctx(&editor_args[0])?
} else {
return Err(err).path_ctx(editor);
}
}
};
if !editor_process.wait()?.success() {
return Err(YageError::Editor);
}
Ok(())
}