oth_rvault 0.2.0

Partial Ansible Vault encoder and decoder
Documentation
use clap::{Args, Parser, Subcommand};
use rvaultlib::{decrypt::decrypt, edit::edit, encrypt::encrypt};

/// Partial vault encryption/decryption for JSON and YAML files (Ansible Vault compatible)
#[derive(Parser)]
#[command(name = "rvault", version)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// Flags shared between encrypt and decrypt commands
#[derive(Args)]
struct CommonArgs {
    /// Path to the input file
    #[arg(short, long, required = true)]
    input: String,

    /// Output file to create (required unless --overwrite or --stdout is given)
    #[arg(short, long)]
    output: Option<String>,

    /// Vault password
    #[arg(short, long, required = true)]
    password: String,

    /// Keys to process using dot notation for nested keys (e.g. second.a.v).
    /// Can be given multiple times or as comma-separated values.
    /// If omitted, all encryptable values are processed.
    #[arg(short = 'k', long, value_delimiter = ',')]
    key: Vec<String>,

    /// Overwrite the input file with the result instead of writing to a separate output file
    #[arg(long)]
    overwrite: bool,

    /// Print the result to stdout instead of writing to a file
    #[arg(long)]
    stdout: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Partially encrypt a JSON or YAML file using Ansible Vault AES256
    Encrypt {
        #[command(flatten)]
        common: CommonArgs,

        /// Encrypt values in the style used by Ansible (intermediate file for YAML)
        #[arg(long)]
        ansible: bool,
    },

    /// Decrypt a partially Ansible Vault encoded JSON or YAML file
    Decrypt {
        #[command(flatten)]
        common: CommonArgs,
    },

    /// Interactively edit vault-encrypted values in a JSON or YAML file.
    /// With --key: edit only the specified keys.
    /// Without --key: requires --interactive to edit all encrypted values.
    Edit {
        #[command(flatten)]
        common: CommonArgs,

        /// Process all encrypted values when no --key is given
        #[arg(short = 'a', long)]
        interactive: bool,
    },
}

/// Resolve the effective output target from the three mutually-exclusive output options.
/// Returns `None` and prints an error if none of the options are provided.
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 main() {
    let cli = Cli::parse();

    match cli.command {
        Commands::Encrypt {
            common,
            ansible: _,
        } => {
            let Some(output) = resolve_output(
                &common.input,
                common.output.as_deref(),
                common.overwrite,
                common.stdout,
            ) else {
                std::process::exit(1);
            };
            if let Err(e) = encrypt(&common.input, &output, &common.password, &common.key) {
                eprintln!("error: {e}");
                std::process::exit(1);
            }
        }

        Commands::Decrypt { common } => {
            let Some(output) = resolve_output(
                &common.input,
                common.output.as_deref(),
                common.overwrite,
                common.stdout,
            ) else {
                std::process::exit(1);
            };
            if let Err(e) = decrypt(&common.input, &output, &common.password, &common.key) {
                eprintln!("error: {e}");
                std::process::exit(1);
            }
        }

        Commands::Edit {
            common,
            interactive,
        } => {
            if common.key.is_empty() && !interactive {
                eprintln!("No keys given. Use --key to specify keys or -a/--interactive to edit all encrypted values.");
                std::process::exit(1);
            }
            let Some(output) = resolve_output(
                &common.input,
                common.output.as_deref(),
                common.overwrite,
                common.stdout,
            ) else {
                std::process::exit(1);
            };
            if let Err(e) = edit(&common.input, &output, &common.password, &common.key) {
                eprintln!("error: {e}");
                std::process::exit(1);
            }
        }
    }
}