// SPDX-License-Identifier: MIT // // Copyright IBM Corp. 2024 use std::fmt::Display; use std::str::FromStr; use std::string::ToString; use std::{env, ffi::OsStr, io::IsTerminal, path::PathBuf}; use clap::{ builder::{PossibleValue, TypedValueParser}, Arg, 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)] #[cfg_attr(test, derive(Default))] 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, } /// CLI Argument collection for handling user-provided keys. #[derive(Args, Debug)] #[cfg_attr(test, derive(Default))] pub struct UserKeys { /// Use the content of FILE as the customer-communication key (CCK). /// /// The file must contain exactly 32 bytes of data. In previous versions, /// this option was called '--comm-key'. #[arg( long, value_name = "FILE", group = "cck-available", 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, /// Use the content of FILE as the image encryption key. /// /// The file must contain exactly 64 bytes of data. #[arg( long, value_name = "FILE", conflicts_with = "disable_image_encryption", alias = "x-comp-key" )] pub image_key: Option, } #[derive(Args, Debug)] #[cfg_attr(test, derive(Default))] #[command( group(ArgGroup::new("header-flags").multiple(true).conflicts_with_all(["x_pcf", "x_scf"])), group(ArgGroup::new("cck-available").multiple(true)))] pub struct CreateBootImageLegacyFlags { /// Enable Secure Execution guest dump support. This option requires the /// '--cck' or '--enable-cck-update' option. #[arg(long, action = clap::ArgAction::SetTrue, requires = "cck-available", 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 CCK update support. Requires z17 or up. This option cannot be /// used in conjunction with the '--enable-cck-extension-secret' option. #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_cck_extension_secret", group="cck-available", group="header-flags")] pub enable_cck_update: Option, /// Disable CCK update support (default). #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_cck_update", group="header-flags")] pub disable_cck_update: 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, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] pub enum OutputFormatKind { /// Human-readable, unstable text format Text, /// JSON format. Json, } impl Display for OutputFormatKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Text => write!(f, "human-readable"), Self::Json => write!(f, "JSON"), } } } impl FromStr for OutputFormatKind { type Err = String; fn from_str(s: &str) -> Result { match s { "text" => Ok(Self::Text), "json" => Ok(Self::Json), _ => Err(format!("Invalid output format: {s}")), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OutputFormatVariant { /// Default Default, /// Full Full, /// Minified Minify, /// Pretty Pretty, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OutputFormatSpec { pub kind: OutputFormatKind, pub variant: OutputFormatVariant, } impl OutputFormatSpec { pub fn detect() -> Self { let kind = if std::io::stdout().is_terminal() { OutputFormatKind::Text } else { OutputFormatKind::Json }; Self { kind, variant: OutputFormatVariant::Default, } } } #[derive(Clone, Default)] struct OutputFormatSpecParser; impl TypedValueParser for OutputFormatSpecParser { type Value = OutputFormatSpec; fn parse_ref( &self, cmd: &Command, arg: Option<&Arg>, value: &OsStr, ) -> Result { let s = value.to_string_lossy(); let mut parts = s.splitn(2, ':'); let arg_name = arg.unwrap().get_id().to_string(); let kind_s = parts.next().unwrap().to_ascii_lowercase(); let variant_s = parts.next().map(|x| x.to_ascii_lowercase()); let kind = kind_s.as_str().parse().map_err(|_| { let mut err = clap::error::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd); err.insert( clap::error::ContextKind::InvalidArg, clap::error::ContextValue::String(arg_name.clone()), ); err.insert( clap::error::ContextKind::InvalidValue, clap::error::ContextValue::String(kind_s.to_string()), ); err })?; let variant = match (kind, variant_s.as_deref()) { (_, None) => OutputFormatVariant::Default, (_, Some("default")) => OutputFormatVariant::Default, (OutputFormatKind::Text, Some("full")) => OutputFormatVariant::Full, (OutputFormatKind::Json, Some("pretty")) => OutputFormatVariant::Pretty, (OutputFormatKind::Json, Some("minify")) => OutputFormatVariant::Minify, (_, Some(other)) => { let mut err = clap::error::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd); err.insert( clap::error::ContextKind::InvalidArg, clap::error::ContextValue::String(arg_name.clone()), ); err.insert( clap::error::ContextKind::InvalidValue, clap::error::ContextValue::String(format!("{kind_s}:{other}")), ); Err(err)? } }; Ok(OutputFormatSpec { kind, variant }) } // This is used for shell completion suggestions for `--format` fn possible_values(&self) -> Option + '_>> { use clap::builder::PossibleValue as PV; let vals: Vec = vec![ PV::new("text") .help("Human-readable, unstable text format (default if a terminal is available)"), PV::new("text:full").help("Human-readable, full detail text format"), PV::new("json") .help("Pretty-printed machine-readable JSON (default if no terminal available)"), PV::new("json:pretty").help("Pretty-printed machine-readable JSON"), PV::new("json:minify").help("Minified machine-readable JSON"), ]; Some(Box::new(vals.into_iter())) } } #[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)] #[command(group(ArgGroup::new("info-mode").required(true).args(["path", "print_schema"])))] pub struct InfoArgs { #[clap(flatten)] pub input: Option, /// Output format for the Secure Execution image information. /// /// If not specified, the format is automatically determined based on /// whether stdout is connected to a terminal. #[arg(long, value_parser=OutputFormatSpecParser::default(), conflicts_with = "print_schema")] pub format: Option, /// Use the key in FILE to verify the Secure Execution header and optionally /// use '--show-secrets' to decrypt it. /// /// The key must be the same key that was specified with '--hdr-key' when the /// Secure Execution image was created. The key is used to: /// 1. Verify the integrity and authenticity of the header /// 2. Optionally decrypt secrets with '--show-secrets' /// /// Without this option, the information is displayed, but NOT verified, and /// a warning is printed. The displayed data should not be trusted without /// verification. #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath, alias = "key", verbatim_doc_comment, conflicts_with = "print_schema")] pub hdr_key: Option, /// This option reveals sensitive information that is normally encrypted in /// the header, such as: /// - Customer communication key (CCK) /// - Image encryption key /// - Other confidential data /// /// SECURITY WARNING: Only use this option in secure, trusted environments. /// The decrypted secrets should never be exposed in untrusted systems. /// /// This option requires '--hdr-key' to decrypt the header. #[arg( long, requires = "hdr_key", verbatim_doc_comment, conflicts_with = "print_schema" )] pub show_secrets: bool, /// Print the schema for the 'info' subcommand and exit. /// /// This outputs the schema that describes the structure of the given output FORMAT /// produced by the 'info' subcommand. The schema can be used for: /// - Validating output /// - Building tools that parse the output #[arg(value_name = "FORMAT", long, verbatim_doc_comment)] pub print_schema: 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)] #[cfg_attr(test, derive(Default))] 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, #[clap(flatten)] pub keys: UserKeys, #[clap(flatten)] pub legacy_flags: CreateBootImageLegacyFlags, #[clap(flatten)] pub experimental_args: CreateBootImageExperimentalArgs, } /// Experimental options #[derive(Args, Debug)] #[cfg_attr(test, derive(Default))] 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 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. /// /// The 'info' subcommand extracts and displays information about an /// existing IBM Secure Execution image, including information from the /// Secure Execution header. By default, the header is displayed without /// verifying its integrity and authenticity. Use '--hdr-key' to /// authenticate and verify the header integrity. /// /// Output can be formatted as human-readable text or JSON. Use '--format' /// to control the output format and level of detail. /// /// SECURITY NOTE: Without '--hdr-key', the displayed information is NOT /// verified and should not be trusted. 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"]), CliOption::new("enable-cck-update", ["--enable-cck-update"])])), 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"]),])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-cck-update", ["--enable-cck-update"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-cck-update", ["--disable-cck-update"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("multiple-cck", ["--disable-cck-update", "--cck", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-comp-key", ["--x-comp-key", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("image-key", ["--image-key", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-image-encryption", ["--enable-image-encryption"]), CliOption::new("image-key", ["--image-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 both `--cck' and `--enable-cck-update' 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"]),])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("extension", ["--enable-cck-extension-secret"]), CliOption::new("update", ["--enable-cck-update"])])), // Image component key cannot be provided multiple times flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-comp-key", ["--x-comp-key", "/dev/null"]), CliOption::new("x-comp-key2", ["--x-comp-key", "/dev/null"])])), flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-comp-key", ["--x-comp-key", "/dev/null"]), CliOption::new("image-key", ["--image-key", "/dev/null"])])), // Disable image encryption and providing an image-key is mutually // exclusive. flat_map_collect(insert(mvca.clone(), vec![CliOption::new("disable-image-encryption", ["--disable-image-encryption"]), CliOption::new("image-key", ["--image-key", "/dev/null"])])), ]; 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 = [ // Format argument is optional flat_map_collect(insert( args.clone(), vec![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("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"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("hdr-key", ["--key", "/dev/null"]), CliOption::new("format", ["--format=json:default"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("hdr-key", ["--key", "/dev/null"]), CliOption::new("format", ["--format=json:minify"]), CliOption::new("image", ["/dev/null"]), ], )), flat_map_collect(insert( args.clone(), vec![ CliOption::new("hdr-key", ["--key", "/dev/null"]), CliOption::new("format", ["--format=json:pretty"]), 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"]), ], )), // --print-schema json works standalone flat_map_collect(insert( args.clone(), vec![CliOption::new( "print-json-schema", ["--print-schema", "json"], )], )), ]; let invalid_test_args = [ // No default defined for --format flat_map_collect(insert( args.clone(), vec![ CliOption::new("format", ["--format"]), CliOption::new("image", ["--", "/dev/null"]), ], )), // --print-json-schema conflicts with input flat_map_collect(insert( args.clone(), vec![ CliOption::new("print-json-schema", ["--print-schema", "json"]), CliOption::new("image", ["/dev/null"]), ], )), // --print-json-schema conflicts with --format flat_map_collect(insert( args.clone(), vec![ CliOption::new("print-json-schema", ["--print-schema", "json"]), CliOption::new("format", ["--format=json"]), ], )), // --print-json-schema conflicts with --hdr-key flat_map_collect(insert( args.clone(), vec![ CliOption::new("print-json-schema", ["--print-schema", "json"]), CliOption::new("hdr-key", ["--hdr-key", "/dev/null"]), ], )), // --print-json-schema conflicts with --show-secrets flat_map_collect(insert( args, vec![ CliOption::new("print-json-schema", ["--print-schema", "json"]), CliOption::new("show-secrets", ["--show-secrets"]), ], )), ]; 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(); } }