use clap::{ArgAction, Parser, Subcommand, ValueEnum};
use config::{ConfigError, Map, Source, Value, ValueKind};
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about)]
pub(crate) struct Cli {
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn up logging verbosity (multiple will turn it up more)",
conflicts_with = "quiet"
)]
verbose: u8,
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn down logging verbosity (multiple will turn it down more)",
conflicts_with = "verbose"
)]
quiet: u8,
#[clap(short, long, help = "Specify a path to the config file")]
config_path: Option<String>,
#[clap(short, long, help = "Specify the path to the IPC socket")]
socket_path: Option<String>,
#[clap(
short = 'a',
long,
help = "Specify the path to the salus-agent IPC socket"
)]
agent_socket_path: Option<String>,
#[command(subcommand)]
command: Commands,
}
impl Cli {
pub(crate) fn command(self) -> Commands {
self.command
}
pub(crate) fn config_path(&self) -> Option<&str> {
self.config_path.as_deref()
}
}
impl Source for Cli {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
let mut map = Map::new();
let origin = String::from("command line");
if self.verbose > 0 {
let _old = map.insert(
"verbose".to_string(),
Value::new(Some(&origin), ValueKind::U64(u8::into(self.verbose))),
);
}
if self.quiet > 0 {
let _old = map.insert(
"quiet".to_string(),
Value::new(Some(&origin), ValueKind::U64(u8::into(self.quiet))),
);
}
if let Some(socket_path) = &self.socket_path {
let _old = map.insert(
"socket_path".to_string(),
Value::new(Some(&origin), ValueKind::String(socket_path.clone())),
);
}
if let Some(agent_socket_path) = &self.agent_socket_path {
let _old = map.insert(
"agent_socket_path".to_string(),
Value::new(Some(&origin), ValueKind::String(agent_socket_path.clone())),
);
}
Ok(map)
}
}
#[derive(Clone, Debug, Subcommand)]
pub(crate) enum Commands {
Shares {
#[arg(short, long, default_value = "5", value_name = "COUNT")]
num_shares: u8,
#[arg(short, long, default_value = "3", value_name = "COUNT")]
threshold: u8,
},
Unlock {
#[arg(short, long, value_name = "NAME")]
set: Option<String>,
#[arg(short = 'f', long = "for", value_name = "SECONDS|forever")]
duration: Option<String>,
},
Lock,
Store {
#[arg(value_name = "KEY")]
key: String,
#[arg(value_name = "VALUE")]
value: Option<String>,
#[arg(long, value_name = "BYTES")]
max_value_bytes: Option<usize>,
#[arg(short, long)]
force: bool,
},
Read {
#[arg(value_name = "KEY")]
key: String,
},
Delete {
#[arg(value_name = "KEY")]
key: String,
#[arg(short, long)]
force: bool,
},
Find {
#[arg(index = 1, value_name = "REGEX")]
regex: String,
},
Search {
#[arg(index = 1, value_name = "QUERY")]
query: Option<String>,
#[arg(short, long)]
limit: Option<usize>,
},
Enroll {
#[arg(short, long, default_value = "default")]
name: String,
#[arg(long)]
force: bool,
#[arg(long)]
independent_auto: bool,
},
Forget {
#[arg(short, long, conflicts_with = "all")]
name: Option<String>,
#[arg(long)]
all: bool,
},
EnrollStatus,
Gen {
#[arg(
short,
long,
default_value_t = 30,
value_parser = clap::value_parser!(u32).range(8..=1024),
value_name = "N"
)]
length: u32,
#[arg(
short,
long,
action = ArgAction::Set,
num_args = 0..=1,
default_value_t = true,
default_missing_value = "true",
value_name = "BOOL"
)]
caps: bool,
#[arg(
short,
long,
action = ArgAction::Set,
num_args = 0..=1,
default_value_t = true,
default_missing_value = "true",
value_name = "BOOL"
)]
numbers: bool,
#[arg(
short,
long,
action = ArgAction::Set,
num_args = 0..=1,
default_value_t = true,
default_missing_value = "true",
value_name = "BOOL"
)]
special: bool,
#[arg(
long,
value_parser = clap::value_parser!(u32).range(1..=20),
value_name = "N",
conflicts_with_all = ["length", "caps", "numbers", "special"]
)]
passphrase: Option<u32>,
#[arg(
long,
value_enum,
default_value_t = GenKind::Space,
conflicts_with_all = ["length", "caps", "numbers", "special"]
)]
kind: GenKind,
#[arg(short, long, value_name = "KEY")]
key: Option<String>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum GenKind {
Space,
Hyphen,
Dot,
Camel,
}
#[cfg(test)]
mod test {
use anyhow::Result;
use clap::Parser;
use config::Source;
use super::Cli;
#[test]
fn collect_omits_unset_flags() -> Result<()> {
let cli = Cli::try_parse_from(["salusc", "unlock"])?;
let map = cli.collect()?;
assert!(
map.is_empty(),
"default Cli should emit nothing, got {map:?}"
);
Ok(())
}
#[test]
fn collect_includes_set_socket_path() -> Result<()> {
let cli = Cli::try_parse_from(["salusc", "-s", "/tmp/s.sock", "unlock"])?;
let map = cli.collect()?;
assert!(map.contains_key("socket_path"));
assert!(!map.contains_key("verbose"));
Ok(())
}
#[test]
fn collect_includes_agent_socket_path() -> Result<()> {
let cli = Cli::try_parse_from(["salusc", "-a", "/tmp/a.sock", "unlock"])?;
let map = cli.collect()?;
assert!(map.contains_key("agent_socket_path"));
assert!(!map.contains_key("socket_path"));
Ok(())
}
}