//! Command-line frontend for SOP.
//!
//! This is an implementation of the SOP Command-Line protocol in
//! terms of the trait [`crate::SOP`], hence it can be shared between
//! SOP implementations.
//!
//! Via the "cliv" feature, it can also produce a smaller standalone
//! binary that only implements the sopv subset.
//!
//! To use, add this snippet to `Cargo.toml`:
//!
//! ```toml
//! [[bin]]
//! name = "my-sop"
//! required-features = ["cli"]
//!
//! [[bin]]
//! name = "my-sopv"
//! required-features = ["cliv"]
//!
//! [features]
//! cli = ["sop/cli"]
//! cliv = ["sop/cliv"]
//! ```
//!
//! And create `src/bin/my-sop.rs` and `src/bin/my-sopv.rs`,
//! containing, respectively, something along the lines of:
//!
//! ```rust,ignore
//! fn main() {
//!     // Be clever and inspect argv[0].
//!     let argv0 = std::env::args_os().next();
//!     let variant = if argv0
//!         .map(|s| s.to_string_lossy().contains("my-sopv"))
//!         .unwrap_or(false)
//!     {
//!         // Restrict to the verification subset.
//!         sop::cli::Variant::Verification
//!     } else {
//!         sop::cli::Variant::Full
//!     };
//!
//!     sop::cli::main(&mut MySOPImplementation::default(),
//!                    sop::cli::Variant::Verification);
//! }
//! ```
//!
//! And for the verification subset:
//!
//! ```rust,ignore
//! fn main() {
//!     sop::cli::main(&mut MySOPImplementation::default(),
//!                    sop::cli::Variant::Verification);
//! }
//! ```
//!
//! Note: If you don't need to tweak your implementation for the
//! verification subset of SOP, you can also build both binaries from
//! the same source.
//!
//! ## Generating shell completions and manual pages
//!
//! To create shell completions, add this snippet to `Cargo.toml`:
//!
//! ```toml
//! [build-dependencies]
//! sop = "..."
//! ```
//!
//! And create `build.rs` along the lines of:
//!
//! ```rust,no_run
//! use std::{
//!     env,
//!     error::Error,
//!     fs,
//!     path::PathBuf,
//! };
//!
//! use sop::cli::Variant;
//!
//! #[path = "src/version.rs"]
//! mod version;
//!
//! fn main() {
//!     #[cfg(feature = "cli")]
//!     cli("sqop", Variant::Full).unwrap();
//!
//!     #[cfg(feature = "cliv")]
//!     cli("sqopv", Variant::Verification).unwrap();
//! }
//!
//! /// Writes shell completions and man pages.
//! fn cli(name: &'static str, variant: Variant) -> Result<(), Box<dyn Error>> {
//!     let version = version::Version::with_name(name);
//!
//!     sop::cli::write_shell_completions2(
//!         variant, name, asset_out_dir("shell-completions")?)?;
//!     sop::cli::write_man_pages(
//!         variant, &version, "Sequoia PGP",
//!         asset_out_dir("man-pages")?)?;
//!
//!     Ok(())
//! }
//!
//! /// Variable name to control the asset out directory with.
//! const ASSET_OUT_DIR: &str = "ASSET_OUT_DIR";
//!
//! /// Returns the directory to write the given assets to.
//! #[allow(dead_code)]
//! fn asset_out_dir(asset: &str) -> Result<PathBuf, Box<dyn Error>> {
//!     println!("cargo:rerun-if-env-changed={}", ASSET_OUT_DIR);
//!     let outdir: PathBuf =
//!         env::var_os(ASSET_OUT_DIR).unwrap_or_else(
//!             || env::var_os("OUT_DIR").expect("OUT_DIR not set")).into();
//!     if outdir.exists() && ! outdir.is_dir() {
//!         return Err(
//!             format!("{}={:?} is not a directory", ASSET_OUT_DIR, outdir).into());
//!     }
//!
//!     let path = outdir.join(asset);
//!     fs::create_dir_all(&path)?;
//!     Ok(path)
//! }
//! ```
//!
//! # Features and limitations
//!
//! - The special designator `@FD:` is only available on UNIX-like systems.
//!
//! - On Windows, certs and keys provided via the `@ENV:` special
//!   designator must be ASCII armored and well-formed UTF-8.

use std::{
    io::{self, Write},
    path::Path,
};

use anyhow::{Context, Result};

use chrono::{DateTime, NaiveTime, offset::Utc};
use clap::{CommandFactory, FromArgMatches, Parser};

use crate::{
    Save,
    Load,
    SOP,
};

#[cfg(feature = "cli")]
use crate::{
    ArmorLabel,
    EncryptAs,
    InlineSignAs,
    Password,
    SignAs,
};

/// The last meaningful update of the command line interface.
///
/// This will be used as-is in the generated manual pages.
pub const LAST_UPDATE_TIMESTAMP: &'static str =
// !!! Update this with every release changing the interface. !!!
//
// date +"%B %Y"
    "February 2025";

#[derive(Debug, Parser)]
#[clap(about = "An implementation of the Stateless OpenPGP Command Line Interface")]
#[clap(disable_version_flag(true))]
struct Cli<O: clap::Subcommand> {
    /// Emit verbose output for debugging.
    #[clap(long = "debug", global = true)]
    debug: bool,

    #[clap(subcommand)]
    operation: O,
}

/// Variant of the SOP specification.
#[derive(Clone, Copy)]
pub enum Variant {
    /// The full functionality.
    Full,

    /// The verification subset.
    Verification,
}

impl<O: clap::Subcommand> Cli<O> {
    fn with_version<'v, V>(v: &V, variant: Variant)
                       -> Result<clap::Command>
    where
        V: crate::ops::Version<'v> + ?Sized
    {
        // Do a little dance to supply name and version to clap.
        let (name, version, about) = {
            let v = v.frontend()?;
            (
                v.name.clone(),
                v.version.clone(),
                format!("An implementation of the {}\
                         Stateless OpenPGP Command \
                         Line Interface using {} {}",
                        match variant {
                            Variant::Full => "",
                            Variant::Verification =>
                                "verification-only subset of the ",
                        },
                        v.name, v.version),
            )
        };

        Ok(Cli::<O>::command()
           .name(Box::leak(name.into_boxed_str()) as &str)
           .about(Box::leak(about.into_boxed_str()) as &str)
           .version(Box::leak(version.into_boxed_str()) as &str))
    }
}

/// All the operations of the verification subset of SOP.
#[derive(Debug, Parser)]
enum VerificationSubset {
    /// Prints version information.
    ///
    /// Invoked without arguments, returns name and version of the SOP
    /// implementation.
    #[clap(display_order = 100)]
    #[cfg(any(feature = "cliv", feature = "cli"))]
    Version {
        /// Returns name and version of the primary underlying OpenPGP
        /// toolkit.
        #[clap(long, conflicts_with("extended"), conflicts_with("sop_spec"), conflicts_with("sopv"))]
        backend: bool,

        /// Returns multiple lines of name and version information.
        ///
        /// The first line is the name and version of the SOP
        /// implementation, but the rest have no defined structure.
        #[clap(long, conflicts_with("backend"), conflicts_with("sop_spec"), conflicts_with("sopv"))]
        extended: bool,

        /// Returns the latest version of the SOP spec that is
        /// implemented.
        #[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sopv"))]
        sop_spec: bool,

        /// Returns "1.0\n" if the sopv subset is implemented.
        #[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sop_spec"))]
        sopv: bool,
    },

    /// Verifies Detached Signatures.
    #[clap(display_order = 500)]
    #[cfg(any(feature = "cli", feature = "cliv"))]
    Verify {
        /// Consider signatures before this date invalid.
        #[clap(long)]
        not_before: Option<NotBefore>,
        /// Consider signatures after this date invalid.
        #[clap(long)]
        not_after: Option<NotAfter>,
        /// Signatures to verify.
        signatures: String,
        /// Certs for verification.
        certs: Vec<String>,
    },

    /// Verifies Inline-Signed Messages.
    #[clap(display_order = 751)]
    #[cfg(any(feature = "cli", feature = "cliv"))]
    InlineVerify {
        /// Consider signatures before this date invalid.
        #[clap(long)]
        not_before: Option<NotBefore>,
        /// Consider signatures after this date invalid.
        #[clap(long)]
        not_after: Option<NotAfter>,
        /// Write verification result here.
        #[clap(long)]
        verifications_out: Option<String>,
        /// Certs for verification.
        certs: Vec<String>,
    },
}

/// All the operations of the full SOP.
#[derive(Debug, Parser)]
enum Operation {
    ///
    #[command(flatten)]
    VerificationSubset(VerificationSubset),

    /// Emits a list of profiles supported by the identified
    /// subcommand.
    #[clap(display_order = 150)]
    #[cfg(feature = "cli")]
    ListProfiles {
        subcommand: String,
    },
    /// Generates a Secret Key.
    #[clap(display_order = 200)]
    #[cfg(feature = "cli")]
    GenerateKey {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// Select the profile to use for key generation.
        #[clap(long)]
        profile: Option<String>,
        /// Create a signing-only key.
        #[clap(long)]
        signing_only: bool,
        /// Protect the newly generated key with the given password.
        #[clap(long)]
        with_key_password: Option<String>,
        /// UserIDs for the generated key.
        userids: Vec<String>,
    },

    /// Updates a key's password.
    ///
    /// The output will be the same set of OpenPGP Transferable Secret
    /// Keys as the input, but with all secret key material locked
    /// according to the password indicated by the
    /// `--new-key-password` option (or, with no password at all, if
    /// `--new-key-password` is absent).  Note that
    /// `--old-key-password` can be supplied multiple times, and each
    /// supplied password will be tried as a means to unlock any
    /// locked key material encountered.  It will normalize a
    /// Transferable Secret Key to use a single password even if it
    /// originally had distinct passwords locking each of the subkeys.
    ///
    /// If any secret key packet is locked but cannot be unlocked with
    /// any of the supplied `--old-key-password` arguments, this
    /// subcommand should fail with `KEY_IS_PROTECTED`.
    #[clap(display_order = 210)]
    #[cfg(feature = "cli")]
    ChangeKeyPassword {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// The new password to lock the key with, or just unlock the
        /// key if the option is absent.
        #[clap(long)]
        new_key_password: Option<String>,
        /// Unlock the keys with these passwords.
        #[clap(long, number_of_values = 1)]
        old_key_password: Vec<String>,
    },

    /// Creates a Revocation Certificate.
    ///
    /// Generate a revocation certificate for each Transferable Secret
    /// Key found.
    #[clap(display_order = 220)]
    #[cfg(feature = "cli")]
    RevokeKey {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// Unlock the keys with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,
    },

    /// Extracts a Certificate from a Secret Key.
    #[clap(display_order = 230)]
    #[cfg(feature = "cli")]
    ExtractCert {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
    },

    /// Keep a Secret Key Up-To-Date.
    #[clap(display_order = 240)]
    #[cfg(feature = "cli")]
    UpdateKey {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,

        /// Don't make the updated key encryption-capable if it isn't
        /// already.
        #[clap(long)]
        signing_only: bool,

        /// Don't advertise support for capabilities that aren't
        /// already advertised by the key.
        #[clap(long)]
        no_added_capabilities: bool,

        /// Unlock the keys with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,

        /// Merge updates into the key.
        #[clap(long, number_of_values = 1)]
        merge_certs: Vec<String>,
    },

    /// Merge OpenPGP Certificates.
    #[clap(display_order = 250)]
    #[cfg(feature = "cli")]
    MergeCerts {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,

        /// Merge updates into the certs.
        certs: Vec<String>,
    },

    /// Certify OpenPGP Certificate User IDs.
    #[clap(display_order = 300)]
    #[cfg(feature = "cli")]
    CertifyUserid {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,

        /// Certify the specified user IDs.
        #[clap(long, required = true, number_of_values = 1)]
        userid: Vec<String>,

        /// Unlock the keys with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,

        /// Don't require self-signatures on the user IDs to be
        /// certified.
        #[clap(long)]
        no_require_self_sig: bool,

        /// Create certifications using these keys.
        keys: Vec<String>,
    },

    /// Validate a User ID in an OpenPGP Certificate.
    #[clap(display_order = 320)]
    #[cfg(feature = "cli")]
    ValidateUserid {
        /// Treat USERID as an e-mail address, and matched only
        /// against the e-mail address part of each correctly bound
        /// User ID.
        #[clap(long)]
        addr_spec_only: bool,

        /// Evaluate the validity of the User ID at the specified
        /// time, not at the current time.
        #[clap(long)]
        validate_at: Option<Timestamp>,

        /// The User ID to validate.
        userid: String,

        /// Authority OpenPGP certificates, i.e. trust roots.
        #[clap(required = true)]
        certs: Vec<String>,
    },

    /// Creates Detached Signatures.
    #[clap(display_order = 400)]
    #[cfg(feature = "cli")]
    Sign {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// Sign binary data or UTF-8 text.
        #[clap(default_value = "binary", long = "as")]
        as_: SignAs,
        /// Emit the digest algorithm used to the specified file.
        #[clap(long)]
        micalg_out: Option<String>,
        /// Try to decrypt the signing KEYS with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,
        /// Keys for signing.
        keys: Vec<String>,
    },
    /// Encrypts a Message.
    #[clap(display_order = 600)]
    #[cfg(feature = "cli")]
    Encrypt {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// Select the profile to use for encryption.
        #[clap(long)]
        profile: Option<String>,
        /// Encrypt binary data or UTF-8 text.
        #[clap(default_value = "binary", long = "as")]
        as_: EncryptAs,
        /// Encrypt with passwords.
        #[clap(long, number_of_values = 1)]
        with_password: Vec<String>,
        /// Keys for signing.
        #[clap(long, number_of_values = 1)]
        sign_with: Vec<String>,
        /// Try to decrypt the signing KEYS with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,
        /// Write the session key here.
        #[clap(long)]
        session_key_out: Option<String>,
        /// Encrypt for these certs.
        certs: Vec<String>,
    },
    /// Decrypts a Message.
    #[clap(display_order = 700)]
    #[cfg(feature = "cli")]
    Decrypt {
        /// Write the session key here.
        #[clap(long)]
        session_key_out: Option<String>,
        /// Try to decrypt with this session key.
        #[clap(long, number_of_values = 1)]
        with_session_key: Vec<String>,
        /// Try to decrypt with this password.
        #[clap(long, number_of_values = 1)]
        with_password: Vec<String>,
        /// Write verification result here.
        #[clap(long, alias("verify-out"))]
        verifications_out: Option<String>,
        /// Certs for verification.
        #[clap(long, number_of_values = 1)]
        verify_with: Vec<String>,
        /// Consider signatures before this date invalid.
        #[clap(long)]
        verify_not_before: Option<NotBefore>,
        /// Consider signatures after this date invalid.
        #[clap(long)]
        verify_not_after: Option<NotAfter>,
        /// Try to decrypt the encryption KEYS with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,
        /// Try to decrypt with these keys.
        keys: Vec<String>,
    },
    /// Converts binary OpenPGP data to ASCII.
    #[clap(display_order = 800)]
    #[cfg(feature = "cli")]
    Armor {
        /// Indicates the kind of data.
        #[clap(long, default_value = "auto", hide(true))]
        label: ArmorLabel,
    },
    /// Converts ASCII OpenPGP data to binary.
    #[clap(display_order = 900)]
    #[cfg(feature = "cli")]
    Dearmor {
    },
    /// Splits Signatures from an Inline-Signed Message.
    #[clap(display_order = 750)]
    #[cfg(feature = "cli")]
    InlineDetach {
        /// Don't ASCII-armor the signatures.
        #[clap(long)]
        no_armor: bool,
        /// Write Signatures here.
        #[clap(long)]
        signatures_out: String,
    },
    /// Creates Inline-Signed Messages.
    #[clap(display_order = 752)]
    #[cfg(feature = "cli")]
    InlineSign {
        /// Don't ASCII-armor output.
        #[clap(long)]
        no_armor: bool,
        /// Sign binary data, UTF-8 text, or using the Cleartext
        /// Signature Framework.
        #[clap(default_value = "binary", long = "as")]
        as_: InlineSignAs,
        /// Try to decrypt the signing KEYS with these passwords.
        #[clap(long, number_of_values = 1)]
        with_key_password: Vec<String>,
        /// Keys for signing.
        keys: Vec<String>,
    },
    /// Unsupported subcommand.
    #[clap(external_subcommand)]
    Unsupported(Vec<String>),
}

/// Generates shell completions.
#[deprecated(note="Use write_shell_completions2.")]
pub fn write_shell_completions<B, P>(binary_name: B,
                                     out_path: P)
                                     -> io::Result<()>
where B: AsRef<str>,
      P: AsRef<Path>,
{
    let variant = if binary_name.as_ref().ends_with("v") {
        Variant::Verification
    } else {
        Variant::Full
    };

    write_shell_completions2(variant, binary_name, out_path)
}

/// Generates shell completions for the given variant.
pub fn write_shell_completions2<B, P>(variant: Variant,
                                      binary_name: B,
                                      out_path: P)
                                      -> io::Result<()>
where
    B: AsRef<str>,
    P: AsRef<Path>,
{
    use clap_complete::{generate_to, Shell};

    let binary_name = binary_name.as_ref();
    let out_path = out_path.as_ref();
    std::fs::create_dir_all(&out_path)?;

    const SHELLS: &[Shell] = &[
        Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell,
        Shell::Elvish,
    ];
    match variant {
        Variant::Verification => {
            let mut clap = Cli::<VerificationSubset>::command();
            for shell in SHELLS {
                generate_to(*shell, &mut clap, binary_name, out_path)?;
            }
        },

        Variant::Full => {
            let mut clap = Cli::<Operation>::command();
            for shell in SHELLS {
                generate_to(*shell, &mut clap, binary_name, out_path)?;
            }
        },
    }

    println!("cargo:warning={}: shell completions written to {}",
             binary_name,
             out_path.display());
    Ok(())
}

/// Generates man pages.
pub fn write_man_pages<A, P, V>(variant: Variant,
                                version: &V,
                                author: A,
                                out_path: P)
                                -> Result<()>
where
    V: for <'a> crate::ops::Version<'a>,
    A: ToString,
    P: AsRef<Path>,
{
    let out_path = out_path.as_ref();
    std::fs::create_dir_all(&out_path)?;

    match variant {
        Variant::Verification => {
            let cmd = Cli::<VerificationSubset>::with_version(version, variant)?
                .author(Box::leak(author.to_string().into_boxed_str()) as &str);
            generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?;
        },

        Variant::Full => {
            let cmd = Cli::<Operation>::with_version(version, variant)?
                .author(Box::leak(author.to_string().into_boxed_str()) as &str);
            generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?;
        },
    }

    println!("cargo:warning={}: man pages written to {}",
             version.frontend()?.name,
             out_path.display());
    Ok(())
}

/// Like clap's version, but modifies the man page's source and date.
fn generate_to(
    cmd: clap::Command,
    date: &str,
    out_dir: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
    fn generate(
        cmd: clap::Command,
        source: &str,
        date: &str,
        out_dir: &std::path::Path,
    ) -> Result<(), std::io::Error> {
        for cmd in cmd.get_subcommands().filter(|s| !s.is_hide_set()).cloned() {
            generate(cmd, source, date, out_dir)?;
        }
        clap_mangen::Man::new(cmd)
            .source(source) // Always use the top-level source.
            .date(date) // Set the last modification timestamp.
            .generate_to(out_dir)?;
        Ok(())
    }

    // Use the top-level source for all man pages.
    let source = format!(
        "{} {}",
        cmd.get_name(),
        cmd.get_version().unwrap_or_default()
    );

    let mut cmd = cmd.disable_help_subcommand(true);
    cmd.build();
    generate(cmd, &source, date, out_dir.as_ref())
}

/// Implements the SOP command-line interface.
pub fn main<'s, S, C, K, Sigs>(sop: &'s mut S, variant: Variant)
                               -> !
where
    S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>,
    C: Load<'s, S> + Save,
    K: Load<'s, S> + Save,
    Sigs: Load<'s, S> + Save,
{
    use std::process::exit;

    match real_main::<S, C, K, Sigs>(sop, variant) {
        Ok(()) => exit(0),
        Err(e) => {
            print_error_chain(&e);
            let e = match e.downcast::<crate::Error>() {
                Ok(e) => exit(Error::from(e).into()),
                Err(e) => e,
            };
            let e = match e.downcast::<Error>() {
                Ok(e) => exit(e.into()),
                Err(e) => e,
            };

            // XXX: Would be nice to at least inform developers that
            // we return a generic error here.
            let _ = e;
            exit(1);
        },
    }
}

fn real_main<'s, S, C, K, Sigs>(sop: &'s mut S, variant: Variant)
                                -> Result<()>
where
    S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>,
    C: Load<'s, S> + Save,
    K: Load<'s, S> + Save,
    Sigs: Load<'s, S> + Save,
{
    // If we are the full version, but are invoked as the verification
    // subset, we first parse the arguments using the verification
    // subset parser.
    #[cfg(feature = "cli")]
    if let Variant::Verification = variant {
        let app = Cli::<VerificationSubset>::with_version(
            sop.version()?.as_ref(), Variant::Verification)?;

        let matches = match app.try_get_matches() {
            Ok(v) => v,
            Err(e) => {
                use clap::error::ErrorKind;
                return match e.kind() {
                    ErrorKind::DisplayHelp => {
                        e.exit()
                    },
                    ErrorKind::UnknownArgument =>
                        Err(anyhow::Error::from(Error::UnsupportedOption)
                            .context(format!("{}", e))),
                    _ => Err(e.into()),
                };
            },
        };

        Cli::<VerificationSubset>::from_arg_matches(&matches)?;

        // If we get here, the syntax checked out.  We now just fall
        // through to the full parser, which accepts a superset of
        // what we just parsed.
    }

    let app = Cli::<Operation>::with_version(sop.version()?.as_ref(), variant)?;

    let matches = match app.clone().try_get_matches() {
        Ok(v) => v,
        Err(e) => {
            use clap::error::ErrorKind;
            return match e.kind() {
                ErrorKind::DisplayHelp => {
                    e.exit()
                },
                ErrorKind::UnknownArgument =>
                    Err(anyhow::Error::from(Error::UnsupportedOption)
                        .context(format!("{}", e))),
                _ => Err(e.into()),
            };
        },
    };

    let cli = Cli::from_arg_matches(&matches)?;
    sop.debug(cli.debug);

    match cli.operation {
        Operation::VerificationSubset(VerificationSubset::Version {
            backend, extended, sop_spec, sopv,
        }) => {
            let version = sop.version()?;
            match (backend, extended, sop_spec, sopv) {
                (false, false, false, false) => {
                    let v = version.frontend()?;
                    println!("{} {}", v.name, v.version);
                },
                (true, false, false, false) => {
                    let v = version.backend()?;
                    println!("{} {}", v.name, v.version);
                },
                (false, true, false, false) => {
                    let v = version.frontend()?;
                    println!("{} {}", v.name, v.version);
                    println!("sop-rs {}", env!("CARGO_PKG_VERSION"));
                    println!("{}", version.extended()?);
                },
                (false, false, true, false) => {
                    println!("{}", sop.spec_version());
                },
                (false, false, false, true) => {
                    println!("{}", sop.sopv_version()?);
                },
                _ => unreachable!("flags are mutually exclusive"),
            }
        },

        #[cfg(feature = "cli")]
        Operation::ListProfiles { subcommand, } => {
            let profiles = match subcommand.as_str() {
                "generate-key" => sop.generate_key()?.list_profiles(),
                "encrypt" => sop.encrypt()?.list_profiles(),
                _ => return Err(Error::UnsupportedProfile.into()),
            };
            for (p, d) in profiles {
                println!("{}: {}", p, d);
            }
        },

        #[cfg(feature = "cli")]
        Operation::GenerateKey {
            no_armor,
            profile,
            signing_only,
            with_key_password,
            userids,
        } => {
            let mut op = sop.generate_key()?;
            if let Some(p) = profile {
                op = op.profile(&p)?;
            }
            if signing_only {
                op = op.signing_only();
            }
            if let Some(p) = with_key_password {
                op = op.with_key_password(get_password(p)?)?;
            }
            for u in userids {
                op = op.userid(&u);
            }
            op.generate()?
                .to_stdout(! no_armor)?;
        },

        #[cfg(feature = "cli")]
        Operation::ChangeKeyPassword {
            no_armor,
            new_key_password,
            old_key_password,
        } => {
            let mut op = sop.change_key_password()?;
            if let Some(p) = new_key_password {
                op = op.new_key_password(get_password(p)?)?;
            }
            for p in old_key_password {
                op = op.old_key_password(get_password(p)?)?;
            }
            op.keys(&K::from_stdin(sop)?)?
                .to_stdout(! no_armor)?;
        }

        #[cfg(feature = "cli")]
        Operation::RevokeKey { no_armor, with_key_password, } => {
            let mut op = sop.revoke_key()?;
            for p in with_key_password {
                op = op.with_key_password(get_password(p)?)?;
            }
            op.keys(&K::from_stdin(sop)?)?
                .to_stdout(! no_armor)?;
        }

        #[cfg(feature = "cli")]
        Operation::ExtractCert { no_armor, } => {
            sop.extract_cert()?
                .keys(&K::from_stdin(sop)?)?
                .to_stdout(! no_armor)?;
        },

        #[cfg(feature = "cli")]
        Operation::UpdateKey {
            no_armor, signing_only, no_added_capabilities,
            with_key_password, merge_certs,
        } => {
            let mut op = sop.update_key()?;
            if signing_only {
                op = op.signing_only();
            }
            if no_added_capabilities {
                op = op.no_added_capabilities();
            }
            for p in with_key_password {
                op = op.with_key_password(get_password(p)?)?;
            }
            for (name, mut stream) in load_files(merge_certs)? {
                op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let keys = op.update(&K::from_stdin(sop)?)?;
            keys.to_stdout(! no_armor)?;
        },

        #[cfg(feature = "cli")]
        Operation::MergeCerts { no_armor, certs, } => {
            let mut op = sop.merge_certs()?;
            for (name, mut stream) in load_files(certs)? {
                op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let certs = op.merge(&C::from_stdin(sop)?)?;
            certs.to_stdout(! no_armor)?;
        },

        #[cfg(feature = "cli")]
        Operation::CertifyUserid {
            no_armor, userid, with_key_password, no_require_self_sig, keys,
        } => {
            let mut op = sop.certify_userid()?;
            if no_require_self_sig {
                op = op.no_require_self_sig();
            }
            for u in userid {
                op = op.userid(u);
            }
            for p in with_key_password {
                op = op.with_key_password(get_password(p)?)?;
            }
            for (name, mut stream) in load_files(keys)? {
                op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let certs = op.certify(&C::from_stdin(sop)?)?;
            certs.to_stdout(! no_armor)?;
        },

        #[cfg(feature = "cli")]
        Operation::ValidateUserid {
            addr_spec_only, validate_at, userid, certs,
        } => {
            let mut op = sop.validate_userid()?;
            if let Some(t) = validate_at {
                op = op.validate_at(t.into())?;
            }
            for (name, mut stream) in load_files(certs)? {
                op = op.trust_roots(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }

            op = op.target_certs(&C::from_stdin(sop)?)?;

            if addr_spec_only {
                op.email(&userid)?;
            } else {
                op.userid(&userid)?;
            }
        },

        #[cfg(feature = "cli")]
        Operation::Sign {
            no_armor,
            as_,
            micalg_out,
            with_key_password,
            keys,
        } => {
            let mut op = sop.sign()?.mode(as_);
            for p in with_key_password {
                op = op.with_key_password(get_password_unchecked(p)?)?;
            }
            for (name, mut stream) in load_files(keys)? {
                op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let (micalg, sigs) = op.data(&mut io::stdin())?;
            if let Some(path) = micalg_out {
                let mut sink = create_file(path)?;
                write!(sink, "{}", micalg)?;
            }
            sigs.to_stdout(! no_armor)?;
        },

        #[cfg(any(feature = "cli", feature = "cliv"))]
        Operation::VerificationSubset(VerificationSubset::Verify {
            not_before, not_after, signatures, certs,
        }) => {
            let mut op = sop.verify()?;
            if let Some(t) = not_before {
                op = op.not_before(t.into());
            }
            if let Some(t) = not_after {
                op = op.not_after(t.into());
            }
            for (name, mut stream) in load_files(certs)? {
                op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let (name, mut stream) = load_file(signatures)?;
            let verifications =
                op.signatures(&Sigs::from_reader(sop, &mut stream, Some(name))?)?
                .data(&mut io::stdin())?;

            for v in verifications {
                println!("{}", v);
            }
        },

        #[cfg(feature = "cli")]
        Operation::Encrypt {
            no_armor,
            profile,
            as_,
            with_password,
            with_key_password,
            sign_with,
            session_key_out,
            certs,
        } =>
        {
            let session_key_out: Option<Box<dyn io::Write>> =
                if let Some(f) = session_key_out {
                    Some(Box::new(create_file(f)?))
                } else {
                    None
                };

            let mut op = sop.encrypt()?.mode(as_);
            if no_armor {
                op = op.no_armor();
            }
            if let Some(p) = profile {
                op = op.profile(&p)?;
            }
            for p in with_key_password {
                op = op.with_key_password(get_password_unchecked(p)?)?;
            }
            for (name, mut stream) in load_files(sign_with)? {
                op = op.sign_with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
            }
            for pw in with_password.into_iter().map(get_password) {
                op = op.with_password(pw?)?;
            }
            for (name, mut stream) in load_files(certs)? {
                op = op.with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let session_key =
                op.plaintext(&mut io::stdin())?.to_writer(&mut io::stdout())?;

            if let Some(mut sko) = session_key_out {
                if let Some(sk) = session_key {
                    writeln!(sko, "{}", sk)?;
                } else {
                    return Err(Error::UnsupportedSubcommand.into());
                }
            }
        },

        #[cfg(feature = "cli")]
        Operation::Decrypt {
            session_key_out,
            with_session_key,
            with_password,
            verifications_out,
            verify_with,
            verify_not_before,
            verify_not_after,
            with_key_password,
            keys,
        } => {
            let session_key_out: Option<Box<dyn io::Write>> =
                if let Some(f) = session_key_out {
                    Some(Box::new(create_file(f)?))
                } else {
                    None
                };

            if verifications_out.is_none() != verify_with.is_empty() {
                return Err(anyhow::Error::from(Error::IncompleteVerification))
                    .context("--verifications-out and --verify-with \
                              must both be given");
            }

            let mut verify_out: Box<dyn io::Write> =
                if let Some(f) = verifications_out {
                    Box::new(create_file(f)?)
                } else {
                    Box::new(io::sink())
                };

            let mut op = sop.decrypt()?;

            for (_name, mut stream) in load_files(with_session_key)? {
                let mut sk = String::new();
                stream.read_to_string(&mut sk)?;
                op = op.with_session_key(sk.parse()?)?;
            }

            for pw in with_password.into_iter().map(get_password_unchecked) {
                op = op.with_password(pw?)?;
            }

            for p in with_key_password {
                op = op.with_key_password(get_password_unchecked(p)?)?;
            }

            for (name, mut stream) in load_files(keys)? {
                op = op.with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
            }

            for (name, mut stream) in load_files(verify_with)? {
                op = op.verify_with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }

            if let Some(t) = verify_not_before {
                op = op.verify_not_before(t.into());
            }

            if let Some(t) = verify_not_after {
                op = op.verify_not_after(t.into());
            }

            let (session_key, verifications) =
                op.ciphertext(&mut io::stdin())?.to_writer(&mut io::stdout())?;

            for v in verifications {
                writeln!(verify_out, "{}", v)?;
            }
            if let Some(mut sko) = session_key_out {
                if let Some(sk) = session_key {
                    writeln!(sko, "{}", sk)?;
                } else {
                    return Err(Error::UnsupportedSubcommand.into());
                }
            }
        },

        #[cfg(feature = "cli")]
        Operation::Armor { label, } => {
            #[allow(deprecated)]
            let op = sop.armor()?.label(label);
            op.data(&mut io::stdin())?
                .to_writer(&mut io::stdout())?;
        },

        #[cfg(feature = "cli")]
        Operation::Dearmor {} => {
            let op = sop.dearmor()?;
            op.data(&mut io::stdin())?
                .to_writer(&mut io::stdout())?;
        },

        #[cfg(feature = "cli")]
        Operation::InlineDetach { no_armor, signatures_out, } => {
            let op = sop.inline_detach()?;
            let mut signatures_out = create_file(signatures_out)?;
            let signatures =
                op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?;
            signatures.to_writer(! no_armor, &mut signatures_out)?;
        },

        #[cfg(any(feature = "cli", feature = "cliv"))]
        Operation::VerificationSubset(VerificationSubset::InlineVerify {
            not_before,
            not_after,
            verifications_out,
            certs,
        }) => {
            let mut op = sop.inline_verify()?;

            if let Some(t) = not_before {
                op = op.not_before(t.into());
            }
            if let Some(t) = not_after {
                op = op.not_after(t.into());
            }
            let mut verifications_out: Box<dyn io::Write> =
                if let Some(f) = verifications_out {
                    Box::new(create_file(f)?)
                } else {
                    Box::new(io::sink())
                };
            for (name, mut stream) in load_files(certs)? {
                op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
            }
            let verifications =
                op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?;

            for v in verifications {
                writeln!(&mut verifications_out, "{}", v)?;
            }
        },

        #[cfg(feature = "cli")]
        Operation::InlineSign { no_armor, as_, with_key_password, keys, } => {
            let mut op = sop.inline_sign()?.mode(as_);
            if no_armor {
                op = op.no_armor();
            }
            for p in with_key_password {
                op = op.with_key_password(get_password_unchecked(p)?)?;
            }
            for (name, mut stream) in load_files(keys)? {
                op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
            }
            op.data(&mut io::stdin())?.to_writer(&mut io::stdout())?;
        },

        Operation::Unsupported(args) => {
            return Err(anyhow::Error::from(Error::UnsupportedSubcommand))
                .context(format!("Subcommand {} is not supported", args[0]));
        },
    }
    Ok(())
}

fn is_special_designator<S: AsRef<str>>(file: S) -> bool {
    file.as_ref().starts_with("@")
}


/// Checks whether the given descriptor is valid.
#[cfg(unix)]
fn check_fd(fd: std::os::fd::RawFd) -> Result<()> {
    unsafe {
        // Try to dup(2) it.
        let dup = libc::dup(fd);

        if dup > 0 {
            libc::close(dup);
            Ok(())
        } else {
            Err(std::io::Error::last_os_error().into())
        }
    }
}

/// Loads the given (special) file.
fn load_file<S: AsRef<str>>(file: S)
                            -> Result<(String, Box<dyn io::Read + Send + Sync>)> {
    let f = file.as_ref();
    let file_name = f.into();

    if is_special_designator(f) {
        if Path::new(f).exists() {
            return Err(anyhow::Error::from(Error::AmbiguousInput))
                .context(format!("File {:?} exists", f));
        }

        #[cfg(unix)]
        {
            if f.starts_with("@FD:")
                && f[4..].chars().all(|c| c.is_ascii_digit())
            {
                use std::os::unix::io::{RawFd, FromRawFd};

                let fd: RawFd = f[4..].parse()
                    .map_err(|_| Error::UnsupportedSpecialPrefix)?;
                check_fd(fd)?;
                let f = unsafe {
                    std::fs::File::from_raw_fd(fd)
                };
                return Ok((file_name, Box::new(f)));
            }

            if f.starts_with("@ENV:") {
                use std::os::unix::ffi::OsStringExt;

                let key = &f[5..];
                let value = std::env::var_os(key)
                    .ok_or(Error::UnsupportedSpecialPrefix)?;
                // Prevent leak to child processes.
                std::env::remove_var(key);
                return Ok((file_name,
                           Box::new(io::Cursor::new(value.into_vec()))));
            }
        }

        #[cfg(windows)]
        {
            if f.starts_with("@ENV:") {
                let key = &f[5..];
                let value = std::env::var(key)
                    .map_err(|_| Error::UnsupportedSpecialPrefix)?;
                // Prevent leak to child processes.
                std::env::remove_var(key);
                return Ok((file_name,
                           Box::new(io::Cursor::new(value.into_bytes()))));
            }
        }

        return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
    }

    std::fs::File::open(f)
        .map(|f| -> (String, Box<dyn io::Read + Send + Sync>) {
            (file_name, Box::new(f))
        })
        .map_err(|_| Error::MissingInput)
        .context(format!("Failed to open file {:?}", f))
}

/// Gets the password from the given location.
///
/// Use this for passwords given to create artifacts.
///
/// Passwords are indirect data types, meaning that they are
/// loaded from files or special designators.
#[cfg(feature = "cli")]
fn get_password<S: AsRef<str>>(location: S) -> Result<Password> {
    let mut stream = load_file(location)?.1;
    let mut pw = Vec::new();
    stream.read_to_end(&mut pw)?;
    Ok(Password::new(pw)?)
}

/// Gets the password from the given location.
///
/// Use this for passwords given to consume artifacts.
///
/// Passwords are indirect data types, meaning that they are
/// loaded from files or special designators.
#[cfg(feature = "cli")]
fn get_password_unchecked<S: AsRef<str>>(location: S) -> Result<Password> {
    let mut stream = load_file(location)?.1;
    let mut pw = Vec::new();
    stream.read_to_end(&mut pw)?;
    Ok(Password::new_unchecked(pw))
}

/// Creates the given (special) file.
fn create_file<S: AsRef<str>>(file: S) -> Result<std::fs::File> {
    let f = file.as_ref();

    if is_special_designator(f) {
        if Path::new(f).exists() {
            return Err(anyhow::Error::from(Error::AmbiguousInput))
                .context(format!("File {:?} exists", f));
        }

        #[cfg(unix)]
        {
            if f.starts_with("@FD:")
                && f[4..].chars().all(|c| c.is_ascii_digit())
            {
                use std::os::unix::io::{RawFd, FromRawFd};

                let fd: RawFd = f[4..].parse()
                    .map_err(|_| Error::UnsupportedSpecialPrefix)?;
                check_fd(fd)?;
                let f = unsafe {
                    std::fs::File::from_raw_fd(fd)
                };
                return Ok(f);
            }
        }

        return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
    }

    if Path::new(f).exists() {
        return Err(anyhow::Error::from(Error::OutputExists))
            .context(format!("File {:?} exists", f));
    }

    std::fs::File::create(f).map_err(|_| Error::MissingInput) // XXX
            .context(format!("Failed to create file {:?}", f))
}

/// Loads the the (special) files.
fn load_files(files: Vec<String>)
              -> Result<Vec<(String, Box<dyn io::Read + Send + Sync>)>> {
    files.iter().map(load_file).collect()
}

/// Parses the given string depicting a ISO 8601 timestamp, rounding down.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct NotBefore(DateTime<Utc>);

impl std::str::FromStr for NotBefore {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<NotBefore> {
        match s {
            // XXX: parse "-" to None once we figure out how to do that
            // with clap.
            "now" => Ok(Utc::now()),
            _ => parse_iso8601(s, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
        }.map(|t| NotBefore(t))
    }
}

impl From<NotBefore> for std::time::SystemTime {
    fn from(t: NotBefore) -> std::time::SystemTime {
        t.0.into()
    }
}

/// Parses the given string depicting a ISO 8601 timestamp, rounding up.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct NotAfter(DateTime<Utc>);

impl std::str::FromStr for NotAfter {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<NotAfter> {
        match s {
            // XXX: parse "-" to None once we figure out how to do that
            // with clap.
            "now" => Ok(Utc::now()),
            _ => parse_iso8601(s, NaiveTime::from_hms_opt(23, 59, 59).unwrap()),
        }.map(|t| NotAfter(t))
    }
}

impl From<NotAfter> for std::time::SystemTime {
    fn from(t: NotAfter) -> std::time::SystemTime {
        t.0.into()
    }
}

/// Parses the given string depicting a ISO 8601 timestamp.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Timestamp(DateTime<Utc>);

impl std::str::FromStr for Timestamp {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Timestamp> {
        match s {
            // XXX: parse "-" to None once we figure out how to do that
            // with clap.
            "now" => Ok(Utc::now()),
            _ => parse_iso8601(s, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
        }.map(|t| Timestamp(t))
    }
}

impl From<Timestamp> for std::time::SystemTime {
    fn from(t: Timestamp) -> std::time::SystemTime {
        t.0.into()
    }
}

/// Parses the given string depicting a ISO 8601 timestamp.
fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime)
                 -> Result<DateTime<Utc>>
{
    // If you modify this function this function, synchronize the
    // changes with the copy in sqv.rs!
    for f in &[
        "%Y-%m-%dT%H:%M:%S%#z",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%dT%H:%M%#z",
        "%Y-%m-%dT%H:%M",
        "%Y-%m-%dT%H%#z",
        "%Y-%m-%dT%H",
        "%Y%m%dT%H%M%S%#z",
        "%Y%m%dT%H%M%S",
        "%Y%m%dT%H%M%#z",
        "%Y%m%dT%H%M",
        "%Y%m%dT%H%#z",
        "%Y%m%dT%H",
    ] {
        if f.ends_with("%#z") {
            if let Ok(d) = DateTime::parse_from_str(s, *f) {
                return Ok(d.into());
            }
        } else {
            if let Ok(d) = chrono::NaiveDateTime::parse_from_str(s, *f) {
                return Ok(DateTime::from_utc(d, Utc));
            }
        }
    }
    for f in &[
        "%Y-%m-%d",
        "%Y-%m",
        "%Y-%j",
        "%Y%m%d",
        "%Y%m",
        "%Y%j",
        "%Y",
    ] {
        if let Ok(d) = chrono::NaiveDate::parse_from_str(s, *f) {
            return Ok(DateTime::from_utc(d.and_time(pad_date_with), Utc));
        }
    }
    Err(anyhow::anyhow!("Malformed ISO8601 timestamp: {}", s))
}

#[test]
fn test_parse_iso8601() {
    let z = chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap();
    parse_iso8601("2017-03-04T13:25:35Z", z).unwrap();
    parse_iso8601("2017-03-04T13:25:35+08:30", z).unwrap();
    parse_iso8601("2017-03-04T13:25:35", z).unwrap();
    parse_iso8601("2017-03-04T13:25Z", z).unwrap();
    parse_iso8601("2017-03-04T13:25", z).unwrap();
    // parse_iso8601("2017-03-04T13Z", z).unwrap(); // XXX: chrono doesn't like
    // parse_iso8601("2017-03-04T13", z).unwrap(); // ditto
    parse_iso8601("2017-03-04", z).unwrap();
    // parse_iso8601("2017-03", z).unwrap(); // ditto
    parse_iso8601("2017-031", z).unwrap();
    parse_iso8601("20170304T132535Z", z).unwrap();
    parse_iso8601("20170304T132535+0830", z).unwrap();
    parse_iso8601("20170304T132535", z).unwrap();
    parse_iso8601("20170304T1325Z", z).unwrap();
    parse_iso8601("20170304T1325", z).unwrap();
    // parse_iso8601("20170304T13Z", z).unwrap(); // ditto
    // parse_iso8601("20170304T13", z).unwrap(); // ditto
    parse_iso8601("20170304", z).unwrap();
    // parse_iso8601("201703", z).unwrap(); // ditto
    parse_iso8601("2017031", z).unwrap();
    // parse_iso8601("2017", z).unwrap(); // ditto
}

/// Errors defined by the Stateless OpenPGP Command-Line Protocol.
#[derive(thiserror::Error, Debug)]
enum Error {
    /// Unspecified failure.
    #[error("Unspecified failure")]
    UnspecifiedFailure,

    /// No acceptable signatures found ("sop verify").
    #[error("No acceptable signatures found")]
    NoSignature,

    /// Asymmetric algorithm unsupported ("sop encrypt").
    #[error("Asymmetric algorithm unsupported")]
    UnsupportedAsymmetricAlgo,

    /// Certificate not encryption-capable (e.g., expired, revoked,
    /// unacceptable usage flags) ("sop encrypt").
    #[error("Certificate not encryption-capable")]
    CertCannotEncrypt,

    /// Key not signature-capable (e.g., expired, revoked,
    /// unacceptable usage flags) (`sop sign` and `sop encrypt` with
    /// `--sign-with`).
    #[error("Key not signing-capable")]
    KeyCannotSign,

    /// Missing required argument.
    #[error("Missing required argument")]
    MissingArg,

    /// Incomplete verification instructions ("sop decrypt").
    #[error("Incomplete verification instructions")]
    IncompleteVerification,

    /// Unable to decrypt ("sop decrypt").
    #[error("Unable to decrypt")]
    CannotDecrypt,

    /// Non-"UTF-8" or otherwise unreliable password ("sop encrypt").
    #[error("Non-UTF-8 or otherwise unreliable password")]
    PasswordNotHumanReadable,

    /// Unsupported option.
    #[error("Unsupported option")]
    UnsupportedOption,

    /// Invalid data type (no secret key where "KEY" expected, etc).
    #[error("Invalid data type")]
    BadData,

    /// Non-text input where text expected.
    #[error("Non-text input where text expected")]
    ExpectedText,

    /// Output file already exists.
    #[error("Output file already exists")]
    OutputExists,

    /// Input file does not exist.
    #[error("Input file does not exist")]
    MissingInput,

    /// A "KEY" input is protected (locked) with a password, and "sop" cannot
    /// unlock it.
    #[error("A KEY input is protected with a password")]
    KeyIsProtected,

    /// Unsupported subcommand.
    #[error("Unsupported subcommand")]
    UnsupportedSubcommand,

    /// An indirect parameter is a special designator (it starts with "@") but
    /// "sop" does not know how to handle the prefix.
    #[error("An indirect parameter is a special designator with unknown prefix")]
    UnsupportedSpecialPrefix,

    /// A indirect input parameter is a special designator (it starts with
    /// "@"), and a filename matching the designator is actually present.
    #[error("A indirect input parameter is a special designator matches file")]
    AmbiguousInput,

    /// Options were supplied that are incompatible with each other.
    #[error("Options were supplied that are incompatible with each other")]
    IncompatibleOptions,

    /// The requested profile is unsupported (sop generate-key, sop
    /// encrypt), or the indicated subcommand does not accept profiles
    /// (sop list-profiles).
    #[error("Profile not supported")]
    UnsupportedProfile,

    /// The primary key of a KEYS object is too weak or revoked.
    #[error("The primary key of a KEYS object is too weak or revoked")]
    PrimaryKeyBad,

    /// The CERTS object has no matching User ID.
    #[error("The CERTS object has no matching User ID")]
    CertUseridNoMatch,

    /// An I/O operation failed.
    #[error(transparent)]
    IoError(#[from] io::Error),
}

impl From<crate::Error> for Error {
    fn from(e: crate::Error) -> Self {
        use crate::Error as E;
        use Error as CE;
        match e {
            E::UnspecifiedFailure => CE::UnspecifiedFailure,
            E::NoSignature => CE::NoSignature,
            E::UnsupportedAsymmetricAlgo => CE::UnsupportedAsymmetricAlgo,
            E::CertCannotEncrypt => CE::CertCannotEncrypt,
            E::KeyCannotSign => CE::KeyCannotSign,
            E::MissingArg => CE::MissingArg,
            E::IncompleteVerification => CE::IncompleteVerification,
            E::CannotDecrypt => CE::CannotDecrypt,
            E::PasswordNotHumanReadable => CE::PasswordNotHumanReadable,
            E::UnsupportedOption => CE::UnsupportedOption,
            E::BadData => CE::BadData,
            E::ExpectedText => CE::ExpectedText,
            E::OutputExists => CE::OutputExists,
            E::MissingInput => CE::MissingInput,
            E::KeyIsProtected => CE::KeyIsProtected,
            E::AmbiguousInput => CE::AmbiguousInput,
            E::IncompatibleOptions => CE::IncompatibleOptions,
            E::NotImplemented => CE::UnsupportedSubcommand,
            E::UnsupportedProfile => CE::UnsupportedProfile,
            E::PrimaryKeyBad => CE::PrimaryKeyBad,
            E::CertUseridNoMatch => CE::CertUseridNoMatch,
            E::IoError(e) => CE::IoError(e),
        }
    }
}

impl From<Error> for i32 {
    fn from(e: Error) -> Self {
        use Error::*;
        match e {
            UnspecifiedFailure => 1,
            NoSignature => 3,
            UnsupportedAsymmetricAlgo => 13,
            CertCannotEncrypt => 17,
            MissingArg => 19,
            IncompleteVerification => 23,
            CannotDecrypt => 29,
            PasswordNotHumanReadable => 31,
            UnsupportedOption => 37,
            BadData => 41,
            ExpectedText => 53,
            OutputExists => 59,
            MissingInput => 61,
            KeyIsProtected => 67,
            UnsupportedSubcommand => 69,
            UnsupportedSpecialPrefix => 71,
            AmbiguousInput => 73,
            KeyCannotSign => 79,
            IncompatibleOptions => 83,
            UnsupportedProfile => 89,
            PrimaryKeyBad => 103,
            CertUseridNoMatch => 107,
            IoError(_) => 1,
        }
    }
}

/// Prints the error and causes, if any.
fn print_error_chain(err: &anyhow::Error) {
    eprintln!("           {}", err);
    err.chain().skip(1).for_each(|cause| eprintln!("  because: {}", cause));
}
