use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use shadow_crypt_core::profile::SecurityProfile;
use crate::errors::{WorkflowError, WorkflowResult};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum CliProfile {
#[default]
Standard,
Paranoid,
#[value(hide = true)]
Test,
}
impl From<CliProfile> for SecurityProfile {
fn from(profile: CliProfile) -> Self {
match profile {
CliProfile::Standard => SecurityProfile::Standard,
CliProfile::Paranoid => SecurityProfile::Paranoid,
CliProfile::Test => SecurityProfile::Test,
}
}
}
#[derive(Debug, Clone, Default, Parser)]
#[command(
name = "shadow",
about = "Encrypt files using shadow format",
version,
after_help = "A directory input becomes a single encrypted archive that hides the file \
count, names, and sizes inside it.\n\n\
Run with --profiles to see each security profile's key derivation \
parameters.\n\n\
Exit codes: 0 success; 1 operation failed; 2 invalid usage or input; \
3 authentication failure (wrong password or corrupted file)."
)]
pub struct EncryptionCliArgs {
#[arg(value_name = "PATH")]
pub input_files: Vec<String>,
#[arg(long = "profile", value_enum, default_value_t = CliProfile::Standard)]
pub profile: CliProfile,
#[arg(long = "profiles")]
pub list_profiles: bool,
#[arg(long = "output-dir", short = 'o', value_name = "DIR")]
pub output_dir: Option<PathBuf>,
#[arg(long = "password-file", value_name = "FILE")]
pub password_file: Option<PathBuf>,
#[arg(long = "quiet", short = 'q')]
pub quiet: bool,
#[arg(long = "delete")]
pub delete: bool,
}
pub fn get_cli_args(args: Vec<String>) -> WorkflowResult<EncryptionCliArgs> {
let cli_args = EncryptionCliArgs::try_parse_from(args).map_err(|e| {
if e.kind() == clap::error::ErrorKind::DisplayHelp
|| e.kind() == clap::error::ErrorKind::DisplayVersion
{
eprintln!("{}", e);
std::process::exit(0);
}
WorkflowError::UserInput(e.to_string())
})?;
if !cli_args.list_profiles && cli_args.input_files.is_empty() {
return Err(WorkflowError::UserInput(
"No input files provided".to_string(),
));
}
Ok(cli_args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_defaults_to_standard() {
let args = get_cli_args(vec!["shadow".to_string(), "file1.txt".to_string()]).unwrap();
assert_eq!(args.profile, CliProfile::Standard);
assert_eq!(
SecurityProfile::from(args.profile),
SecurityProfile::Standard
);
}
#[test]
fn test_profile_parses_all_levels() {
for (name, expected) in [
("standard", SecurityProfile::Standard),
("paranoid", SecurityProfile::Paranoid),
("test", SecurityProfile::Test),
] {
let args = get_cli_args(vec![
"shadow".to_string(),
"--profile".to_string(),
name.to_string(),
"file1.txt".to_string(),
])
.unwrap();
assert_eq!(SecurityProfile::from(args.profile), expected);
}
}
#[test]
fn test_parse_cli_args_with_files() {
let args = vec![
"shadow".to_string(),
"file1.txt".to_string(),
"file2.txt".to_string(),
];
let cli_args = get_cli_args(args).unwrap();
assert_eq!(
cli_args.input_files,
vec!["file1.txt".to_string(), "file2.txt".to_string()]
);
}
#[test]
fn test_parse_cli_args_no_files() {
let args = vec!["shadow".to_string()];
let result = get_cli_args(args);
assert!(result.is_err());
if let Err(WorkflowError::UserInput(msg)) = result {
assert_eq!(msg, "No input files provided");
} else {
panic!("Expected UserInput error");
}
}
#[test]
fn test_profiles_flag_needs_no_input_files() {
let args = get_cli_args(vec!["shadow".to_string(), "--profiles".to_string()]).unwrap();
assert!(args.list_profiles);
assert!(args.input_files.is_empty());
}
}