rpz

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

args.rs (19443B)


      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(crate) 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(crate) 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(crate) 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(crate) 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 as _;
    201     use std::process::Stdio;
    202     use std::thread;
    203     #[expect(clippy::too_many_lines, reason = "a lot to test")]
    204     #[test]
    205     #[ignore = "requires I/O"]
    206     fn args() {
    207         test_prog::verify_files();
    208         assert!(
    209             test_prog::get_command()
    210                 .stderr(Stdio::piped())
    211                 .stdin(Stdio::null())
    212                 .stdout(Stdio::null())
    213                 .output()
    214                 .is_ok_and(|output| {
    215                     !output.status.success()
    216                         && output.stderr
    217                             == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes()
    218                 })
    219         );
    220         assert!(
    221             test_prog::get_command()
    222                 .arg("-f")
    223                 .stderr(Stdio::piped())
    224                 .stdin(Stdio::null())
    225                 .stdout(Stdio::null())
    226                 .output()
    227                 .is_ok_and(|output| {
    228                     !output.status.success()
    229                         && output.stderr
    230                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    231                                 .into_bytes()
    232                 })
    233         );
    234         assert!(
    235             test_prog::get_command()
    236                 .arg("-q")
    237                 .stderr(Stdio::piped())
    238                 .stdin(Stdio::null())
    239                 .stdout(Stdio::null())
    240                 .output()
    241                 .is_ok_and(|output| {
    242                     !output.status.success()
    243                         && output.stderr
    244                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    245                                 .into_bytes()
    246                 })
    247         );
    248         assert!(
    249             test_prog::get_command()
    250                 .arg("-v")
    251                 .stderr(Stdio::piped())
    252                 .stdin(Stdio::null())
    253                 .stdout(Stdio::null())
    254                 .output()
    255                 .is_ok_and(|output| {
    256                     !output.status.success()
    257                         && output.stderr
    258                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    259                                 .into_bytes()
    260                 })
    261         );
    262         assert!(
    263             test_prog::get_command()
    264                 .arg("-fq")
    265                 .stderr(Stdio::piped())
    266                 .stdin(Stdio::null())
    267                 .stdout(Stdio::null())
    268                 .output()
    269                 .is_ok_and(|output| {
    270                     !output.status.success()
    271                         && output.stderr
    272                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    273                                 .into_bytes()
    274                 })
    275         );
    276         assert!(
    277             test_prog::get_command()
    278                 .arg("-qf")
    279                 .stderr(Stdio::piped())
    280                 .stdin(Stdio::null())
    281                 .stdout(Stdio::null())
    282                 .output()
    283                 .is_ok_and(|output| {
    284                     !output.status.success()
    285                         && output.stderr
    286                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    287                                 .into_bytes()
    288                 })
    289         );
    290         assert!(
    291             test_prog::get_command()
    292                 .arg("-fv")
    293                 .stderr(Stdio::piped())
    294                 .stdin(Stdio::null())
    295                 .stdout(Stdio::null())
    296                 .output()
    297                 .is_ok_and(|output| {
    298                     !output.status.success()
    299                         && output.stderr
    300                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    301                                 .into_bytes()
    302                 })
    303         );
    304         assert!(
    305             test_prog::get_command()
    306                 .arg("-vf")
    307                 .stderr(Stdio::piped())
    308                 .stdin(Stdio::null())
    309                 .stdout(Stdio::null())
    310                 .output()
    311                 .is_ok_and(|output| {
    312                     !output.status.success()
    313                         && output.stderr
    314                             == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed))
    315                                 .into_bytes()
    316                 })
    317         );
    318         assert!(
    319             test_prog::get_command()
    320                 .args(["-h", "-V"])
    321                 .stderr(Stdio::piped())
    322                 .stdin(Stdio::null())
    323                 .stdout(Stdio::null())
    324                 .output()
    325                 .is_ok_and(|output| {
    326                     !output.status.success()
    327                         && output.stderr
    328                             == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption))
    329                                 .into_bytes()
    330                 })
    331         );
    332         assert!(
    333             test_prog::get_command()
    334                 .args(["-h", "-h"])
    335                 .stderr(Stdio::piped())
    336                 .stdin(Stdio::null())
    337                 .stdout(Stdio::null())
    338                 .output()
    339                 .is_ok_and(|output| {
    340                     !output.status.success()
    341                         && output.stderr
    342                             == format!(
    343                                 "Error: {:?}\n",
    344                                 E::Args(ArgsErr::DuplicateOption("-h/--help"))
    345                             )
    346                             .into_bytes()
    347                 })
    348         );
    349         assert!(
    350             test_prog::get_command()
    351                 .args(["-f", "/home/zack/foo", "-V"])
    352                 .stderr(Stdio::piped())
    353                 .stdin(Stdio::null())
    354                 .stdout(Stdio::null())
    355                 .output()
    356                 .is_ok_and(|output| {
    357                     !output.status.success()
    358                         && output.stderr
    359                             == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption))
    360                                 .into_bytes()
    361                 })
    362         );
    363         assert!(
    364             test_prog::get_command()
    365                 .args(["-f", "home/zack/foo"])
    366                 .stderr(Stdio::piped())
    367                 .stdin(Stdio::null())
    368                 .stdout(Stdio::null())
    369                 .output()
    370                 .is_ok_and(|output| {
    371                     !output.status.success()
    372                         && output.stderr
    373                             == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath))
    374                                 .into_bytes()
    375                 })
    376         );
    377         assert!(
    378             test_prog::get_command()
    379                 .args(["-f", "/home/zack/foo/"])
    380                 .stderr(Stdio::piped())
    381                 .stdin(Stdio::null())
    382                 .stdout(Stdio::null())
    383                 .output()
    384                 .is_ok_and(|output| {
    385                     !output.status.success()
    386                         && output.stderr
    387                             == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath))
    388                                 .into_bytes()
    389                 })
    390         );
    391         assert!(
    392             test_prog::get_command()
    393                 .arg("-foo")
    394                 .stderr(Stdio::piped())
    395                 .stdin(Stdio::null())
    396                 .stdout(Stdio::null())
    397                 .output()
    398                 .is_ok_and(|output| {
    399                     !output.status.success()
    400                         && output.stderr
    401                             == format!(
    402                                 "Error: {:?}\n",
    403                                 E::Args(ArgsErr::InvalidOption(String::from("-foo")))
    404                             )
    405                             .into_bytes()
    406                 })
    407         );
    408         assert!(
    409             test_prog::get_command()
    410                 .args(["-f", "-"])
    411                 .stderr(Stdio::piped())
    412                 .stdin(Stdio::null())
    413                 .stdout(Stdio::null())
    414                 .output()
    415                 .is_ok_and(|output| !output.status.success()
    416                     && output.stderr.get(..23) == Some(b"Error: TOML parse error"))
    417         );
    418         assert!(
    419             test_prog::get_command()
    420                 .args(["-f", "-"])
    421                 .stderr(Stdio::piped())
    422                 .stdin(Stdio::piped())
    423                 .stdout(Stdio::null())
    424                 .spawn()
    425                 .is_ok_and(|mut cmd| {
    426                     cmd.stdin.take().is_some_and(|mut stdin| {
    427                         thread::spawn(move || {
    428                             stdin
    429                                 .write_all(b"junk")
    430                                 .is_ok_and(|()| stdin.flush().is_ok())
    431                         })
    432                         .join()
    433                         .is_ok_and(convert::identity)
    434                     }) && cmd.wait_with_output().is_ok_and(|output| {
    435                         !output.status.success()
    436                             && output.stderr.get(..23) == Some(b"Error: TOML parse error")
    437                     })
    438                 })
    439         );
    440         assert!(
    441             test_prog::get_command()
    442                 .arg("-h")
    443                 .stderr(Stdio::null())
    444                 .stdin(Stdio::null())
    445                 .stdout(Stdio::piped())
    446                 .output()
    447                 .is_ok_and(|output| output.status.success()
    448                     && output.stdout.get(..crate::HELP.len()) == 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                 .is_ok_and(|output| output.status.success()
    458                     && output.stdout.get(..crate::VERSION.len())
    459                         == Some(crate::VERSION.as_bytes()))
    460         );
    461     }
    462 }