1085 lines
30 KiB
Rust
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"));
|
|
}
|
|
}
|