Skip to main content

shadow_crypt_shell/listing/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, error::ErrorKind};
4
5use crate::errors::WorkflowError;
6
7/// Listing CLI arguments structure
8#[derive(Debug, Clone, Default, Parser)]
9#[command(
10    name = "shadows",
11    about = "List shadow files in a directory",
12    version,
13    after_help = "By default the original filenames are decrypted (prompts for the \
14                  password and runs the full key derivation for every file); files the \
15                  password does not open are still listed by their obfuscated names. \
16                  With --no-names, no password is needed and only the plaintext header \
17                  metadata is shown."
18)]
19pub struct ListingCliArgs {
20    /// Directory to list (defaults to the current directory)
21    #[arg(value_name = "DIR")]
22    pub dir: Option<PathBuf>,
23
24    /// Skip filename decryption; list plaintext header metadata only (no password needed)
25    #[arg(long = "no-names", conflicts_with = "password_file")]
26    pub no_names: bool,
27
28    /// Read the password from this file instead of prompting
29    #[arg(long = "password-file", value_name = "FILE")]
30    pub password_file: Option<PathBuf>,
31
32    /// Print the listing as JSON (original_filename is null when not decrypted)
33    #[arg(long = "json")]
34    pub json: bool,
35}
36
37/// Parse listing command line arguments
38pub fn get_cli_args(args: Vec<String>) -> Result<ListingCliArgs, WorkflowError> {
39    ListingCliArgs::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() == ErrorKind::DisplayHelp || e.kind() == ErrorKind::DisplayVersion {
42            // Print the message and exit successfully
43            eprintln!("{}", e);
44            std::process::exit(0);
45        }
46        WorkflowError::UserInput(e.to_string())
47    })
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_defaults_to_names_and_no_dir() {
56        let args = get_cli_args(vec!["shadows".to_string()]).unwrap();
57        assert!(!args.no_names);
58        assert!(args.dir.is_none());
59    }
60
61    #[test]
62    fn test_parses_dir_and_no_names() {
63        let args = get_cli_args(vec![
64            "shadows".to_string(),
65            "--no-names".to_string(),
66            "/some/dir".to_string(),
67        ])
68        .unwrap();
69        assert!(args.no_names);
70        assert_eq!(args.dir, Some(PathBuf::from("/some/dir")));
71    }
72
73    #[test]
74    fn test_no_names_conflicts_with_password_file() {
75        let result = get_cli_args(vec![
76            "shadows".to_string(),
77            "--no-names".to_string(),
78            "--password-file".to_string(),
79            "pw.txt".to_string(),
80        ]);
81        assert!(result.is_err());
82    }
83}