shadow_crypt_shell/encryption/
cli.rs1use std::path::PathBuf;
2
3use clap::{Parser, ValueEnum};
4use shadow_crypt_core::profile::SecurityProfile;
5
6use crate::errors::{WorkflowError, WorkflowResult};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
11pub enum CliProfile {
12 #[default]
14 Standard,
15 Paranoid,
17 #[value(hide = true)]
19 Test,
20}
21
22impl From<CliProfile> for SecurityProfile {
23 fn from(profile: CliProfile) -> Self {
24 match profile {
25 CliProfile::Standard => SecurityProfile::Standard,
26 CliProfile::Paranoid => SecurityProfile::Paranoid,
27 CliProfile::Test => SecurityProfile::Test,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Default, Parser)]
34#[command(
35 name = "shadow",
36 about = "Encrypt files using shadow format",
37 version,
38 after_help = "A directory input becomes a single encrypted archive that hides the file \
39 count, names, and sizes inside it.\n\n\
40 Run with --profiles to see each security profile's key derivation \
41 parameters.\n\n\
42 Exit codes: 0 success; 1 operation failed; 2 invalid usage or input; \
43 3 authentication failure (wrong password or corrupted file)."
44)]
45pub struct EncryptionCliArgs {
46 #[arg(value_name = "PATH")]
48 pub input_files: Vec<String>,
49
50 #[arg(long = "profile", value_enum, default_value_t = CliProfile::Standard)]
52 pub profile: CliProfile,
53
54 #[arg(long = "profiles")]
56 pub list_profiles: bool,
57
58 #[arg(long = "output-dir", short = 'o', value_name = "DIR")]
60 pub output_dir: Option<PathBuf>,
61
62 #[arg(long = "password-file", value_name = "FILE")]
64 pub password_file: Option<PathBuf>,
65
66 #[arg(long = "quiet", short = 'q')]
68 pub quiet: bool,
69
70 #[arg(long = "delete")]
74 pub delete: bool,
75}
76
77pub fn get_cli_args(args: Vec<String>) -> WorkflowResult<EncryptionCliArgs> {
79 let cli_args = EncryptionCliArgs::try_parse_from(args).map_err(|e| {
80 if e.kind() == clap::error::ErrorKind::DisplayHelp
82 || e.kind() == clap::error::ErrorKind::DisplayVersion
83 {
84 eprintln!("{}", e);
86 std::process::exit(0);
87 }
88 WorkflowError::UserInput(e.to_string())
89 })?;
90
91 if !cli_args.list_profiles && cli_args.input_files.is_empty() {
92 return Err(WorkflowError::UserInput(
93 "No input files provided".to_string(),
94 ));
95 }
96
97 Ok(cli_args)
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_profile_defaults_to_standard() {
106 let args = get_cli_args(vec!["shadow".to_string(), "file1.txt".to_string()]).unwrap();
107 assert_eq!(args.profile, CliProfile::Standard);
108 assert_eq!(
109 SecurityProfile::from(args.profile),
110 SecurityProfile::Standard
111 );
112 }
113
114 #[test]
115 fn test_profile_parses_all_levels() {
116 for (name, expected) in [
117 ("standard", SecurityProfile::Standard),
118 ("paranoid", SecurityProfile::Paranoid),
119 ("test", SecurityProfile::Test),
120 ] {
121 let args = get_cli_args(vec![
122 "shadow".to_string(),
123 "--profile".to_string(),
124 name.to_string(),
125 "file1.txt".to_string(),
126 ])
127 .unwrap();
128 assert_eq!(SecurityProfile::from(args.profile), expected);
129 }
130 }
131
132 #[test]
133 fn test_parse_cli_args_with_files() {
134 let args = vec![
135 "shadow".to_string(),
136 "file1.txt".to_string(),
137 "file2.txt".to_string(),
138 ];
139 let cli_args = get_cli_args(args).unwrap();
140 assert_eq!(
141 cli_args.input_files,
142 vec!["file1.txt".to_string(), "file2.txt".to_string()]
143 );
144 }
145
146 #[test]
147 fn test_parse_cli_args_no_files() {
148 let args = vec!["shadow".to_string()];
149 let result = get_cli_args(args);
150 assert!(result.is_err());
151 if let Err(WorkflowError::UserInput(msg)) = result {
152 assert_eq!(msg, "No input files provided");
153 } else {
154 panic!("Expected UserInput error");
155 }
156 }
157
158 #[test]
159 fn test_profiles_flag_needs_no_input_files() {
160 let args = get_cli_args(vec!["shadow".to_string(), "--profiles".to_string()]).unwrap();
161 assert!(args.list_profiles);
162 assert!(args.input_files.is_empty());
163 }
164}