rpz

Response policy zone (RPZ) file generator.
git clone https://git.philomathiclife.com/repos/rpz
Log | Files | Refs | README

args.rs (18330B)


      1 use core::{
      2     error::Error,
      3     fmt::{self, Display, Formatter},
      4 };
      5 use rpz::file::AbsFilePath;
      6 use std::env::{self, Args};
      7 /// Error returned when parsing arguments passed to the application.
      8 #[expect(clippy::module_name_repetitions, reason = "prefer the name")]
      9 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
     10 pub enum ArgsErr {
     11     /// Error when no arguments were passed to the application.
     12     NoArgs,
     13     /// Error when `-f`/`--file` is not passed or is passed
     14     /// without a path to the file.
     15     ConfigPathNotPassed,
     16     /// Error when an invalid option is passed. The contained [`String`]
     17     /// is the value of the invalid option.
     18     InvalidOption(String),
     19     /// Some options when passed must be the only option passed.
     20     /// For such options, this is the error when other options are passed.
     21     MoreThanOneOption,
     22     /// Error when the passed path to the config file is not `-` nor an absolute file path to a file.
     23     InvalidConfigPath,
     24     /// Error when an option is passed more than once.
     25     DuplicateOption(&'static str),
     26     /// Error when the quiet and verbose options were passed.
     27     QuietAndVerbose,
     28 }
     29 impl Display for ArgsErr {
     30     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     31         match *self {
     32             Self::NoArgs => write!(f, "no arguments were passed, but at least two are required containing the option '-f' and its value which must be an absolute path to the config file"),
     33             Self::ConfigPathNotPassed => f.write_str("'-f' followed by '-' or the absolute file path to the config file was not passed"),
     34             Self::InvalidOption(ref arg) => write!(f, "{arg} is an invalid option. Only '-f'/'--file' followed by the absolute path to the config file, '-q'/'--quiet', '-h'/'--help', '-v'/'--verbose' and '-V'/'--version' are allowed"),
     35             Self::MoreThanOneOption => f.write_str("'-V'/'--version' or '-h'/'--help' was passed with other options; but when those options are passed, they must be the only one"),
     36             Self::InvalidConfigPath => write!(f, "an absolute file path to the config file or '-' was not passed"),
     37             Self::DuplicateOption(arg) => write!(f, "{arg} was passed more than once"),
     38             Self::QuietAndVerbose => f.write_str("'-q'/'--quiet' and '-v'/'--verbose' were both passed, but at most only one of them is allowed to be passed"),
     39         }
     40     }
     41 }
     42 impl Error for ArgsErr {}
     43 /// The location of the configuration file.
     44 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
     45 pub enum ConfigPath {
     46     /// The config file is to be read from `stdin`.
     47     Stdin,
     48     /// The config file resides on the local file system.
     49     Path(AbsFilePath<false>),
     50 }
     51 /// The options passed to the application.
     52 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
     53 pub enum Opts {
     54     /// Variant when no arguments were passed.
     55     None,
     56     /// Variant when the help argument was passed.
     57     Help,
     58     /// Variant when the version argument was passed.
     59     Version,
     60     /// Variant when the quiet argument was passed.
     61     Quiet,
     62     /// Variant when the verbose argument was passed.
     63     Verbose,
     64     /// Variant when the file argument with the path to the file was passed.
     65     Config(ConfigPath),
     66     /// Variant when the quiet argument and the file argument with the path to the file
     67     /// were passed.
     68     ConfigQuiet(ConfigPath),
     69     /// Variant when the verbose argument and the file argument with the path to the file
     70     /// were passed.
     71     ConfigVerbose(ConfigPath),
     72 }
     73 impl Opts {
     74     /// Returns `Opts` based on arguments passed to the application.
     75     #[expect(clippy::too_many_lines, reason = "this is fine")]
     76     pub fn from_args() -> Result<Self, ArgsErr> {
     77         /// Attempts to parse the next `Arg` into `-` or an absolute
     78         /// path to a file.
     79         fn get_path(args: &mut Args) -> Result<ConfigPath, ArgsErr> {
     80             args.next()
     81                 .map_or(Err(ArgsErr::ConfigPathNotPassed), |path| {
     82                     if path == "-" {
     83                         Ok(ConfigPath::Stdin)
     84                     } else {
     85                         AbsFilePath::<false>::from_string(path)
     86                             .map_or(Err(ArgsErr::InvalidConfigPath), |config| {
     87                                 Ok(ConfigPath::Path(config))
     88                             })
     89                     }
     90                 })
     91         }
     92         let mut args = env::args();
     93         if args.next().is_some() {
     94             let mut opts = Self::None;
     95             while let Some(arg) = args.next() {
     96                 match arg.as_str() {
     97                     "-h" | "--help" => match opts {
     98                         Self::None => {
     99                             opts = Self::Help;
    100                         }
    101                         Self::Help => return Err(ArgsErr::DuplicateOption("-h/--help")),
    102                         Self::Verbose
    103                         | Self::Quiet
    104                         | Self::Version
    105                         | Self::Config(_)
    106                         | Self::ConfigQuiet(_)
    107                         | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption),
    108                     },
    109                     "-V" | "--version" => match opts {
    110                         Self::None => {
    111                             opts = Self::Version;
    112                         }
    113                         Self::Version => return Err(ArgsErr::DuplicateOption("-V/--version")),
    114                         Self::Verbose
    115                         | Self::Quiet
    116                         | Self::Help
    117                         | Self::Config(_)
    118                         | Self::ConfigQuiet(_)
    119                         | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption),
    120                     },
    121                     "-f" | "--file" => match opts {
    122                         Self::None => {
    123                             opts = Self::Config(get_path(&mut args)?);
    124                         }
    125                         Self::Quiet => {
    126                             opts = Self::ConfigQuiet(get_path(&mut args)?);
    127                         }
    128                         Self::Verbose => {
    129                             opts = Self::ConfigVerbose(get_path(&mut args)?);
    130                         }
    131                         Self::Config(_) | Self::ConfigQuiet(_) | Self::ConfigVerbose(_) => {
    132                             return Err(ArgsErr::DuplicateOption("-f/--file"));
    133                         }
    134                         Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption),
    135                     },
    136                     "-q" | "--quiet" => match opts {
    137                         Self::None => {
    138                             opts = Self::Quiet;
    139                         }
    140                         Self::Config(path) => {
    141                             opts = Self::ConfigQuiet(path);
    142                         }
    143                         Self::Quiet | Self::ConfigQuiet(_) => {
    144                             return Err(ArgsErr::DuplicateOption("-q/--quiet"));
    145                         }
    146                         Self::Verbose | Self::ConfigVerbose(_) => {
    147                             return Err(ArgsErr::QuietAndVerbose)
    148                         }
    149                         Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption),
    150                     },
    151                     "-v" | "--verbose" => match opts {
    152                         Self::None => {
    153                             opts = Self::Verbose;
    154                         }
    155                         Self::Config(path) => {
    156                             opts = Self::ConfigVerbose(path);
    157                         }
    158                         Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose),
    159                         Self::Verbose | Self::ConfigVerbose(_) => {
    160                             return Err(ArgsErr::DuplicateOption("-v/--verbose"));
    161                         }
    162                         Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption),
    163                     },
    164                     "-fq" | "-qf" => match opts {
    165                         Self::None => {
    166                             opts = Self::ConfigQuiet(get_path(&mut args)?);
    167                         }
    168                         Self::Config(_) | Self::ConfigQuiet(_) => {
    169                             return Err(ArgsErr::DuplicateOption("-f/--file"))
    170                         }
    171                         Self::Quiet => return Err(ArgsErr::DuplicateOption("-q/--quiet")),
    172                         Self::Verbose | Self::ConfigVerbose(_) => {
    173                             return Err(ArgsErr::QuietAndVerbose)
    174                         }
    175                         Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption),
    176                     },
    177                     "-fv" | "-vf" => match opts {
    178                         Self::None => {
    179                             opts = Self::ConfigVerbose(get_path(&mut args)?);
    180                         }
    181                         Self::Config(_) | Self::ConfigVerbose(_) => {
    182                             return Err(ArgsErr::DuplicateOption("-f/--file"))
    183                         }
    184                         Self::Verbose => return Err(ArgsErr::DuplicateOption("-v/--verbose")),
    185                         Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose),
    186                         Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption),
    187                     },
    188                     _ => return Err(ArgsErr::InvalidOption(arg)),
    189                 }
    190             }
    191             Ok(opts)
    192         } else {
    193             Err(ArgsErr::NoArgs)
    194         }
    195     }
    196 }
    197 #[cfg(test)]
    198 mod tests {
    199     use crate::{test_prog, ArgsErr, E};
    200     use core::convert;
    201     use std::io::Write;
    202     use std::process::Stdio;
    203     use std::thread;
    204     #[test]
    205     #[ignore]
    206     fn test_args() {
    207         test_prog::verify_files();
    208         assert!(test_prog::get_command()
    209             .stderr(Stdio::piped())
    210             .stdin(Stdio::null())
    211             .stdout(Stdio::null())
    212             .output()
    213             .map_or(false, |output| {
    214                 !output.status.success()
    215                     && output.stderr
    216                         == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes()
    217             }));
    218         assert!(test_prog::get_command()
    219             .arg("-f")
    220             .stderr(Stdio::piped())
    221             .stdin(Stdio::null())
    222             .stdout(Stdio::null())
    223             .output()
    224             .map_or(false, |output| {
    225                 !output.status.success()
    226                     && output.stderr
    227                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    228                             .into_bytes()
    229             }));
    230         assert!(test_prog::get_command()
    231             .arg("-q")
    232             .stderr(Stdio::piped())
    233             .stdin(Stdio::null())
    234             .stdout(Stdio::null())
    235             .output()
    236             .map_or(false, |output| {
    237                 !output.status.success()
    238                     && output.stderr
    239                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    240                             .into_bytes()
    241             }));
    242         assert!(test_prog::get_command()
    243             .arg("-v")
    244             .stderr(Stdio::piped())
    245             .stdin(Stdio::null())
    246             .stdout(Stdio::null())
    247             .output()
    248             .map_or(false, |output| {
    249                 !output.status.success()
    250                     && output.stderr
    251                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    252                             .into_bytes()
    253             }));
    254         assert!(test_prog::get_command()
    255             .arg("-fq")
    256             .stderr(Stdio::piped())
    257             .stdin(Stdio::null())
    258             .stdout(Stdio::null())
    259             .output()
    260             .map_or(false, |output| {
    261                 !output.status.success()
    262                     && output.stderr
    263                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    264                             .into_bytes()
    265             }));
    266         assert!(test_prog::get_command()
    267             .arg("-qf")
    268             .stderr(Stdio::piped())
    269             .stdin(Stdio::null())
    270             .stdout(Stdio::null())
    271             .output()
    272             .map_or(false, |output| {
    273                 !output.status.success()
    274                     && output.stderr
    275                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    276                             .into_bytes()
    277             }));
    278         assert!(test_prog::get_command()
    279             .arg("-fv")
    280             .stderr(Stdio::piped())
    281             .stdin(Stdio::null())
    282             .stdout(Stdio::null())
    283             .output()
    284             .map_or(false, |output| {
    285                 !output.status.success()
    286                     && output.stderr
    287                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    288                             .into_bytes()
    289             }));
    290         assert!(test_prog::get_command()
    291             .arg("-vf")
    292             .stderr(Stdio::piped())
    293             .stdin(Stdio::null())
    294             .stdout(Stdio::null())
    295             .output()
    296             .map_or(false, |output| {
    297                 !output.status.success()
    298                     && output.stderr
    299                         == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    300                             .into_bytes()
    301             }));
    302         assert!(test_prog::get_command()
    303             .args(["-h", "-V"])
    304             .stderr(Stdio::piped())
    305             .stdin(Stdio::null())
    306             .stdout(Stdio::null())
    307             .output()
    308             .map_or(false, |output| {
    309                 !output.status.success()
    310                     && output.stderr
    311                         == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption))
    312                             .into_bytes()
    313             }));
    314         assert!(test_prog::get_command()
    315             .args(["-h", "-h"])
    316             .stderr(Stdio::piped())
    317             .stdin(Stdio::null())
    318             .stdout(Stdio::null())
    319             .output()
    320             .map_or(false, |output| {
    321                 !output.status.success()
    322                     && output.stderr
    323                         == format!(
    324                             "Error: {:?}\n",
    325                             E::Args(ArgsErr::DuplicateOption("-h/--help"))
    326                         )
    327                         .into_bytes()
    328             }));
    329         assert!(test_prog::get_command()
    330             .args(["-f", "/home/zack/foo", "-V"])
    331             .stderr(Stdio::piped())
    332             .stdin(Stdio::null())
    333             .stdout(Stdio::null())
    334             .output()
    335             .map_or(false, |output| {
    336                 !output.status.success()
    337                     && output.stderr
    338                         == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption))
    339                             .into_bytes()
    340             }));
    341         assert!(test_prog::get_command()
    342             .args(["-f", "home/zack/foo"])
    343             .stderr(Stdio::piped())
    344             .stdin(Stdio::null())
    345             .stdout(Stdio::null())
    346             .output()
    347             .map_or(false, |output| {
    348                 !output.status.success()
    349                     && output.stderr
    350                         == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath))
    351                             .into_bytes()
    352             }));
    353         assert!(test_prog::get_command()
    354             .args(["-f", "/home/zack/foo/"])
    355             .stderr(Stdio::piped())
    356             .stdin(Stdio::null())
    357             .stdout(Stdio::null())
    358             .output()
    359             .map_or(false, |output| {
    360                 !output.status.success()
    361                     && output.stderr
    362                         == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath))
    363                             .into_bytes()
    364             }));
    365         assert!(test_prog::get_command()
    366             .arg("-foo")
    367             .stderr(Stdio::piped())
    368             .stdin(Stdio::null())
    369             .stdout(Stdio::null())
    370             .output()
    371             .map_or(false, |output| {
    372                 !output.status.success()
    373                     && output.stderr
    374                         == format!(
    375                             "Error: {:?}\n",
    376                             E::Args(ArgsErr::InvalidOption(String::from("-foo")))
    377                         )
    378                         .into_bytes()
    379             }));
    380         assert!(test_prog::get_command()
    381             .args(["-f", "-"])
    382             .stderr(Stdio::piped())
    383             .stdin(Stdio::null())
    384             .stdout(Stdio::null())
    385             .output()
    386             .map_or(false, |output| !output.status.success()
    387                 && output.stderr.get(..23) == Some(b"Error: TOML parse error")));
    388         assert!(test_prog::get_command()
    389             .args(["-f", "-"])
    390             .stderr(Stdio::piped())
    391             .stdin(Stdio::piped())
    392             .stdout(Stdio::null())
    393             .spawn()
    394             .map_or(false, |mut cmd| {
    395                 cmd.stdin.take().map_or(false, |mut stdin| {
    396                     thread::spawn(move || {
    397                         stdin
    398                             .write_all(b"junk")
    399                             .map_or(false, |_| stdin.flush().map_or(false, |_| true))
    400                     })
    401                     .join()
    402                     .map_or(false, convert::identity)
    403                 }) && cmd.wait_with_output().map_or(false, |output| {
    404                     !output.status.success()
    405                         && output.stderr.get(..23) == Some(b"Error: TOML parse error")
    406                 })
    407             }));
    408         assert!(test_prog::get_command()
    409             .arg("-h")
    410             .stderr(Stdio::null())
    411             .stdin(Stdio::null())
    412             .stdout(Stdio::piped())
    413             .output()
    414             .map_or(false, |output| output.status.success()
    415                 && output.stdout.get(..crate::HELP.len())
    416                     == Some(crate::HELP.as_bytes())));
    417         assert!(test_prog::get_command()
    418             .arg("-V")
    419             .stderr(Stdio::null())
    420             .stdin(Stdio::null())
    421             .stdout(Stdio::piped())
    422             .output()
    423             .map_or(false, |output| output.status.success()
    424                 && output.stdout.get(..crate::VERSION.len())
    425                     == Some(crate::VERSION.as_bytes())));
    426     }
    427 }