Files
IronStorage/crates/storage/src/command.rs
2026-08-10 01:51:47 +00:00

1085 lines
30 KiB
Rust

//! Complete pass-compatible command request contract.
use std::{error::Error, ffi::OsString, fmt, num::NonZeroUsize, path::PathBuf};
use clap::{Args, CommandFactory, Parser, Subcommand, error::ErrorKind};
use clap_complete::{Shell, generate};
use crate::PRODUCT_NAME;
pub const EXIT_SUCCESS: u8 = 0;
pub const EXIT_FAILURE: u8 = 1;
pub const EXIT_USAGE: u8 = EXIT_FAILURE;
pub const EXIT_UNAVAILABLE: u8 = 69;
pub const EXIT_CONFIG: u8 = 78;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CliAction {
Run(CliInvocation),
Display(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CliInvocation {
config: Option<PathBuf>,
request: CommandRequest,
}
impl CliInvocation {
pub fn config(&self) -> Option<&std::path::Path> {
self.config.as_deref()
}
pub fn request(&self) -> &CommandRequest {
&self.request
}
pub fn into_parts(self) -> (Option<PathBuf>, CommandRequest) {
(self.config, self.request)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandRequest {
Init(InitRequest),
List(ListRequest),
Show(ShowRequest),
Find(FindRequest),
Grep(GrepRequest),
Insert(InsertRequest),
Edit(EditRequest),
Generate(GenerateRequest),
Remove(RemoveRequest),
Move(MoveRequest),
Copy(CopyRequest),
Git(GitRequest),
Otp(OtpRequest),
Completion { shell: CompletionShell },
Help { topic: Option<HelpTopic> },
Version,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompletionShell {
Bash,
Elvish,
Fish,
PowerShell,
Zsh,
}
impl CompletionShell {
fn generator(self) -> Shell {
match self {
Self::Bash => Shell::Bash,
Self::Elvish => Shell::Elvish,
Self::Fish => Shell::Fish,
Self::PowerShell => Shell::PowerShell,
Self::Zsh => Shell::Zsh,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InitRequest {
pub path: Option<String>,
pub key_identities: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ListRequest {
pub path: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShowRequest {
pub entry: Option<String>,
pub presentation: Presentation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Presentation {
Terminal,
Clipboard { line: NonZeroUsize },
QrCode { line: NonZeroUsize },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindRequest {
pub terms: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GrepRequest {
pub pattern: String,
pub ignore_case: bool,
pub invert_match: bool,
pub line_number: bool,
pub fixed_strings: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InsertRequest {
pub entry: String,
pub input: InsertInput,
pub force: bool,
}
impl InsertRequest {
pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
if !standard_input_is_terminal {
return if self.input == InsertInput::Multiline {
InputPlan::StandardInputToEnd
} else {
InputPlan::StandardInputLine
};
}
match self.input {
InsertInput::HiddenConfirmed => InputPlan::HiddenConfirmed,
InsertInput::EchoedLine => InputPlan::EchoedLine,
InsertInput::Multiline => InputPlan::StandardInputToEnd,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InsertInput {
HiddenConfirmed,
EchoedLine,
Multiline,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InputPlan {
HiddenConfirmed,
EchoedLine,
StandardInputLine,
StandardInputToEnd,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EditRequest {
pub entry: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GenerateRequest {
pub entry: String,
pub length: Option<NonZeroUsize>,
pub no_symbols: bool,
pub force: bool,
pub in_place: bool,
pub presentation: GeneratedPresentation,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GeneratedPresentation {
Terminal,
Clipboard,
QrCode,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemoveRequest {
pub entry: String,
pub recursive: bool,
pub force: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MoveRequest {
pub source: String,
pub destination: String,
pub force: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopyRequest {
pub source: String,
pub destination: String,
pub force: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitRequest {
Init,
Status,
Log {
maximum: Option<NonZeroUsize>,
},
Diff {
paths: Vec<String>,
},
Add {
paths: Vec<String>,
},
Commit {
message: String,
},
Remote(GitRemoteRequest),
Config(GitConfigRequest),
Fetch {
remote: Option<String>,
},
Pull {
remote: Option<String>,
branch: Option<String>,
},
Push {
remote: Option<String>,
branch: Option<String>,
},
Sync {
remote: Option<String>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitRemoteRequest {
List,
GetUrl { name: String },
Add { name: String, url: String },
SetUrl { name: String, url: String },
Remove { name: String },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitConfigRequest {
Get { key: String },
Set { key: String, value: String },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OtpRequest {
Code(OtpCodeRequest),
Insert(OtpInsertRequest),
Append(OtpAppendRequest),
Uri(OtpUriRequest),
Validate { uri: String },
Help,
Version,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpCodeRequest {
pub entry: String,
pub clipboard: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpInsertRequest {
pub entry: Option<String>,
pub force: bool,
pub echo: bool,
pub source: OtpInputSource,
}
impl OtpInsertRequest {
pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
line_input_plan(self.echo, standard_input_is_terminal)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpAppendRequest {
pub entry: String,
pub force: bool,
pub echo: bool,
pub source: OtpInputSource,
}
impl OtpAppendRequest {
pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
line_input_plan(self.echo, standard_input_is_terminal)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OtpInputSource {
Uri,
Secret {
issuer: Option<String>,
account: Option<String>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpUriRequest {
pub entry: String,
pub presentation: OtpUriPresentation,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpUriPresentation {
Terminal,
Clipboard,
QrCode,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HelpTopic {
Init,
List,
Show,
Find,
Grep,
Insert,
Edit,
Generate,
Remove,
Move,
Copy,
Git,
Otp,
}
impl HelpTopic {
fn parse(value: &str) -> Option<Self> {
match value {
"init" => Some(Self::Init),
"ls" | "list" => Some(Self::List),
"show" => Some(Self::Show),
"find" | "search" => Some(Self::Find),
"grep" => Some(Self::Grep),
"insert" | "add" => Some(Self::Insert),
"edit" => Some(Self::Edit),
"generate" => Some(Self::Generate),
"rm" | "remove" | "delete" => Some(Self::Remove),
"mv" | "rename" => Some(Self::Move),
"cp" | "copy" => Some(Self::Copy),
"git" => Some(Self::Git),
"otp" => Some(Self::Otp),
_ => None,
}
}
fn command_name(self) -> &'static str {
match self {
Self::Init => "init",
Self::List => "ls",
Self::Show => "show",
Self::Find => "find",
Self::Grep => "grep",
Self::Insert => "insert",
Self::Edit => "edit",
Self::Generate => "generate",
Self::Remove => "rm",
Self::Move => "mv",
Self::Copy => "cp",
Self::Git => "git",
Self::Otp => "otp",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CliParseError {
message: String,
exit_code: u8,
}
impl CliParseError {
pub fn exit_code(&self) -> u8 {
self.exit_code
}
}
impl fmt::Display for CliParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for CliParseError {}
pub fn parse_env() -> Result<CliAction, CliParseError> {
parse_from(std::env::args_os())
}
pub fn parse_from<I, T>(arguments: I) -> Result<CliAction, CliParseError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
let arguments = normalize_dispatch(arguments);
match CliArguments::try_parse_from(arguments) {
Ok(arguments) => convert_arguments(arguments).map(CliAction::Run),
Err(error)
if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) =>
{
Ok(CliAction::Display(error.to_string()))
}
Err(error) => Err(CliParseError {
message: error.to_string(),
exit_code: EXIT_USAGE,
}),
}
}
pub fn help_text(topic: Option<HelpTopic>) -> String {
let mut command = CliArguments::command();
if let Some(topic) = topic {
let name = topic.command_name();
if let Some(subcommand) = command.find_subcommand_mut(name) {
return subcommand.render_long_help().to_string();
}
}
command.render_long_help().to_string()
}
pub fn version_text() -> String {
format!("{PRODUCT_NAME} {}\n", env!("CARGO_PKG_VERSION"))
}
/// The observable version string from the pinned pass-otp v1.2.0 source.
pub fn otp_version_text() -> &'static str {
"1.1.1\n"
}
pub fn completion_script(shell: CompletionShell) -> Vec<u8> {
let mut command = CliArguments::command();
let mut output = Vec::new();
generate(shell.generator(), &mut command, "ironstorage", &mut output);
output
}
#[derive(Parser)]
#[command(
name = "ironstorage",
about = "A pass-compatible password-store client",
disable_help_subcommand = true,
version
)]
struct CliArguments {
#[arg(long, global = true, value_name = "PATH")]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<CommandArguments>,
}
#[derive(Subcommand)]
enum CommandArguments {
Init(InitArguments),
#[command(name = "ls", visible_alias = "list")]
List(ListArguments),
Show(ShowArguments),
#[command(visible_alias = "search")]
Find(FindArguments),
Grep(GrepArguments),
#[command(visible_alias = "add")]
Insert(InsertArguments),
Edit(EditArguments),
Generate(GenerateArguments),
#[command(name = "rm", visible_aliases = ["remove", "delete"])]
Remove(RemoveArguments),
#[command(name = "mv", visible_alias = "rename")]
Move(MoveArguments),
#[command(name = "cp", visible_alias = "copy")]
Copy(CopyArguments),
Git(GitArguments),
Otp(OtpArguments),
Completion(CompletionArguments),
Help(HelpArguments),
Version,
}
#[derive(Args)]
struct CompletionArguments {
#[arg(value_enum)]
shell: CompletionShellArgument,
}
#[derive(Clone, Copy, clap::ValueEnum)]
enum CompletionShellArgument {
Bash,
Elvish,
Fish,
Powershell,
Zsh,
}
#[derive(Args)]
struct InitArguments {
#[arg(short = 'p', long = "path")]
path: Option<String>,
#[arg(required = true, num_args = 1.., value_name = "GPG-ID")]
key_identities: Vec<String>,
}
#[derive(Args)]
struct ListArguments {
path: Option<String>,
}
#[derive(Args)]
struct ShowArguments {
#[arg(
short = 'c',
long = "clip",
num_args = 0..=1,
default_missing_value = "1",
require_equals = true,
conflicts_with = "qrcode",
value_name = "LINE"
)]
clip: Option<NonZeroUsize>,
#[arg(
short = 'q',
long = "qrcode",
num_args = 0..=1,
default_missing_value = "1",
require_equals = true,
conflicts_with = "clip",
value_name = "LINE"
)]
qrcode: Option<NonZeroUsize>,
entry: Option<String>,
}
#[derive(Args)]
struct FindArguments {
#[arg(required = true, num_args = 1..)]
terms: Vec<String>,
}
#[derive(Args)]
struct GrepArguments {
#[arg(short = 'i', long = "ignore-case")]
ignore_case: bool,
#[arg(short = 'v', long = "invert-match")]
invert_match: bool,
#[arg(short = 'n', long = "line-number")]
line_number: bool,
#[arg(short = 'F', long = "fixed-strings")]
fixed_strings: bool,
pattern: String,
}
#[derive(Args)]
struct InsertArguments {
#[arg(short = 'e', long = "echo", conflicts_with = "multiline")]
echo: bool,
#[arg(short = 'm', long = "multiline", conflicts_with = "echo")]
multiline: bool,
#[arg(short = 'f', long = "force")]
force: bool,
entry: String,
}
#[derive(Args)]
struct EditArguments {
entry: String,
}
#[derive(Args)]
struct GenerateArguments {
#[arg(short = 'n', long = "no-symbols")]
no_symbols: bool,
#[arg(short = 'c', long = "clip", conflicts_with = "qrcode")]
clip: bool,
#[arg(short = 'q', long = "qrcode", conflicts_with = "clip")]
qrcode: bool,
#[arg(short = 'i', long = "in-place", conflicts_with = "force")]
in_place: bool,
#[arg(short = 'f', long = "force", conflicts_with = "in_place")]
force: bool,
entry: String,
length: Option<NonZeroUsize>,
}
#[derive(Args)]
struct RemoveArguments {
#[arg(short = 'r', long = "recursive")]
recursive: bool,
#[arg(short = 'f', long = "force")]
force: bool,
entry: String,
}
#[derive(Args)]
struct MoveArguments {
#[arg(short = 'f', long = "force")]
force: bool,
source: String,
destination: String,
}
#[derive(Args)]
struct CopyArguments {
#[arg(short = 'f', long = "force")]
force: bool,
source: String,
destination: String,
}
#[derive(Args)]
struct GitArguments {
#[command(subcommand)]
command: GitCommandArguments,
}
#[derive(Subcommand)]
enum GitCommandArguments {
Init,
Status,
Log {
#[arg(short = 'n', long = "max-count")]
maximum: Option<NonZeroUsize>,
},
Diff {
paths: Vec<String>,
},
Add {
#[arg(required = true, num_args = 1..)]
paths: Vec<String>,
},
Commit {
#[arg(short = 'm', long = "message")]
message: String,
},
Remote {
#[command(subcommand)]
command: Option<GitRemoteArguments>,
},
Config(GitConfigArguments),
Fetch {
remote: Option<String>,
},
Pull {
remote: Option<String>,
branch: Option<String>,
},
Push {
remote: Option<String>,
branch: Option<String>,
},
Sync {
remote: Option<String>,
},
}
#[derive(Subcommand)]
enum GitRemoteArguments {
GetUrl { name: String },
Add { name: String, url: String },
SetUrl { name: String, url: String },
Remove { name: String },
}
#[derive(Args)]
struct GitConfigArguments {
#[arg(long, conflicts_with = "values")]
get: Option<String>,
#[arg(num_args = 2, value_names = ["KEY", "VALUE"])]
values: Vec<String>,
}
#[derive(Args)]
struct OtpArguments {
#[command(subcommand)]
command: OtpCommandArguments,
}
#[derive(Subcommand)]
enum OtpCommandArguments {
#[command(visible_alias = "show")]
Code(OtpCodeArguments),
#[command(visible_alias = "add")]
Insert(OtpInsertArguments),
Append(OtpAppendArguments),
Uri(OtpUriArguments),
Validate {
uri: String,
},
Version,
}
#[derive(Args)]
struct OtpCodeArguments {
#[arg(short = 'c', long = "clip")]
clipboard: bool,
entry: String,
}
#[derive(Args)]
struct OtpInsertArguments {
#[arg(short = 'f', long = "force")]
force: bool,
#[arg(short = 'e', long = "echo")]
echo: bool,
#[arg(short = 's', long = "secret")]
secret: bool,
#[arg(short = 'i', long = "issuer")]
issuer: Option<String>,
#[arg(short = 'a', long = "account")]
account: Option<String>,
entry: Option<String>,
}
#[derive(Args)]
struct OtpAppendArguments {
#[arg(short = 'f', long = "force")]
force: bool,
#[arg(short = 'e', long = "echo")]
echo: bool,
#[arg(short = 's', long = "secret")]
secret: bool,
#[arg(short = 'i', long = "issuer")]
issuer: Option<String>,
#[arg(short = 'a', long = "account")]
account: Option<String>,
entry: String,
}
#[derive(Args)]
struct OtpUriArguments {
#[arg(short = 'c', long = "clip", conflicts_with = "qrcode")]
clipboard: bool,
#[arg(short = 'q', long = "qrcode", conflicts_with = "clipboard")]
qrcode: bool,
entry: String,
}
#[derive(Args)]
struct HelpArguments {
topic: Option<String>,
}
fn convert_arguments(arguments: CliArguments) -> Result<CliInvocation, CliParseError> {
let request = match arguments.command {
None => CommandRequest::Show(ShowRequest {
entry: None,
presentation: Presentation::Terminal,
}),
Some(CommandArguments::Init(arguments)) => CommandRequest::Init(InitRequest {
path: arguments.path,
key_identities: arguments.key_identities,
}),
Some(CommandArguments::List(arguments)) => CommandRequest::List(ListRequest {
path: arguments.path,
}),
Some(CommandArguments::Show(arguments)) => CommandRequest::Show(ShowRequest {
entry: arguments.entry,
presentation: match (arguments.clip, arguments.qrcode) {
(Some(line), None) => Presentation::Clipboard { line },
(None, Some(line)) => Presentation::QrCode { line },
(None, None) => Presentation::Terminal,
(Some(_), Some(_)) => unreachable!("clap rejects conflicting presentation flags"),
},
}),
Some(CommandArguments::Find(arguments)) => CommandRequest::Find(FindRequest {
terms: arguments.terms,
}),
Some(CommandArguments::Grep(arguments)) => CommandRequest::Grep(GrepRequest {
pattern: arguments.pattern,
ignore_case: arguments.ignore_case,
invert_match: arguments.invert_match,
line_number: arguments.line_number,
fixed_strings: arguments.fixed_strings,
}),
Some(CommandArguments::Insert(arguments)) => CommandRequest::Insert(InsertRequest {
entry: arguments.entry,
input: if arguments.multiline {
InsertInput::Multiline
} else if arguments.echo {
InsertInput::EchoedLine
} else {
InsertInput::HiddenConfirmed
},
force: arguments.force,
}),
Some(CommandArguments::Edit(arguments)) => CommandRequest::Edit(EditRequest {
entry: arguments.entry,
}),
Some(CommandArguments::Generate(arguments)) => CommandRequest::Generate(GenerateRequest {
entry: arguments.entry,
length: arguments.length,
no_symbols: arguments.no_symbols,
force: arguments.force,
in_place: arguments.in_place,
presentation: if arguments.clip {
GeneratedPresentation::Clipboard
} else if arguments.qrcode {
GeneratedPresentation::QrCode
} else {
GeneratedPresentation::Terminal
},
}),
Some(CommandArguments::Remove(arguments)) => CommandRequest::Remove(RemoveRequest {
entry: arguments.entry,
recursive: arguments.recursive,
force: arguments.force,
}),
Some(CommandArguments::Move(arguments)) => CommandRequest::Move(MoveRequest {
source: arguments.source,
destination: arguments.destination,
force: arguments.force,
}),
Some(CommandArguments::Copy(arguments)) => CommandRequest::Copy(CopyRequest {
source: arguments.source,
destination: arguments.destination,
force: arguments.force,
}),
Some(CommandArguments::Git(arguments)) => {
CommandRequest::Git(convert_git(arguments.command)?)
}
Some(CommandArguments::Otp(arguments)) => {
CommandRequest::Otp(convert_otp(arguments.command)?)
}
Some(CommandArguments::Completion(arguments)) => CommandRequest::Completion {
shell: match arguments.shell {
CompletionShellArgument::Bash => CompletionShell::Bash,
CompletionShellArgument::Elvish => CompletionShell::Elvish,
CompletionShellArgument::Fish => CompletionShell::Fish,
CompletionShellArgument::Powershell => CompletionShell::PowerShell,
CompletionShellArgument::Zsh => CompletionShell::Zsh,
},
},
Some(CommandArguments::Help(arguments)) => {
let topic = arguments
.topic
.map(|topic| {
HelpTopic::parse(&topic).ok_or_else(|| usage_error("unknown help topic"))
})
.transpose()?;
CommandRequest::Help { topic }
}
Some(CommandArguments::Version) => CommandRequest::Version,
};
Ok(CliInvocation {
config: arguments.config,
request,
})
}
fn convert_git(arguments: GitCommandArguments) -> Result<GitRequest, CliParseError> {
Ok(match arguments {
GitCommandArguments::Init => GitRequest::Init,
GitCommandArguments::Status => GitRequest::Status,
GitCommandArguments::Log { maximum } => GitRequest::Log { maximum },
GitCommandArguments::Diff { paths } => GitRequest::Diff { paths },
GitCommandArguments::Add { paths } => GitRequest::Add { paths },
GitCommandArguments::Commit { message } => GitRequest::Commit { message },
GitCommandArguments::Remote { command } => GitRequest::Remote(match command {
None => GitRemoteRequest::List,
Some(GitRemoteArguments::GetUrl { name }) => GitRemoteRequest::GetUrl { name },
Some(GitRemoteArguments::Add { name, url }) => GitRemoteRequest::Add { name, url },
Some(GitRemoteArguments::SetUrl { name, url }) => {
GitRemoteRequest::SetUrl { name, url }
}
Some(GitRemoteArguments::Remove { name }) => GitRemoteRequest::Remove { name },
}),
GitCommandArguments::Config(arguments) => {
let request = match (arguments.get, arguments.values.as_slice()) {
(Some(key), []) => GitConfigRequest::Get { key },
(None, [key, value]) => GitConfigRequest::Set {
key: key.clone(),
value: value.clone(),
},
_ => return Err(usage_error("git config requires --get KEY or KEY VALUE")),
};
GitRequest::Config(request)
}
GitCommandArguments::Fetch { remote } => GitRequest::Fetch { remote },
GitCommandArguments::Pull { remote, branch } => GitRequest::Pull { remote, branch },
GitCommandArguments::Push { remote, branch } => GitRequest::Push { remote, branch },
GitCommandArguments::Sync { remote } => GitRequest::Sync { remote },
})
}
fn convert_otp(arguments: OtpCommandArguments) -> Result<OtpRequest, CliParseError> {
Ok(match arguments {
OtpCommandArguments::Code(arguments) => OtpRequest::Code(OtpCodeRequest {
entry: arguments.entry,
clipboard: arguments.clipboard,
}),
OtpCommandArguments::Insert(arguments) => OtpRequest::Insert(OtpInsertRequest {
entry: arguments.entry,
force: arguments.force,
echo: arguments.echo,
source: otp_source(arguments.secret, arguments.issuer, arguments.account)?,
}),
OtpCommandArguments::Append(arguments) => OtpRequest::Append(OtpAppendRequest {
entry: arguments.entry,
force: arguments.force,
echo: arguments.echo,
source: otp_source(arguments.secret, arguments.issuer, arguments.account)?,
}),
OtpCommandArguments::Uri(arguments) => OtpRequest::Uri(OtpUriRequest {
entry: arguments.entry,
presentation: if arguments.clipboard {
OtpUriPresentation::Clipboard
} else if arguments.qrcode {
OtpUriPresentation::QrCode
} else {
OtpUriPresentation::Terminal
},
}),
OtpCommandArguments::Validate { uri } => OtpRequest::Validate { uri },
OtpCommandArguments::Version => OtpRequest::Version,
})
}
fn otp_source(
secret: bool,
issuer: Option<String>,
account: Option<String>,
) -> Result<OtpInputSource, CliParseError> {
if secret {
if issuer.is_none() && account.is_none() {
return Err(usage_error(
"--secret requires at least one of --issuer or --account",
));
}
Ok(OtpInputSource::Secret { issuer, account })
} else if issuer.is_some() || account.is_some() {
Err(usage_error("--issuer and --account require --secret"))
} else {
Ok(OtpInputSource::Uri)
}
}
fn line_input_plan(echo: bool, standard_input_is_terminal: bool) -> InputPlan {
if !standard_input_is_terminal {
InputPlan::StandardInputLine
} else if echo {
InputPlan::EchoedLine
} else {
InputPlan::HiddenConfirmed
}
}
fn usage_error(message: &str) -> CliParseError {
CliParseError {
message: format!("error: {message}\n"),
exit_code: EXIT_USAGE,
}
}
fn normalize_dispatch(mut arguments: Vec<OsString>) -> Vec<OsString> {
if arguments.is_empty() {
arguments.push(OsString::from("ironstorage"));
}
let Some(command_index) = first_command_index(&arguments) else {
return arguments;
};
let command = arguments[command_index].to_string_lossy();
const ROOT_COMMANDS: &[&str] = &[
"init",
"ls",
"list",
"show",
"find",
"search",
"grep",
"insert",
"add",
"edit",
"generate",
"rm",
"remove",
"delete",
"mv",
"rename",
"cp",
"copy",
"git",
"otp",
"help",
"version",
"completion",
];
if !ROOT_COMMANDS.contains(&command.as_ref()) && !command.starts_with('-') {
arguments.insert(command_index, OsString::from("show"));
normalize_show_presentation(&mut arguments, command_index + 1);
return arguments;
}
if command == "show" {
normalize_show_presentation(&mut arguments, command_index + 1);
} else if command == "otp" {
normalize_otp_dispatch(&mut arguments, command_index + 1);
}
arguments
}
fn normalize_show_presentation(arguments: &mut Vec<OsString>, mut index: usize) {
while index < arguments.len() {
let argument = arguments[index].to_string_lossy();
let long_name = match argument.as_ref() {
"-c" | "--clip" => Some("--clip"),
"-q" | "--qrcode" => Some("--qrcode"),
_ => None,
};
if let Some(long_name) = long_name {
let numeric_value = arguments
.get(index + 1)
.and_then(|value| value.to_str())
.filter(|value| {
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
})
.map(str::to_owned);
if let Some(value) = numeric_value {
arguments[index] = OsString::from(format!("{long_name}={value}"));
arguments.remove(index + 1);
}
} else if let Some(value) = argument
.strip_prefix("-c")
.filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
{
arguments[index] = OsString::from(format!("--clip={value}"));
} else if let Some(value) = argument
.strip_prefix("-q")
.filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
{
arguments[index] = OsString::from(format!("--qrcode={value}"));
}
index += 1;
}
}
fn first_command_index(arguments: &[OsString]) -> Option<usize> {
let mut index = 1;
while index < arguments.len() {
let argument = arguments[index].to_string_lossy();
if argument == "--config" {
index += 2;
} else if argument.starts_with("--config=") {
index += 1;
} else {
return Some(index);
}
}
None
}
fn normalize_otp_dispatch(arguments: &mut Vec<OsString>, mut index: usize) {
while index < arguments.len() {
let argument = arguments[index].to_string_lossy();
if argument == "--config" {
index += 2;
} else if argument.starts_with("--config=") {
index += 1;
} else {
break;
}
}
let Some(argument) = arguments.get(index).map(|value| value.to_string_lossy()) else {
return;
};
const OTP_COMMANDS: &[&str] = &[
"code", "show", "insert", "add", "append", "uri", "validate", "help", "version",
];
if argument == "help" {
arguments[index] = OsString::from("--help");
} else if argument == "--version" {
arguments[index] = OsString::from("version");
} else if !OTP_COMMANDS.contains(&argument.as_ref()) && argument != "--help" && argument != "-h"
{
arguments.insert(index, OsString::from("code"));
}
}