secenv 0.0.0

Secure environments.
use {
    crate::config::ConfigProfile,
    anyhow::{
        Context,
        Result,
    },
    clap::Arg,
    path_clean::PathClean,
    std::{
        path::PathBuf,
        str::FromStr,
    },
};

#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Privilege {
    Normal,
    Experimental,
}

#[derive(Debug)]
pub(crate) enum ManualFormat {
    Manpages,
    Markdown,
}

#[derive(Debug)]
pub(crate) struct CallArgs {
    pub privileges: Privilege,
    pub command: Command,
}

impl CallArgs {
    pub(crate) fn validate(&self) -> Result<()> {
        if self.privileges == Privilege::Experimental {
            return Ok(());
        }

        match &self.command {
            | _ => (),
        }

        Ok(())
    }
}

#[derive(Debug)]
pub(crate) enum Command {
    Manual { path: PathBuf, format: ManualFormat },
    Autocomplete { path: PathBuf, shell: clap_complete::Shell },
    Unlock { 
        profile: ConfigProfile,
        command: Option<Vec<String>>,
    },
    Init { path: PathBuf, force: bool },
}

pub(crate) struct ClapArgumentLoader {}

impl ClapArgumentLoader {
    fn get_absolute_path(matches: &clap::ArgMatches, name: &str) -> Result<PathBuf> {
        let path_str: &String = matches.get_one(name).unwrap();
        let path = std::path::Path::new(path_str);
        if path.is_absolute() {
            Ok(path.to_path_buf().clean())
        } else {
            Ok(std::env::current_dir()?.join(path).clean())
        }
    }

    pub(crate) fn root_command() -> clap::Command {
        let root = clap::Command::new(env!("CARGO_PKG_NAME"))
            .version(env!("CARGO_PKG_VERSION"))
            .about(env!("CARGO_PKG_DESCRIPTION"))
            .author("cchexcode <alexanderh.weber@outlook.com>")
            .propagate_version(true)
            .subcommand_required(false)
            .args([Arg::new("experimental")
                .short('e')
                .long("experimental")
                .help("Enables experimental features.")
                .num_args(0)])
            .subcommand(
                clap::Command::new("man")
                    .about("Renders the manual.")
                    .arg(clap::Arg::new("out").short('o').long("out").required(true))
                    .arg(
                        clap::Arg::new("format")
                            .short('f')
                            .long("format")
                            .value_parser(["manpages", "markdown"])
                            .required(true),
                    ),
            )
            .subcommand(
                clap::Command::new("autocomplete")
                    .about("Renders shell completion scripts.")
                    .arg(clap::Arg::new("out").short('o').long("out").required(true))
                    .arg(
                        clap::Arg::new("shell")
                            .short('s')
                            .long("shell")
                            .value_parser(["bash", "zsh", "fish", "elvish", "powershell"])
                            .required(true),
                    ),
            )
            .subcommand(
                clap::Command::new("unlock")
                    .about("Unlocks encrypted values and optionally executes a command.")
                    .arg(
                        clap::Arg::new("config")
                            .short('c')
                            .long("config")
                            .required(false)
                            .default_value("secenv.yaml"),
                    )
                    .arg(
                        clap::Arg::new("profile")
                            .short('p')
                            .long("profile")
                            .required(false)
                            .default_value("default"),
                    )
                    .arg(
                        clap::Arg::new("command")
                            .help("Command to execute with environment variables set")
                            .num_args(0..)
                            .last(true)
                            .value_name("COMMAND"),
                    ),
            )
            .subcommand(
                clap::Command::new("init")
                    .about("Initialize a new secenv configuration file.")
                    .arg(
                        clap::Arg::new("path")
                            .short('p')
                            .long("path")
                            .required(false)
                            .default_value("secenv.yaml")
                            .help("Path for the new config file"),
                    )
                    .arg(
                        clap::Arg::new("force")
                            .short('f')
                            .long("force")
                            .action(clap::ArgAction::SetTrue)
                            .help("Overwrite existing file"),
                    ),
            );
        root
    }

    pub(crate) fn load() -> Result<CallArgs> {
        let command = Self::root_command().get_matches();

        let privileges = if command.get_flag("experimental") {
            Privilege::Experimental
        } else {
            Privilege::Normal
        };

        let cmd = if let Some(subc) = command.subcommand_matches("man") {
            Command::Manual {
                path: Self::get_absolute_path(subc, "out")?,
                format: match subc.get_one::<String>("format").unwrap().as_str() {
                    | "manpages" => ManualFormat::Manpages,
                    | "markdown" => ManualFormat::Markdown,
                    | _ => return Err(anyhow::anyhow!("argument \"format\": unknown format")),
                },
            }
        } else if let Some(subc) = command.subcommand_matches("autocomplete") {
            Command::Autocomplete {
                path: Self::get_absolute_path(subc, "out")?,
                shell: clap_complete::Shell::from_str(subc.get_one::<String>("shell").unwrap().as_str()).unwrap(),
            }
        } else if let Some(subc) = command.subcommand_matches("unlock") {
            let config_path = Self::get_absolute_path(subc, "config")?;
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config: {}", config_path.display()))?;
            let cfg: crate::config::Config = serde_yml::from_str(&contents)?;
            let profile_name = subc.get_one::<String>("profile").unwrap();

            let selected_profile = cfg
                .profiles
                .get(profile_name)
                .with_context(|| format!("Profile '{}' not found in config", profile_name))?;

            let command = subc.get_many::<String>("command")
                .map(|values| values.cloned().collect::<Vec<String>>());

            Command::Unlock {
                profile: selected_profile.clone(),
                command,
            }
        } else if let Some(subc) = command.subcommand_matches("init") {
            let config_path = Self::get_absolute_path(subc, "path")?;
            let force = subc.get_flag("force");

            Command::Init {
                path: config_path,
                force,
            }
        } else {
            anyhow::bail!("unknown command")
        };

        let callargs = CallArgs {
            privileges,
            command: cmd,
        };

        callargs.validate()?;
        Ok(callargs)
    }
}