use std::{ffi::OsString, fmt, io, ops::Deref, path::PathBuf, str::FromStr, time::Duration};
use anyhow::anyhow;
use byte_unit::{n_eib_bytes, n_mib_bytes};
use clap::{value_parser, ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueHint};
use clap_complete::{Generator, Shell};
use fraction::{Fraction, Zero};
#[derive(Debug, Parser)]
#[command(
name("rscrypt"),
version,
about,
max_term_width(100),
propagate_version(true),
arg_required_else_help(true),
args_conflicts_with_subcommands(true)
)]
pub struct Opt {
#[arg(long, value_enum, value_name("SHELL"))]
pub generate_completion: Option<Shell>,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(name("enc"))]
Encrypt(Encrypt),
#[command(name("dec"))]
Decrypt(Decrypt),
#[command(name("info"))]
Information(Information),
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Debug)]
#[command(
group(ArgGroup::new("password")),
group(
ArgGroup::new("resources")
.multiple(true)
.conflicts_with("force")
.conflicts_with("parameters")
),
group(ArgGroup::new("parameters").multiple(true))
)]
pub struct Encrypt {
#[arg(short, long, requires("parameters"))]
pub force: bool,
#[arg(short('M'), long, value_name("BYTES"), group("resources"))]
pub max_memory: Option<Byte>,
#[arg(
short,
long,
default_value("0.125"),
value_name("RATE"),
group("resources")
)]
pub max_memory_fraction: Rate,
#[arg(
short('t'),
long,
default_value("5s"),
value_name("DURATION"),
group("resources")
)]
pub max_time: Time,
#[arg(
value_parser(value_parser!(u8).range(10..=40)),
long,
requires("r"),
requires("p"),
value_name("VALUE"),
group("parameters")
)]
pub log_n: Option<u8>,
#[arg(
value_parser(value_parser!(u32).range(1..=32)),
short,
requires("log_n"),
requires("p"),
value_name("VALUE"),
group("parameters")
)]
pub r: Option<u32>,
#[arg(
value_parser(value_parser!(u32).range(1..=32)),
short,
requires("log_n"),
requires("r"),
value_name("VALUE"),
group("parameters")
)]
pub p: Option<u32>,
#[arg(long, group("password"))]
pub passphrase_from_tty: bool,
#[arg(long, group("password"))]
pub passphrase_from_stdin: bool,
#[arg(long, group("password"))]
pub passphrase_from_tty_once: bool,
#[arg(long, value_name("VAR"), group("password"))]
pub passphrase_from_env: Option<OsString>,
#[arg(
long,
value_name("FILE"),
value_hint(ValueHint::FilePath),
group("password")
)]
pub passphrase_from_file: Option<PathBuf>,
#[arg(short, long)]
pub verbose: bool,
#[arg(value_name("INFILE"), value_hint(ValueHint::FilePath))]
pub input: PathBuf,
#[arg(value_name("OUTFILE"), value_hint(ValueHint::FilePath))]
pub output: Option<PathBuf>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Debug)]
#[command(
group(ArgGroup::new("password")),
group(ArgGroup::new("resources").multiple(true).conflicts_with("force"))
)]
pub struct Decrypt {
#[arg(short, long)]
pub force: bool,
#[arg(short('M'), long, value_name("BYTES"), group("resources"))]
pub max_memory: Option<Byte>,
#[arg(
short,
long,
default_value("0.5"),
value_name("RATE"),
group("resources")
)]
pub max_memory_fraction: Rate,
#[arg(
short('t'),
long,
default_value("300s"),
value_name("DURATION"),
group("resources")
)]
pub max_time: Time,
#[arg(long, group("password"))]
pub passphrase_from_tty: bool,
#[arg(long, group("password"))]
pub passphrase_from_stdin: bool,
#[arg(long, value_name("VAR"), group("password"))]
pub passphrase_from_env: Option<OsString>,
#[arg(
long,
value_name("FILE"),
value_hint(ValueHint::FilePath),
group("password")
)]
pub passphrase_from_file: Option<PathBuf>,
#[arg(short, long)]
pub verbose: bool,
#[arg(value_name("INFILE"), value_hint(ValueHint::FilePath))]
pub input: PathBuf,
#[arg(value_name("OUTFILE"), value_hint(ValueHint::FilePath))]
pub output: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct Information {
#[cfg(any(
feature = "cbor",
feature = "json",
feature = "msgpack",
feature = "toml",
feature = "yaml"
))]
#[arg(short, long, value_enum, value_name("FORMAT"), ignore_case(true))]
pub format: Option<Format>,
#[arg(value_name("FILE"), value_hint(ValueHint::FilePath))]
pub input: PathBuf,
}
impl Opt {
pub fn print_completion(gen: impl Generator) {
clap_complete::generate(
gen,
&mut Self::command(),
Self::command().get_name(),
&mut io::stdout(),
);
}
}
#[derive(Clone, Copy, Debug)]
pub struct Byte(byte_unit::Byte);
impl Deref for Byte {
type Target = byte_unit::Byte;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for Byte {
type Err = anyhow::Error;
fn from_str(bytes: &str) -> anyhow::Result<Self> {
match byte_unit::Byte::from_str(bytes) {
Ok(b) if b.get_bytes() < n_mib_bytes!(1) => {
Err(anyhow!("amount of RAM is less than 1 MiB"))
}
Ok(b) if b.get_bytes() > n_eib_bytes!(16) => {
Err(anyhow!("amount of RAM is more than 16 EiB"))
}
Err(err) => Err(anyhow!("amount of RAM is not a valid value: {err}")),
Ok(b) => Ok(Self(b)),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Rate(Fraction);
impl Deref for Rate {
type Target = Fraction;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for Rate {
type Err = anyhow::Error;
fn from_str(rate: &str) -> anyhow::Result<Self> {
match Fraction::from_str(rate) {
Ok(r) if r == Fraction::zero() => Err(anyhow!("fraction is 0")),
Ok(r) if r > Fraction::from(0.5) => Err(anyhow!("fraction is more than 0.5")),
Err(err) => Err(anyhow!("fraction is not a valid number: {err}")),
Ok(r) => Ok(Self(r)),
}
}
}
#[derive(Clone, Copy)]
pub struct Time(Duration);
impl fmt::Debug for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Deref for Time {
type Target = Duration;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for Time {
type Err = anyhow::Error;
fn from_str(duration: &str) -> anyhow::Result<Self> {
match humantime::Duration::from_str(duration) {
Ok(d) => Ok(Self(*d)),
Err(err) => Err(anyhow!("time is not a valid value: {err}")),
}
}
}
#[cfg(any(
feature = "cbor",
feature = "json",
feature = "msgpack",
feature = "toml",
feature = "yaml"
))]
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
pub enum Format {
#[cfg(feature = "cbor")]
Cbor,
#[cfg(feature = "json")]
Json,
#[cfg(feature = "msgpack")]
#[value(name("msgpack"))]
MessagePack,
#[cfg(feature = "toml")]
Toml,
#[cfg(feature = "yaml")]
Yaml,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_app() {
Opt::command().debug_assert();
}
}