rpz

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

args.rs (19459B)


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