Skip to main content

shadow_crypt_shell/decryption/
cli.rs

1use std::path::PathBuf;
2
3use crate::errors::{WorkflowError, WorkflowResult};
4use clap::Parser;
5
6/// Decryption CLI arguments structure
7#[derive(Debug, Clone, Default, Parser)]
8#[command(
9    name = "unshadow",
10    about = "Decrypt shadow files",
11    version,
12    after_help = "Exit codes: 0 success; 1 operation failed; 2 invalid usage or input; \
13                  3 authentication failure (wrong password or corrupted file)."
14)]
15pub struct DecryptionCliArgs {
16    /// Input files to decrypt
17    #[arg(value_name = "FILE")]
18    pub input_files: Vec<String>,
19
20    /// Write decrypted files to this directory (created if missing; defaults to the current directory)
21    #[arg(long = "output-dir", short = 'o', value_name = "DIR")]
22    pub output_dir: Option<PathBuf>,
23
24    /// Read the password from this file instead of prompting (one trailing newline is ignored)
25    #[arg(long = "password-file", value_name = "FILE")]
26    pub password_file: Option<PathBuf>,
27
28    /// Overwrite existing output files instead of failing
29    #[arg(long = "force", short = 'f')]
30    pub force: bool,
31
32    /// Suppress progress and per-file success output (errors are still shown)
33    #[arg(long = "quiet", short = 'q')]
34    pub quiet: bool,
35}
36
37/// Parse decryption command line arguments
38pub fn get_cli_args(args: Vec<String>) -> WorkflowResult<DecryptionCliArgs> {
39    let cli_args = DecryptionCliArgs::try_parse_from(args).map_err(|e| {
40        // If it's help or version, it's not a user input error
41        if e.kind() == clap::error::ErrorKind::DisplayHelp
42            || e.kind() == clap::error::ErrorKind::DisplayVersion
43        {
44            // Print the message and exit successfully
45            eprintln!("{}", e);
46            std::process::exit(0);
47        }
48        WorkflowError::UserInput(e.to_string())
49    })?;
50
51    if cli_args.input_files.is_empty() {
52        return Err(WorkflowError::UserInput(
53            "No input files provided".to_string(),
54        ));
55    }
56
57    Ok(cli_args)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_parse_cli_args_with_files() {
66        let args = vec![
67            "unshadow".to_string(),
68            "file1.txt".to_string(),
69            "file2.txt".to_string(),
70        ];
71        let cli_args = get_cli_args(args).unwrap();
72        assert_eq!(
73            cli_args.input_files,
74            vec!["file1.txt".to_string(), "file2.txt".to_string()]
75        );
76    }
77
78    #[test]
79    fn test_parse_cli_args_with_single_file() {
80        let args = vec!["unshadow".to_string(), "file1.txt".to_string()];
81        let cli_args = get_cli_args(args).unwrap();
82        assert_eq!(cli_args.input_files, vec!["file1.txt".to_string()]);
83    }
84
85    #[test]
86    fn test_parse_cli_args_no_files() {
87        let args = vec!["unshadow".to_string()];
88        let result = get_cli_args(args);
89        assert!(result.is_err());
90        if let Err(WorkflowError::UserInput(msg)) = result {
91            assert_eq!(msg, "No input files provided");
92        } else {
93            panic!("Expected UserInput error");
94        }
95    }
96
97    #[test]
98    fn test_parse_cli_args_help() {
99        // Note: --help now causes the function to exit successfully, so this test is removed
100    }
101}