// SPDX-License-Identifier: MIT // // Copyright IBM Corp. 2024 use std::{env, fmt::Display, path::PathBuf}; use clap::{ArgGroup, Args, Command, CommandFactory, Parser, ValueEnum, ValueHint}; use log::warn; use utils::{CertificateOptions, DeprecatedVerbosityOptions}; /// Create and inspect IBM Secure Execution images. /// /// Use pvimg to create an IBM Secure Execution image, which can be loaded using /// zipl or QEMU. pvimg can also be used to inspect existing Secure Execution /// images. #[derive(Parser, Debug)] #[command()] pub struct CliOptions { #[clap(flatten)] pub verbose: DeprecatedVerbosityOptions, /// Print version information and exit. // Implemented for the help message only. Actual parsing happens in the // version command. #[arg(long)] pub version: bool, #[command(subcommand)] pub cmd: SubCommands, } impl From for CliOptions { fn from(value: GenprotimgCliOptions) -> Self { Self { verbose: value.verbose, version: false, cmd: SubCommands::Create(value.args), } } } impl CliOptions { pub fn new_version_cmd_opts() -> Self { Self { verbose: DeprecatedVerbosityOptions::default(), version: true, cmd: SubCommands::Version, } } } /// Validates the given command line options. /// /// # Errors /// /// This function will return an error if an argument is missing. pub fn validate_cli(opts: &CliOptions) -> Result<(), clap::error::Error> { match &opts.cmd { SubCommands::Create(create_opts) => { if let Some(dir) = create_opts .experimental_args .x_bootloader_directory .as_ref() { warn!("Use bootloader directory: {}", dir.display()); } Ok(()) } _ => Ok(()), } } /// CLI Argument collection for handling input components. #[derive(Args, Debug)] pub struct ComponentPaths { /// Use the content of FILE as a raw binary Linux kernel. /// /// The Linux kernel must be a raw binary s390x Linux kernel. The ELF format /// is not supported. #[arg(short='i', long = "kernel", value_name = "FILE", value_hint = ValueHint::FilePath, visible_alias = "image")] pub kernel: PathBuf, /// Use the content of FILE as the Linux initial RAM disk. #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)] pub ramdisk: Option, /// Use the content of FILE as the Linux kernel command line. /// /// The Linux kernel command line must be shorter than the maximum kernel /// command line size supported by the given Linux kernel. #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)] pub parmfile: Option, } #[derive(Args, Debug)] #[command(group(ArgGroup::new("header-flags").multiple(true).conflicts_with_all(["x_pcf", "x_scf"])))] pub struct CreateBootImageLegacyFlags { /// Enable Secure Execution guest dump support. This option requires the /// '--cck' option. #[arg(long, action = clap::ArgAction::SetTrue, requires="cck", group="header-flags")] pub enable_dump: Option, /// Disable Secure Execution guest dump support (default). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_dump", group="header-flags")] pub disable_dump: Option, /// Add-secret requests must provide an extension secret that matches the /// CCK-derived extension secret. This option requires the '--cck' /// option. #[arg(long, action = clap::ArgAction::SetTrue, requires="cck", group="header-flags")] pub enable_cck_extension_secret: Option, /// Add-secret requests don't have to provide the CCK-derived extension /// secret (default). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_cck_extension_secret", group="header-flags")] pub disable_cck_extension_secret: Option, /// Enable the support for the DEA, TDEA, AES, and ECC PCKMO key encryption /// functions (default). #[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")] pub enable_pckmo: Option, /// Disable the support for the DEA, TDEA, AES, and ECC PCKMO key encryption /// functions. #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_pckmo", group="header-flags")] pub disable_pckmo: Option, /// Enable the support for the HMAC PCKMO key encryption function. #[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")] pub enable_pckmo_hmac: Option, /// Disable the support for the HMAC PCKMO key encryption function (default). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_pckmo_hmac", group="header-flags")] pub disable_pckmo_hmac: Option, /// Enable the support for backup target keys. #[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")] pub enable_backup_keys: Option, /// Disable the support for backup target keys (default). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_backup_keys", group="header-flags")] pub disable_backup_keys: Option, /// Enable encryption of the image components (default). /// /// The image components are: the kernel, ramdisk, and kernel command line. #[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")] pub enable_image_encryption: Option, /// Disable encryption of the image components. /// /// The image components are: the kernel, ramdisk, and kernel command line. /// Use only if the components used do not contain any confidential content /// (for example, secrets like non-public cryptographic keys). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_image_encryption", group="header-flags")] pub disable_image_encryption: Option, } #[non_exhaustive] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] pub enum OutputFormat { /// JSON format. Json, } impl Display for OutputFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::Json => "JSON", } ) } } #[derive(Args, Debug)] pub struct SeImgInputArgs { /// Use INPUT as the Secure Execution image. #[arg(value_name = "INPUT", value_hint = ValueHint::FilePath,)] pub path: PathBuf, } #[derive(Args, Debug)] pub struct InfoArgs { #[clap(flatten)] pub input: SeImgInputArgs, /// The output format #[arg(long, value_enum)] pub format: OutputFormat, /// Use the key in FILE to decrypt the Secure Execution header. /// /// It is the key that was specified with the command line option /// '--hdr-key' at the Secure Execution image creation. #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath, alias = "key")] pub hdr_key: Option, } #[derive(Args, Debug)] #[command(group(ArgGroup::new("test-args").multiple(true).required(true)))] pub struct TestArgs { #[clap(flatten)] pub input: SeImgInputArgs, /// Use FILE to check for a host key document. /// /// Verifies that the image contains the host key hash of one of the /// specified host keys. The check fails if none of the host keys match the /// hash in the image. This parameter can be specified multiple times. /// Mutually exclusive with '--key-hashes'. #[arg( short = 'k', long = "host-key-document", value_name = "FILE", value_hint = ValueHint::FilePath, use_value_delimiter = true, value_delimiter = ',', group = "test-args", )] pub host_key_documents: Vec, /// Use FILE to check for the host key hashes provided by the ultravisor. If /// no FILE is specified, FILE defaults to '/sys/firmware/uv/keys/all'. /// /// The default file is only available if the local system supports the /// Query Ultravisor Keys UVC. Verifies that the image contains the host key /// hash of one of the specified hashes in FILE. The check fails if none of /// the host keys match a hash in the response. Mutually exclusive with /// '--host-key-document'. #[arg( long = "key-hashes", value_name = "FILE", value_hint = ValueHint::FilePath, num_args = 0..=1, require_equals = true, default_missing_value = "/sys/firmware/uv/keys/all", conflicts_with="host_key_documents", group = "test-args", )] pub key_hashes: Option, } /// Create an IBM Secure Execution image. /// /// Create a new IBM Secure Execution image. Only create these images in a /// trusted environment, such as your workstation. The 'genprotimg' command /// creates randomly generated keys to protect the image. The generated image /// can then be booted on an IBM Secure Execution system as a KVM guest. /// /// Note: The 'genprotimg' command is a symbolic link to the 'pvimg create' /// command. #[derive(Parser, Debug)] pub struct GenprotimgCliOptions { #[clap(flatten)] pub args: Box, #[clap(flatten)] pub verbose: DeprecatedVerbosityOptions, /// Print version information and exit. // Implemented for the help message only. Actual parsing happens in the // version command. #[arg(long, action = clap::ArgAction::SetTrue )] pub version: (), #[arg(long, action = clap::ArgAction::HelpLong, hide(true))] /// Print help (deprecated, use '--help' instead). help_all: (), #[arg(long, action = clap::ArgAction::HelpLong, hide(true))] /// Print help (deprecated, use '--help' instead). help_experimental: (), } impl GenprotimgCliOptions { pub fn command() -> Command { let cmd = ::command(); // Make sure that the correct binary is shown in the clap error // messages. cmd.bin_name("genprotimg") } pub fn own_parse() -> CliOptions { let args = env::args_os(); let args_len = args.len(); let version_count = args.filter(|value| value == "--version").count(); if version_count > 1 || version_count == 1 && (args_len != version_count + 1) { Self::command() .error( clap::error::ErrorKind::UnknownArgument, "unexpected argument", ) .exit() } if version_count == 1 { CliOptions::new_version_cmd_opts() } else { let genprotimg_opts = Self::parse(); genprotimg_opts.into() } } } #[derive(Parser, Debug)] pub struct CreateBootImageArgs { #[clap(flatten)] pub component_paths: ComponentPaths, /// Write the generated Secure Execution boot image to FILE. #[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)] pub output: PathBuf, #[clap(flatten)] pub certificate_args: CertificateOptions, /// Disable all input component checks. /// /// For example, for the Linux kernel, it tests if the given kernel looks /// like a raw binary s390x kernel. #[arg(long)] pub no_component_check: bool, /// Overwrite an existing Secure Execution boot image. #[arg(long)] pub overwrite: bool, /// Use the content of FILE as the customer-communication key (CCK). /// /// The file must contain exactly 32 bytes of data. #[arg(long, value_name = "FILE", visible_alias = "comm-key")] pub cck: Option, /// Use the content of FILE as the Secure Execution header protection key. /// /// The file must contain exactly 32 bytes of data. If the option is not /// specified, the Secure Execution header protection key is a randomly /// generated key. #[arg(long, value_name = "FILE", alias = "x-header-key")] pub hdr_key: Option, #[clap(flatten)] pub legacy_flags: CreateBootImageLegacyFlags, #[clap(flatten)] pub experimental_args: CreateBootImageExperimentalArgs, } /// Experimental options #[derive(Args, Debug)] pub struct CreateBootImageExperimentalArgs { /// Manually set the directory used to load the Secure Execution bootloaders /// (stage3a and stage3b) (experimental option). // Hidden in user documentation. #[arg(long, value_name = "DIR", hide(true))] pub x_bootloader_directory: Option, /// Manually set the image components encryption key (experimental option). // Hidden in user documentation. #[arg(long, value_name = "FILE", hide(true))] pub x_comp_key: Option, /// Manually set the PSW address used for the Secure Execution header (experimental option). // Hidden in user documentation. #[arg(long, value_name = "ADDRESS", hide(true))] pub x_psw: Option, /// Manually set the plaintext control flags (experimental option). // No validity checks made. Hidden in user documentation. #[arg(long, value_name = "PCF", hide(true))] pub x_pcf: Option, /// Manually set the secret control flags (experimental option). // No validity checks made. Hidden in user documentation. #[arg(long, value_name = "SCF", hide(true))] pub x_scf: Option, } #[derive(Debug, clap::Subcommand)] pub enum SubCommands { /// Create an IBM Secure Execution image. /// /// Create a new IBM Secure Execution image. Only create these images in a /// trusted environment, such as your workstation. The 'pvimg create' /// command creates randomly generated keys to protect the image. The /// generated image can then be booted on an IBM Secure Execution system as /// a KVM guest. Create(Box), /// Print information about the IBM Secure Execution image. /// /// Note that the API and output format is experimental and subject to /// change. Info(InfoArgs), /// Test different aspects of an existing IBM Secure Execution image. Test(Box), /// Print version information and exit. #[command(aliases(["--version"]), hide(true))] Version, } #[allow(clippy::shadow_unrelated)] #[cfg(test)] mod test { use std::collections::BTreeMap; use super::*; #[derive(Hash, Eq, PartialEq, Debug, Clone)] struct CliOption { name: String, args: Vec, } impl CliOption { fn new, T: AsRef, P: AsRef<[S]>>(name: T, args: P) -> Self { let name = name.as_ref().to_owned(); let args = args .as_ref() .iter() .map(|v| v.as_ref().to_owned()) .collect(); Self { name, args } } } impl From for Vec { fn from(val: CliOption) -> Self { let CliOption { args, .. } = val; args } } fn flat_map_collect(map: BTreeMap) -> Vec { map.into_values().flat_map(|v| v.args).collect() } fn insert( mut map: BTreeMap, values: Vec, ) -> BTreeMap { for value in values { map.insert(value.name.to_owned(), value); } map } fn remove>( mut map: BTreeMap, key: S, ) -> BTreeMap { map.remove(key.as_ref()); map } #[test] #[rustfmt::skip] fn genprotimg_and_pvimg_create_args() { // Minimal valid create arguments using no-verify let mut mvcanv = BTreeMap::new(); mvcanv = insert(mvcanv, vec![CliOption::new("image", ["--image", "/dev/null"])]); mvcanv = insert(mvcanv, vec![CliOption::new("hkd", ["--host-key-document", "/dev/null"])]); mvcanv = insert(mvcanv, vec![CliOption::new("output", ["--output", "/dev/null"])]); mvcanv = insert(mvcanv, vec![CliOption::new("no-verify", ["--no-verify"])]); // Minimal valid create arguments using --cert let mut mvca = mvcanv.clone(); mvca.remove("no-verify"); mvca = insert(mvca, vec![CliOption::new("cert", ["--cert", "/dev/null"])]); let valid_create_args = [ flat_map_collect(mvcanv.clone()), flat_map_collect(insert(remove(mvcanv.clone(), "image"), vec![CliOption::new("kernel", ["--kernel", "/dev/kernel"])])), flat_map_collect(insert(mvcanv.clone(), vec![CliOption::new("root-ca", ["--root-ca", "/dev/null"])])), flat_map_collect(mvca.clone()), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("quiet", ["-q"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("verbose", ["-vvv"])])), // Verify the old verbosity is still working. flat_map_collect(insert(mvca.clone(), vec![CliOption::new("verbose", ["-VVV"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("offline", ["--offline"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("ramdisk", ["--ramdisk", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("parmfile", ["--parmfile", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"]), CliOption::new("comm-key", ["--comm-key", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"]), CliOption::new("comm-key", ["--cck", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"]), CliOption::new("comm-key", ["--comm-key", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-pcf", ["--x-pcf", "0x0"]), CliOption::new("x-scf", ["--x-scf", "0x0"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-psw", ["--x-psw", "0x0"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("no-component-check", ["--no-component-check"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-pckmo", ["--enable-pckmo"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-pckmo-hmac", ["--enable-pckmo-hmac"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-backup-keys", ["--enable-backup-keys"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-image-encryption", ["--disable-image-encryption"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-image-encryption", ["--enable-image-encryption"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-header-key", ["--x-header-key", "/dev/null"]),])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-header-key", ["--hdr-key", "/dev/null"]),])), ]; let invalid_create_args = [ flat_map_collect(remove(mvcanv.clone(), "no-verify")), flat_map_collect(remove(mvcanv.clone(), "image")), flat_map_collect(remove(mvcanv.clone(), "hkd")), flat_map_collect(remove(mvcanv, "output")), // missing `--comm-key` flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"])])), // -v and -q cannot be combined flat_map_collect(insert(mvca.clone(), vec![ CliOption::new("verbose", ["-v"]), CliOption::new("quiet", ["-q"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("image2", ["--image", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("output2", ["--output", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("ramdisk", ["--ramdisk", "/dev/null"]), CliOption::new("ramdisk2", ["--ramdisk", "/dev/null"]) ])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("parmfile", ["--parmfile", "/dev/null"]), CliOption::new("parmfile2", ["--parmfile", "/dev/null"]) ])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-pcf", ["--x-pcf", "0x0"]), CliOption::new("x-pcf2", ["--x-pcf", "0x0"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-pckmo", ["--enable-pckmo"]), CliOption::new("disable-pckmo", ["--disable-pckmo"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-image-encryption", ["--enable-image-encryption"]), CliOption::new("disable-image-encryption", ["--disable-image-encryption"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-header-key", ["--hdr-key"]),])), ]; let mut genprotimg_valid_args = vec![ // See workaround `parse_version` in `pvimg/main.rs`. // vec!["genprotimg", "--version"], ]; let mut pvimg_valid_args = vec![ vec!["pvimg", "--version"], vec!["pvimg", "version"], ]; // Test for invalid combinations let mut genprotimg_invalid_args = vec![ vec!["genprotimg"], ]; let mut pvimg_invalid_args = vec![ vec!["pvimg"], ]; // Test that `genprotimg` and `pvimg create` behave equally. for create_args in &valid_create_args { genprotimg_valid_args.push([["genprotimg"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat()); pvimg_valid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat()); } for invalid_create_args in &invalid_create_args { genprotimg_invalid_args.push([["genprotimg"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat()); pvimg_invalid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat()); } for arg in pvimg_valid_args { let res = CliOptions::try_parse_from(&arg); #[allow(clippy::use_debug, clippy::print_stdout)] if let Err(e) = &res { println!("arg: {arg:?}"); println!("{e}"); } assert!(res.is_ok()); } for arg in pvimg_invalid_args { let res = CliOptions::try_parse_from(&arg); assert!(res.is_err()); } for arg in genprotimg_valid_args { let res = GenprotimgCliOptions::try_parse_from(&arg); #[allow(clippy::use_debug, clippy::print_stdout)] if let Err(e) = &res { println!("arg: {arg:?}"); println!("{e}"); } assert!(res.is_ok()); } for arg in genprotimg_invalid_args { let res = GenprotimgCliOptions::try_parse_from(&arg); assert!(res.is_err()); } } #[test] fn pvimg_test_cli() { let args = BTreeMap::new(); let valid_test_args = [ flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes", ["--key-hashes"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]), CliOption::new("image", ["/dev/null"]), // global works CliOption::new("quiet", ["-q"]), ], )), // separation between keyword and positional args works flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]), CliOption::new("image", ["--", "/dev/null"]), ], )), // Verify that the old verbosity is still working. flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]), CliOption::new("image", ["/dev/null"]), CliOption::new("verbose", ["-VVV"]), ], )), ]; let invalid_test_args = [ flat_map_collect(insert( args.clone(), vec![CliOption::new("image", ["/dev/null"])], )), // the argument '--key-hashes[=]' cannot be used with '--host-key-document // ' flat_map_collect(insert( args.clone(), vec![ CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]), CliOption::new("host-key-document", ["--host-key-document", "/dev/null"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args, vec![ CliOption::new("host-key-hashes2", ["--key-hashes", "/sys/null"]), CliOption::new("image", ["--", "/dev/null"]), ], )), ]; let mut pvimg_valid_args = vec![]; // Test for invalid combinations // Input is missing let mut pvimg_invalid_args = vec![vec!["pvimg", "test"]]; for create_args in &valid_test_args { pvimg_valid_args.push( [ ["pvimg", "test"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str)), ] .concat(), ); } for invalid_test_arg in &invalid_test_args { pvimg_invalid_args.push( [ ["pvimg", "test"].to_vec(), Vec::from_iter(invalid_test_arg.iter().map(String::as_str)), ] .concat(), ); } for arg in pvimg_valid_args { let res = CliOptions::try_parse_from(&arg); #[allow(clippy::use_debug, clippy::print_stdout)] if let Err(e) = &res { println!("arg: {arg:?}"); println!("{e}"); } assert!(res.is_ok()); } for arg in pvimg_invalid_args { let res = CliOptions::try_parse_from(&arg); assert!(res.is_err()); } } #[test] fn pvimg_info_cli() { let args = BTreeMap::new(); let valid_test_args = [ flat_map_collect(insert( args.clone(), vec![ CliOption::new("format", ["--format", "json"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("format", ["--format=json"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("hdr-key", ["--hdr-key", "/dev/null"]), CliOption::new("format", ["--format=json"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("hdr-key", ["--key", "/dev/null"]), CliOption::new("format", ["--format=json"]), CliOption::new("image", ["/dev/null"]), ], )), // separation between keyword and positional args works flat_map_collect(insert( args.clone(), vec![ CliOption::new("format", ["--format=json"]), CliOption::new("image", ["--", "/dev/null"]), ], )), // Verify that the old verbosity is still working. flat_map_collect(insert( args.clone(), vec![ CliOption::new("format", ["--format=json"]), CliOption::new("image", ["/dev/null"]), CliOption::new("verbose", ["-VVV"]), ], )), ]; let invalid_test_args = [ // the argument '--key-hashes[=]' cannot be used with '--host-key-document // ' flat_map_collect(insert( args.clone(), vec![CliOption::new("image", ["/dev/null"])], )), // No default defined for --format flat_map_collect(insert( args, vec![ CliOption::new("format", ["--format"]), CliOption::new("image", ["--", "/dev/null"]), ], )), ]; let mut pvimg_valid_args = vec![]; // Test for invalid combinations // Input is missing let mut pvimg_invalid_args = vec![vec!["pvimg", "info"]]; for create_args in &valid_test_args { pvimg_valid_args.push( [ ["pvimg", "info"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str)), ] .concat(), ); } for invalid_test_arg in &invalid_test_args { pvimg_invalid_args.push( [ ["pvimg", "info"].to_vec(), Vec::from_iter(invalid_test_arg.iter().map(String::as_str)), ] .concat(), ); } for arg in pvimg_valid_args { let res = CliOptions::try_parse_from(&arg); #[allow(clippy::use_debug, clippy::print_stdout)] if let Err(e) = &res { println!("arg: {arg:?}"); println!("{e}"); } assert!(res.is_ok()); } for arg in pvimg_invalid_args { let res = CliOptions::try_parse_from(&arg); assert!(res.is_err()); } } #[test] fn verify_cli() { use clap::CommandFactory; CliOptions::command().debug_assert(); } }