use clap::{Args, Parser, Subcommand};
use rvaultlib::{
clip::clip,
decrypt::{decrypt, decrypt_fuzzy, decrypt_value_only},
edit::{edit, edit_fuzzy},
encrypt::encrypt,
};
#[derive(Parser)]
#[command(name = "rvault", version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Args)]
struct CommonArgs {
#[arg(short, long, required = true)]
input: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
password: Option<String>,
#[arg(short = 'k', long, value_delimiter = ',')]
key: Vec<String>,
#[arg(long)]
overwrite: bool,
#[arg(long)]
stdout: bool,
#[arg(long)]
pfile: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Encrypt {
#[command(flatten)]
common: CommonArgs,
#[arg(long)]
ansible: bool,
#[arg(long)]
level: Option<u32>,
},
Decrypt {
#[command(flatten)]
common: CommonArgs,
#[arg(short = 'v', long)]
value_only: bool,
#[arg(long)]
interactive: bool,
#[arg(long)]
no_color: bool,
},
Edit {
#[command(flatten)]
common: CommonArgs,
#[arg(short = 'a', long)]
interactive: bool,
},
Clip {
#[arg(short, long, required = true)]
input: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
pfile: Option<String>,
#[arg(long, default_value = "30")]
timeout: u64,
#[arg(long)]
no_color: bool,
},
}
fn resolve_output(
input: &str,
output: Option<&str>,
overwrite: bool,
stdout: bool,
) -> Option<String> {
match output {
Some(o) => Some(o.to_string()),
None if overwrite => Some(input.to_string()),
None if stdout => Some("stdout".to_string()),
None => {
eprintln!("No output file, --overwrite flag or --stdout given, cancel.");
None
}
}
}
fn resolve_vault_password(password: Option<&str>, pfile: Option<&str>) -> Result<String, String> {
if let Some(pw) = password {
if !pw.is_empty() {
return Ok(pw.to_string());
}
}
if let Some(pf) = pfile {
return std::fs::read_to_string(pf)
.map(|s| s.trim().to_string())
.map_err(|e| format!("failed to read password file '{}': {}", pf, e));
}
if let Ok(env_path) = std::env::var("MY_RVAULT_PWD") {
if std::path::Path::new(&env_path).is_file() {
return std::fs::read_to_string(&env_path)
.map(|s| s.trim().to_string())
.map_err(|e| {
format!(
"failed to read password file from MY_RVAULT_PWD '{}': {}",
env_path, e
)
});
}
return Err(format!(
"MY_RVAULT_PWD is set to '{}' but it is not a file",
env_path
));
}
Err("no password provided: use --password, --pfile, or set MY_RVAULT_PWD to a password file path".to_string())
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Encrypt {
common,
ansible: _,
level,
} => {
if let Some(_lvl) = level {
if !common.key.is_empty() {
eprintln!("error: --level and --key cannot be combined");
std::process::exit(1);
}
}
let Some(output) = resolve_output(
&common.input,
common.output.as_deref(),
common.overwrite,
common.stdout,
) else {
std::process::exit(1);
};
let password =
match resolve_vault_password(common.password.as_deref(), common.pfile.as_deref()) {
Ok(pw) => pw,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
};
if let Err(e) = encrypt(&common.input, &output, &password, &common.key, level) {
eprintln!("error: {e}");
std::process::exit(1);
}
}
Commands::Decrypt {
common,
value_only,
interactive,
no_color,
} => {
let color = !no_color && std::env::var_os("NO_COLOR").is_none();
if interactive && !common.key.is_empty() {
eprintln!("error: --interactive and --key cannot be combined");
std::process::exit(1);
}
if value_only && !interactive && common.key.is_empty() {
eprintln!("error: --value-only requires at least one --key or --interactive");
std::process::exit(1);
}
let Some(output) = resolve_output(
&common.input,
common.output.as_deref(),
common.overwrite,
common.stdout,
) else {
std::process::exit(1);
};
let password =
match resolve_vault_password(common.password.as_deref(), common.pfile.as_deref()) {
Ok(pw) => pw,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
};
let result = if interactive {
decrypt_fuzzy(&common.input, &output, &password, value_only, color)
} else if value_only {
decrypt_value_only(&common.input, &output, &password, &common.key)
} else {
decrypt(&common.input, &output, &password, &common.key)
};
if let Err(e) = result {
eprintln!("error: {e}");
std::process::exit(1);
}
}
Commands::Edit {
common,
interactive,
} => {
let Some(output) = resolve_output(
&common.input,
common.output.as_deref(),
common.overwrite,
common.stdout,
) else {
std::process::exit(1);
};
let password =
match resolve_vault_password(common.password.as_deref(), common.pfile.as_deref()) {
Ok(pw) => pw,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
};
let result = if !common.key.is_empty() || interactive {
edit(&common.input, &output, &password, &common.key)
} else {
edit_fuzzy(&common.input, &output, &password)
};
if let Err(e) = result {
eprintln!("error: {e}");
std::process::exit(1);
}
}
Commands::Clip {
input,
password,
pfile,
timeout,
no_color,
} => {
let color = !no_color && std::env::var_os("NO_COLOR").is_none();
let password = match resolve_vault_password(password.as_deref(), pfile.as_deref()) {
Ok(pw) => pw,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
};
if let Err(e) = clip(&input, &password, timeout, color) {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
}