Replace CLI I/O argument duplication with common facilities

- Replace `sq_cli::types::IoArgs` with the more granular
  `sq_cli::types::FileOrStdin`, `sq_cli::types::FileOrCertStore` and
  `sq_cli::types::FileOrStdout`.
- Replace all generic `input` (describing single files) and `output`
  arguments with the respective new facilities to share code and not
  repeat ourselves.
- Replace the `open_or_stdin()` function with `FileOrStdin::open()`.
- Replace the `create_or_stdout()` function with the private
  `FileOrStdout::create()`, so that it can not be called directly.
- Replace the `emit_unstable_cli_warning()`
  and `create_or_stdout_unsafe()` functions with
  `FileOrStdout::create_unsafe()`.
- Replace the `create_or_stdout_safe()` function with
  `FileOrStdout::create_safe()`.
- Replace the `create_or_stdout_pgp()` function with
  `FileOrStdout::create_pgp_safe()`.
- Remove the field `unstable_cli_warning_emitted` from `Config`, as
  it is replaced by the static `UNSTABLE_CLI_WARNING`, which allows for
  tracking whether a warning has been emitted across several instances
  of `FileOrStdout`.
This commit is contained in:
David Runge 2023-06-07 13:17:13 +02:00
parent 8f57c0d9f2
commit ed6069623b
No known key found for this signature in database
GPG Key ID: BB992F9864FAD168
35 changed files with 915 additions and 399 deletions

View File

@ -38,7 +38,7 @@ sequoia-net = { version = "0.27", default-features = false }
anyhow = "1.0.18"
chrono = "0.4.10"
# For an MSRV of 1.63: 4.0.32.
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] }
itertools = "0.10"
once_cell = "1.17"
sequoia-cert-store = "0.3"
@ -53,8 +53,9 @@ terminal_size = "0.2.6"
[build-dependencies]
anyhow = "1.0.18"
buffered-reader = { version = "1.0.0", default-features = false, features = ["compression-deflate"] }
# For an MSRV of 1.63: 4.0.32
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] }
# For an MSRV of 1.63: 4.0.7.
clap_complete = "4"
# For an MSRV of 1.63: 0.2.6
@ -64,6 +65,7 @@ sequoia-openpgp = { version = "1.13", default-features = false }
sequoia-net = { version = "0.27", default-features = false }
subplot-build = { version = "0.7.0", optional = true }
cfg-if = "1"
terminal_size = "0.2.6"
[dev-dependencies]
subplotlib = "0.7.0"

View File

@ -10,7 +10,6 @@ use sequoia_autocrypt as autocrypt;
use crate::{
Config,
open_or_stdin,
sq_cli,
};
@ -19,9 +18,9 @@ pub fn dispatch(config: Config, c: &sq_cli::autocrypt::Command) -> Result<()> {
match &c.subcommand {
Decode(command) => {
let input = open_or_stdin(command.io.input.as_deref())?;
let mut output = config.create_or_stdout_pgp(
command.io.output.as_deref(),
let input = command.input.open()?;
let mut output = command.output.create_pgp_safe(
config.force,
command.binary,
armor::Kind::PublicKey,
)?;
@ -34,9 +33,8 @@ pub fn dispatch(config: Config, c: &sq_cli::autocrypt::Command) -> Result<()> {
output.finalize()?;
}
EncodeSender(command) => {
let input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let input = command.input.open()?;
let mut output = command.output.create_safe(config.force)?;
let cert = Cert::from_reader(input)?;
let addr = command.address.clone()
.or_else(|| {

View File

@ -150,8 +150,8 @@ pub fn certify(config: Config, c: certify::Command)
// And export it.
let mut message = config.create_or_stdout_pgp(
c.output.as_deref(),
let mut message = c.output.create_pgp_safe(
config.force,
c.binary,
sequoia_openpgp::armor::Kind::PublicKey,
)?;

View File

@ -13,6 +13,7 @@ use sequoia_cert_store as cert_store;
use cert_store::Store;
use cert_store::store::UserIDQueryParams;
use crate::sq_cli::types::FileOrStdout;
use crate::{
Config,
print_error_chain,
@ -76,8 +77,12 @@ pub fn dispatch(config: Config, cmd: export::Command) -> Result<()> {
return Err(anyhow::anyhow!("Invalid arguments."));
}
let mut sink = config.create_or_stdout_pgp(
None, cmd.binary, armor::Kind::PublicKey)?;
let output = FileOrStdout::default();
let mut sink = output.create_pgp_safe(
config.force,
cmd.binary,
armor::Kind::PublicKey,
)?;
let mut exported_something = false;

View File

@ -12,10 +12,8 @@ use sequoia_cert_store as cert_store;
use cert_store::LazyCert;
use cert_store::StoreUpdate;
use crate::{
Config,
open_or_stdin,
};
use crate::sq_cli::types::FileOrStdin;
use crate::Config;
use crate::sq_cli::import;
@ -32,8 +30,7 @@ pub fn dispatch<'store>(mut config: Config<'store>, cmd: import::Command)
let inner = || -> Result<()> {
for input in inputs.into_iter() {
let input = open_or_stdin(
if input == PathBuf::from("-") { None } else { Some(&input) })?;
let input = FileOrStdin::from(input).open()?;
let raw_certs = RawCertParser::from_reader(input)?;
let cert_store = config.cert_store_mut_or_else()?;

View File

@ -25,26 +25,23 @@ use crate::SECONDS_IN_YEAR;
use crate::SECONDS_IN_DAY;
use crate::sq_cli::inspect;
use crate::sq_cli::types::FileOrStdout;
pub fn inspect(mut config: Config, c: inspect::Command)
pub fn inspect(config: Config, c: inspect::Command)
-> Result<()>
{
// sq inspect does not have --output, but commands::inspect does.
// Work around this mismatch by always creating a stdout output.
let output = &mut config.create_or_stdout_unsafe(None)?;
let output_type = FileOrStdout::default();
let output = &mut output_type.create_unsafe(config.force)?;
let policy = &config.policy;
let time = Some(config.time);
let print_certifications = c.certifications;
let input = c.input.as_deref();
let input_name = if let Some(input) = c.input.as_ref() {
format!("{}", input.display())
} else {
"-".to_string()
};
write!(output, "{}: ", input_name)?;
let input = c.input;
write!(output, "{}: ", input)?;
let mut type_called = false; // Did we print the type yet?
let mut encrypted = false; // Is it an encrypted message?
@ -56,16 +53,16 @@ pub fn inspect(mut config: Config, c: inspect::Command)
let mut bytes: Vec<u8> = Vec::new();
let mut ppr = if c.cert.is_empty() {
if let Some(input) = input {
if ! input.exists() &&
format!("{}", input.display()).parse::<KeyHandle>().is_ok() {
if let Some(path) = input.inner() {
if ! path.exists() &&
format!("{}", input).parse::<KeyHandle>().is_ok() {
eprintln!("The file {} does not exist, \
did you mean \"sq inspect --cert {}\"?",
input.display(), input.display());
input, input);
}
}
openpgp::parse::PacketParser::from_reader(crate::open_or_stdin(input)?)?
openpgp::parse::PacketParser::from_reader(input.open()?)?
} else {
let cert_store = config.cert_store_or_else()?;
for cert in c.cert.into_iter() {
@ -174,7 +171,7 @@ pub fn inspect(mut config: Config, c: inspect::Command)
writeln!(output, " Cert: {}", is_cert.unwrap_err())?;
writeln!(output, " Keyring: {}", is_keyring.unwrap_err())?;
writeln!(output)?;
writeln!(output, "Hint: Try 'sq packet dump {}'", input_name)?;
writeln!(output, "Hint: Try 'sq packet dump {}'", input)?;
}
} else {
unreachable!()

View File

@ -20,12 +20,11 @@ use openpgp::Result;
use sequoia_openpgp as openpgp;
use crate::decrypt_key;
use crate::open_or_stdin;
use crate::sq_cli;
use crate::Config;
pub fn adopt(config: Config, command: sq_cli::key::AdoptCommand) -> Result<()> {
let input = open_or_stdin(command.certificate.as_deref())?;
let input = command.certificate.open()?;
let cert = Cert::from_reader(input)?;
let mut wanted: Vec<(
KeyHandle,
@ -209,7 +208,7 @@ pub fn adopt(config: Config, command: sq_cli::key::AdoptCommand) -> Result<()> {
let cert = cert.clone().insert_packets(packets.clone())?;
let mut sink = config.create_or_stdout_safe(command.output.as_deref())?;
let mut sink = command.output.create_safe(config.force)?;
if command.binary {
cert.as_tsk().serialize(&mut sink)?;
} else {

View File

@ -5,7 +5,6 @@ use openpgp::serialize::Serialize;
use sequoia_openpgp as openpgp;
use crate::decrypt_key;
use crate::open_or_stdin;
use crate::sq_cli;
use crate::Config;
@ -16,7 +15,7 @@ pub fn attest_certifications(
// Attest to all certifications?
let all = !command.none; // All is the default.
let input = open_or_stdin(command.key.as_deref())?;
let input = command.key.open()?;
let key = Cert::from_reader(input)?;
// Get a signer.
@ -63,7 +62,7 @@ pub fn attest_certifications(
// Finally, add the new signatures.
let key = key.insert_packets(attestation_signatures)?;
let mut sink = config.create_or_stdout_safe(command.output.as_deref())?;
let mut sink = command.output.create_safe(config.force)?;
if command.binary {
key.as_tsk().serialize(&mut sink)?;
} else {

View File

@ -4,7 +4,6 @@ use openpgp::Cert;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use crate::open_or_stdin;
use crate::sq_cli;
use crate::Config;
@ -12,9 +11,8 @@ pub fn extract_cert(
config: Config,
command: sq_cli::key::ExtractCertCommand,
) -> Result<()> {
let input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let input = command.input.open()?;
let mut output = command.output.create_safe(config.force)?;
let cert = Cert::from_reader(input)?;
if command.binary {

View File

@ -14,6 +14,7 @@ use sequoia_openpgp as openpgp;
use crate::sq_cli;
use crate::Config;
use crate::sq_cli::types::FileOrStdout;
pub fn generate(
config: Config,
@ -123,7 +124,7 @@ pub fn generate(
let (cert, rev) = builder.generate()?;
// Export
if let Some(key_path) = command.export.as_ref() {
if let Some(key_path) = command.export {
if &format!("{}", key_path.display()) == "-"
&& command.rev_cert.is_none()
{
@ -132,10 +133,12 @@ pub fn generate(
))
}
let key_path = FileOrStdout::from(key_path);
let rev_path = if command.rev_cert.is_some() {
command.rev_cert
FileOrStdout::new(command.rev_cert)
} else {
Some(PathBuf::from(format!("{}.rev", key_path.display())))
FileOrStdout::from(PathBuf::from(format!("{}.rev", key_path)))
};
let headers = cert.armor_headers();
@ -147,7 +150,7 @@ pub fn generate(
.map(|value| ("Comment", value.as_str()))
.collect();
let w = config.create_or_stdout_safe(Some(key_path))?;
let w = key_path.create_safe(config.force)?;
let mut w = Writer::with_headers(w, Kind::SecretKey, headers)?;
cert.as_tsk().serialize(&mut w)?;
w.finalize()?;
@ -161,7 +164,7 @@ pub fn generate(
.collect();
headers.insert(0, ("Comment", "Revocation certificate for"));
let w = config.create_or_stdout_safe(rev_path.as_deref())?;
let w = rev_path.create_safe(config.force)?;
let mut w = Writer::with_headers(w, Kind::Signature, headers)?;
Packet::Signature(rev).serialize(&mut w)?;
w.finalize()?;

View File

@ -8,7 +8,6 @@ use openpgp::Result;
use sequoia_openpgp as openpgp;
use crate::decrypt_key;
use crate::open_or_stdin;
use crate::sq_cli;
use crate::Config;
@ -16,7 +15,7 @@ pub fn password(
config: Config,
command: sq_cli::key::PasswordCommand,
) -> Result<()> {
let input = open_or_stdin(command.io.input.as_deref())?;
let input = command.input.open()?;
let key = Cert::from_reader(input)?;
if !key.is_tsk() {
@ -82,8 +81,7 @@ pub fn password(
key = key.insert_packets(encrypted)?;
}
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut output = command.output.create_safe(config.force)?;
if command.binary {
key.as_tsk().serialize(&mut output)?;
} else {

View File

@ -9,7 +9,6 @@ use openpgp::Cert;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use crate::open_or_stdin;
use crate::sq_cli::key::EncryptPurpose;
use crate::sq_cli::key::SubkeyCommand;
use crate::sq_cli::key::SubkeyAddCommand;
@ -32,7 +31,7 @@ fn subkey_add(
config: Config,
command: SubkeyAddCommand,
) -> Result<()> {
let input = open_or_stdin(command.io.input.as_deref())?;
let input = command.input.open()?;
let cert = Cert::from_reader(input)?;
let valid_cert = cert.with_policy(&config.policy, config.time)?;
@ -59,8 +58,7 @@ fn subkey_add(
.set_key_validity_period(validity)?
.attach_cert()?;
let mut sink =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut sink = command.output.create_safe(config.force)?;
if command.binary {
new_cert.as_tsk().serialize(&mut sink)?;
} else {

View File

@ -19,7 +19,6 @@ use openpgp::Result;
use sequoia_openpgp as openpgp;
use crate::commands::get_primary_keys;
use crate::open_or_stdin;
use crate::sq_cli;
use crate::Config;
@ -39,7 +38,7 @@ fn userid_add(
config: Config,
command: sq_cli::key::UseridAddCommand,
) -> Result<()> {
let input = open_or_stdin(command.io.input.as_deref())?;
let input = command.input.open()?;
let key = Cert::from_reader(input)?;
// Fail if any of the User IDs to add already exist in the ValidCert
@ -183,8 +182,7 @@ fn userid_add(
// Merge additional User IDs into key
let cert = key.insert_packets(add)?;
let mut sink =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut sink = command.output.create_safe(config.force)?;
if command.binary {
cert.as_tsk().serialize(&mut sink)?;
} else {
@ -197,7 +195,7 @@ fn userid_strip(
config: Config,
command: sq_cli::key::UseridStripCommand,
) -> Result<()> {
let input = open_or_stdin(command.io.input.as_deref())?;
let input = command.input.open()?;
let key = Cert::from_reader(input)?;
let orig_cert_valid = key.with_policy(&config.policy, None).is_ok();
@ -238,8 +236,7 @@ signatures on other User IDs to make the key valid again.",
}
}
let mut sink =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut sink = command.output.create_safe(config.force)?;
if command.binary {
cert.as_tsk().serialize(&mut sink)?;
} else {

View File

@ -30,7 +30,6 @@ use openpgp::{
use crate::{
Config,
Model,
open_or_stdin,
output::KeyringListItem,
};
@ -138,10 +137,11 @@ pub fn dispatch(config: Config, c: keyring::Command) -> Result<()> {
// better to use Kind::SecretKey here. However, this
// requires buffering all certs, which has its own
// problems.
let mut output =
config.create_or_stdout_pgp(command.output.as_deref(),
command.binary,
armor::Kind::PublicKey)?;
let mut output = command.output.create_pgp_safe(
config.force,
command.binary,
armor::Kind::PublicKey,
)?;
filter(&command.input, &mut output, filter_fn, to_certificate)?;
output.finalize()
},
@ -151,35 +151,38 @@ pub fn dispatch(config: Config, c: keyring::Command) -> Result<()> {
// better to use Kind::SecretKey here. However, this
// requires buffering all certs, which has its own
// problems.
let mut output =
config.create_or_stdout_pgp(c.output.as_deref(),
c.binary,
armor::Kind::PublicKey)?;
let mut output = c.output.create_pgp_safe(
config.force,
c.binary,
armor::Kind::PublicKey,
)?;
filter(&c.input, &mut output, Some, false)?;
output.finalize()
},
Merge(c) => {
let mut output =
config.create_or_stdout_pgp(c.output.as_deref(),
c.binary,
armor::Kind::PublicKey)?;
let mut output = c.output.create_pgp_safe(
config.force,
c.binary,
armor::Kind::PublicKey,
)?;
merge(&c.input, &mut output)?;
output.finalize()
},
List(c) => {
let mut input = open_or_stdin(c.input.as_deref())?;
let mut input = c.input.open()?;
list(config, &mut input, c.all_userids)
},
Split(c) => {
let mut input = open_or_stdin(c.input.as_deref())?;
let mut input = c.input.open()?;
let prefix =
// The prefix is either specified explicitly...
c.prefix.unwrap_or(
// ... or we derive it from the input file...
c.input.and_then(|i| {
let p = PathBuf::from(i);
c.input.and_then(|x| {
// (but only use the filename)
p.file_name().map(|f| String::from(f.to_string_lossy()))
x.file_name().map(|f|
String::from(f.to_string_lossy())
)
})
// ... or we use a generic prefix...
.unwrap_or_else(|| String::from("output"))

View File

@ -4,7 +4,6 @@ use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
use std::time::SystemTime;
use sequoia_net::pks;
@ -39,6 +38,7 @@ use cert_store::Store;
use sequoia_wot::store::Store as _;
use crate::sq_cli::types::FileOrStdout;
use crate::{
Config,
};
@ -946,25 +946,20 @@ pub fn join(config: Config, c: packet::JoinCommand) -> Result<()> {
let kind = c.kind.into();
let output = c.output;
let mut sink = if c.binary {
// TODO: Does this mean kind is silently ignored if binary is given?
// No need for any auto-detection.
Some(config.create_or_stdout_pgp(output.as_deref(),
true, // Binary.
armor::Kind::File)?)
} else if let Some(kind) = kind {
Some(config.create_or_stdout_pgp(output.as_deref(),
false, // Armored.
kind)?)
} else {
None // Defer.
};
// No need for any auto-detection.
Some(output.create_pgp_safe(config.force, true, armor::Kind::File)?)
} else if let Some(kind) = kind {
Some(output.create_pgp_safe(config.force, false, kind)?)
} else {
None // Defer.
};
/// Writes a bit-accurate copy of all top-level packets in PPR to
/// OUTPUT.
fn copy(config: &Config,
fn copy<'a, 'b>(config: &Config,
mut ppr: PacketParserResult,
output: Option<&Path>,
sink: &mut Option<Message>)
output: &'a FileOrStdout,
sink: &'b mut Option<Message<'a>>)
-> Result<()> {
while let PacketParserResult::Some(pp) = ppr {
if sink.is_none() {
@ -978,9 +973,9 @@ pub fn join(config: Config, c: packet::JoinCommand) -> Result<()> {
_ => armor::Kind::File,
};
*sink = Some(config.create_or_stdout_pgp(output,
false, // Armored.
kind)?);
*sink = Some(
output.create_pgp_safe(config.force, false, kind)?
);
}
// We (ab)use the mapping feature to create byte-accurate
@ -1000,13 +995,13 @@ pub fn join(config: Config, c: packet::JoinCommand) -> Result<()> {
let ppr =
openpgp::parse::PacketParserBuilder::from_file(name)?
.map(true).build()?;
copy(&config, ppr, output.as_deref(), &mut sink)?;
copy(&config, ppr, &output, &mut sink)?;
}
} else {
let ppr =
openpgp::parse::PacketParserBuilder::from_reader(io::stdin())?
.map(true).build()?;
copy(&config, ppr, output.as_deref(), &mut sink)?;
copy(&config, ppr, &output, &mut sink)?;
}
sink.unwrap().finalize()?;

View File

@ -42,13 +42,13 @@ use crate::{
},
Config,
Model,
open_or_stdin,
serialize_keyring,
output::WkdUrlVariant,
print_error_chain,
};
use crate::sq_cli;
use crate::sq_cli::types::FileOrStdout;
const NP: NullPolicy = NullPolicy::new();
@ -409,9 +409,9 @@ pub fn dispatch_keyserver(mut config: Config, c: sq_cli::keyserver::Command)
let cert = rt.block_on(ks.get(handle))
.context("Failed to retrieve cert")?;
if let Some(output) = c.output {
let mut output =
config.create_or_stdout_safe(Some(&output))?;
if c.output.path().is_some() {
let mut output = FileOrStdout::from(c.output)
.create_safe(config.force)?;
if !c.binary {
cert.armored().serialize(&mut output)
} else {
@ -432,9 +432,9 @@ pub fn dispatch_keyserver(mut config: Config, c: sq_cli::keyserver::Command)
let certs = rt.block_on(ks.search(addr))
.context("Failed to retrieve certs")?;
if let Some(output) = c.output {
let mut output =
config.create_or_stdout_safe(Some(&output))?;
if c.output.path().is_some() {
let mut output = FileOrStdout::from(c.output)
.create_safe(config.force)?;
serialize_keyring(&mut output, &certs, c.binary)?;
} else {
let certs = if let Some((ca_filename, ca_userid)) = ca() {
@ -454,7 +454,7 @@ pub fn dispatch_keyserver(mut config: Config, c: sq_cli::keyserver::Command)
}
},
Send(c) => {
let mut input = open_or_stdin(c.input.as_deref())?;
let mut input = c.input.open()?;
let cert = Cert::from_reader(&mut input).
context("Malformed key")?;
@ -514,9 +514,9 @@ pub fn dispatch_wkd(mut config: Config, c: sq_cli::wkd::Command) -> Result<()> {
// ```
// But to keep the parallelism with `store export` and `keyserver get`,
// The output is armored if not `--binary` option is given.
if let Some(output) = c.output {
let mut output =
config.create_or_stdout_safe(Some(&output))?;
if c.output.path().is_some() {
let mut output = FileOrStdout::from(c.output)
.create_safe(config.force)?;
serialize_keyring(&mut output, &certs, c.binary)?;
} else {
let certs = certify_downloads(
@ -528,7 +528,7 @@ pub fn dispatch_wkd(mut config: Config, c: sq_cli::wkd::Command) -> Result<()> {
Generate(c) => {
let domain = c.domain;
let skip = c.skip;
let f = open_or_stdin(c.input.as_deref())?;
let f = c.input.open()?;
let base_path = c.base_directory;
let variant = if c.direct_method {
wkd::Variant::Direct
@ -587,9 +587,9 @@ pub fn dispatch_dane(mut config: Config, c: sq_cli::dane::Command) -> Result<()>
// Because it might be created a WkdServer struct, not
// doing it for now.
let certs = rt.block_on(dane::get(&email_address))?;
if let Some(output) = c.output {
let mut output =
config.create_or_stdout_safe(Some(&output))?;
if c.output.path().is_some() {
let mut output = FileOrStdout::from(c.output)
.create_safe(config.force)?;
serialize_keyring(&mut output, &certs, c.binary)?;
} else {
let certs = certify_downloads(

View File

@ -15,11 +15,12 @@ use openpgp::Result;
use openpgp::serialize::Serialize;
use openpgp::types::KeyFlags;
use openpgp::types::ReasonForRevocation;
use crate::sq_cli::types::FileOrStdin;
use crate::sq_cli::types::FileOrStdout;
use crate::{
commands::cert_stub,
Config,
load_certs,
open_or_stdin,
parse_notations,
};
@ -147,7 +148,7 @@ pub fn revoke_userid(config: Config, c: revoke::UseridCommand) -> Result<()> {
/// Parse the cert from input and ensure it is only one cert.
fn read_cert(input: Option<&Path>) -> Result<Cert> {
let input = open_or_stdin(input)?;
let input = FileOrStdin::from(input).open()?;
let cert = CertParser::from_reader(input)?.collect::<Vec<_>>();
let cert = match cert.len() {
@ -182,7 +183,8 @@ fn revoke(config: Config,
notations: &[(bool, NotationData)])
-> Result<()>
{
let mut output = config.create_or_stdout_safe(None)?;
let output_type = FileOrStdout::default();
let mut output = output_type.create_safe(config.force)?;
let (secret, mut signer) = if let Some(secret) = secret.as_ref() {
if let Ok(keys) = super::get_certification_keys(

View File

@ -1,7 +1,6 @@
use anyhow::Context as _;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use tempfile::NamedTempFile;
@ -20,6 +19,7 @@ use openpgp::serialize::stream::{
Message, Armorer, Signer, LiteralWriter,
};
use openpgp::types::SignatureType;
use crate::sq_cli::types::FileOrStdout;
use crate::{
Config,
};
@ -28,7 +28,7 @@ pub struct SignOpts<'a, 'certdb> {
pub config: Config<'certdb>,
pub private_key_store: Option<&'a str>,
pub input: &'a mut (dyn io::Read + Sync + Send),
pub output_path: Option<&'a Path>,
pub output_path: &'a FileOrStdout,
pub secrets: Vec<openpgp::Cert>,
pub detached: bool,
pub binary: bool,
@ -61,11 +61,12 @@ fn sign_data(opts: SignOpts) -> Result<()> {
notations, ..} = opts;
let (mut output, prepend_sigs, tmp_path):
(Box<dyn io::Write + Sync + Send>, Vec<Signature>, Option<PathBuf>) =
if detached && append && output_path.is_some() {
if detached && append && output_path.path().is_some() {
let output_path = output_path.path().unwrap();
// First, read the existing signatures.
let mut sigs = Vec::new();
let mut ppr =
openpgp::parse::PacketParser::from_file(output_path.unwrap())?;
openpgp::parse::PacketParser::from_file(output_path)?;
while let PacketParserResult::Some(pp) = ppr {
let (packet, ppr_tmp) = pp.recurse()?;
@ -84,12 +85,12 @@ fn sign_data(opts: SignOpts) -> Result<()> {
// successful with adding our signature(s), we rename the
// file replacing the old one.
let tmp_file = NamedTempFile::new_in(
PathBuf::from(output_path.unwrap()).parent()
PathBuf::from(output_path).parent()
.unwrap_or(&PathBuf::from(".")))?;
let tmp_path = tmp_file.path().into();
(Box::new(tmp_file), sigs, Some(tmp_path))
} else {
(config.create_or_stdout_safe(output_path)?, Vec::new(), None)
(output_path.create_safe(config.force)?, Vec::new(), None)
};
let mut keypairs = super::get_signing_keys(
@ -159,17 +160,20 @@ fn sign_data(opts: SignOpts) -> Result<()> {
if let Some(path) = tmp_path {
// Atomically replace the old file.
fs::rename(path,
output_path.expect("must be Some if tmp_path is Some"))?;
fs::rename(
path,
output_path.path().expect("must be Some if tmp_path is Some"),
)?;
}
Ok(())
}
fn sign_message(opts: SignOpts) -> Result<()> {
let mut output =
opts.config.create_or_stdout_pgp(opts.output_path,
opts.binary,
armor::Kind::Message)?;
let mut output = opts.output_path.create_pgp_safe(
opts.config.force,
opts.binary,
armor::Kind::Message,
)?;
sign_message_(opts, &mut output)?;
output.finalize()?;
Ok(())

180
src/sq.rs
View File

@ -7,10 +7,10 @@
#![doc = include_str!(concat!(env!("OUT_DIR"), "/sq-usage.md"))]
use anyhow::Context as _;
use sq_cli::types::FileOrStdin;
use std::borrow::Borrow;
use std::borrow::Cow;
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
@ -21,7 +21,7 @@ use once_cell::unsync::OnceCell;
use terminal_size::terminal_size;
use buffered_reader::{BufferedReader, Dup, File, Generic, Limitor};
use buffered_reader::{BufferedReader, Dup, File, Limitor};
use sequoia_openpgp as openpgp;
use openpgp::{
@ -36,7 +36,7 @@ use openpgp::packet::prelude::*;
use openpgp::parse::{Parse, PacketParser, PacketParserResult};
use openpgp::packet::signature::subpacket::NotationData;
use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::{Serialize, stream::{Message, Armorer}};
use openpgp::serialize::Serialize;
use openpgp::cert::prelude::*;
use openpgp::policy::StandardPolicy as P;
use openpgp::serialize::SerializeInto;
@ -70,17 +70,6 @@ mod commands;
pub mod output;
pub use output::{wkd::WkdUrlVariant, Model, OutputFormat, OutputVersion};
fn open_or_stdin(f: Option<&Path>)
-> Result<Box<dyn BufferedReader<()>>> {
match f {
Some(f) => Ok(Box::new(
File::open(f)
.with_context(|| format!("Failed to open {}", f.display()))?)),
None => Ok(Box::new(Generic::new(io::stdin(), None))),
}
}
/// Loads one TSK from every given file.
fn load_keys<'a, I>(files: I) -> openpgp::Result<Vec<Cert>>
where I: Iterator<Item=&'a Path>
@ -250,37 +239,12 @@ fn help_warning(arg: &str) {
}
}
/// Prints a warning if sq is run in a non-interactive setting without
/// a terminal.
///
/// Detecting non-interactive use is done using a heuristic.
fn emit_unstable_cli_warning() {
if terminal_size().is_some() {
// stdout is connected to a terminal, assume interactive use.
return;
}
// For bash shells, we can use a very simple heuristic. We simply
// look at whether the COLUMNS variable is defined in our
// environment.
if std::env::var_os("COLUMNS").is_some() {
// Heuristic detected interactive use.
return;
}
eprintln!("\nWARNING: sq does not have a stable CLI interface. \
Use with caution in scripts.\n");
}
pub struct Config<'a> {
force: bool,
output_format: OutputFormat,
output_version: Option<OutputVersion>,
policy: P<'a>,
time: SystemTime,
/// Have we emitted the warning yet?
unstable_cli_warning_emitted: bool,
// --no-cert-store
no_rw_cert_store: bool,
cert_store_path: Option<PathBuf>,
@ -297,73 +261,6 @@ pub struct Config<'a> {
}
impl<'store> Config<'store> {
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use.
///
/// This is suitable for any kind of OpenPGP data, or decrypted or
/// authenticated payloads.
fn create_or_stdout_safe(&self, f: Option<&Path>)
-> Result<Box<dyn io::Write + Sync + Send>> {
Config::create_or_stdout(f, self.force)
}
/// Opens the file (or stdout) for writing data that is NOT safe
/// for non-interactive use.
///
/// If our heuristic detects non-interactive use, we will emit a
/// warning.
fn create_or_stdout_unsafe(&mut self, f: Option<&Path>)
-> Result<Box<dyn io::Write + Sync + Send>> {
if ! self.unstable_cli_warning_emitted {
emit_unstable_cli_warning();
self.unstable_cli_warning_emitted = true;
}
Config::create_or_stdout(f, self.force)
}
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use because it is an OpenPGP data stream.
fn create_or_stdout_pgp<'a>(&self, f: Option<&Path>,
binary: bool, kind: armor::Kind)
-> Result<Message<'a>> {
let sink = self.create_or_stdout_safe(f)?;
let mut message = Message::new(sink);
if ! binary {
message = Armorer::new(message).kind(kind).build()?;
}
Ok(message)
}
/// Helper function, do not use directly. Instead, use create_or_stdout_safe
/// or create_or_stdout_unsafe.
fn create_or_stdout(
f: Option<&Path>,
force: bool,
) -> Result<Box<dyn io::Write + Sync + Send>> {
match f {
None => Ok(Box::new(io::stdout())),
Some(p) if p == Path::new("-") => Ok(Box::new(io::stdout())),
Some(f) => {
if !f.exists() || force {
Ok(Box::new(
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(f)
.context("Failed to create output file")?,
))
} else {
Err(anyhow::anyhow!(format!(
"File {:?} exists, use \"sq --force ...\" to \
overwrite",
f.display()
)))
}
}
}
}
/// Returns the cert store's base directory, if it is enabled.
fn cert_store_base(&self) -> Option<PathBuf> {
if self.no_rw_cert_store {
@ -1118,13 +1015,12 @@ fn main() -> Result<()> {
None
};
let mut config = Config {
let config = Config {
force,
output_format,
output_version,
policy: policy.clone(),
time,
unstable_cli_warning_emitted: false,
no_rw_cert_store: c.no_cert_store,
cert_store_path: c.cert_store.clone(),
pep_cert_store_path: c.pep_cert_store.clone(),
@ -1147,9 +1043,8 @@ fn main() -> Result<()> {
SqSubcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut input = command.input.open()?;
let mut output = command.output.create_safe(config.force)?;
let certs = load_certs(
command.sender_cert_file.iter().map(|s| s.as_ref()),
@ -1197,10 +1092,10 @@ fn main() -> Result<()> {
recipients.extend(
config.lookup_by_userid(&command.recipients_userid, false)
.context("--recipient-userid")?);
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut input = command.input.open()?;
let output = config.create_or_stdout_pgp(
command.io.output.as_deref(),
let output = command.output.create_pgp_safe(
config.force,
command.binary,
armor::Kind::Message,
)?;
@ -1224,8 +1119,8 @@ fn main() -> Result<()> {
})?;
},
SqSubcommands::Sign(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let output = command.io.output.as_deref();
let mut input = command.input.open()?;
let output = &command.output;
let detached = command.detached;
let binary = command.binary;
let append = command.append;
@ -1238,12 +1133,16 @@ fn main() -> Result<()> {
let notations = parse_notations(command.notation)?;
if let Some(merge) = command.merge {
let output = config.create_or_stdout_pgp(output, binary,
armor::Kind::Message)?;
let mut input2 = open_or_stdin(Some(&merge))?;
let output = output.create_pgp_safe(
config.force,
binary,
armor::Kind::Message,
)?;
let data: FileOrStdin = merge.into();
let mut input2 = data.open()?;
commands::merge_signatures(&mut input, &mut input2, output)?;
} else if command.clearsign {
let output = config.create_or_stdout_safe(output)?;
let output = output.create_safe(config.force)?;
commands::sign::clearsign(config, private_key_store, input, output, secrets,
time, &notations)?;
} else {
@ -1263,10 +1162,8 @@ fn main() -> Result<()> {
}
},
SqSubcommands::Verify(command) => {
// TODO: Fix interface of open_or_stdin, create_or_stdout_safe, etc.
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut input = command.input.open()?;
let mut output = command.output.create_safe(config.force)?;
let mut detached = if let Some(f) = command.detached {
Some(File::open(f)?)
} else {
@ -1289,7 +1186,7 @@ fn main() -> Result<()> {
// TODO: Extract body to commands/armor.rs
SqSubcommands::Armor(command) => {
let input = open_or_stdin(command.io.input.as_deref())?;
let input = command.input.open()?;
let mut want_kind: Option<armor::Kind> = command.kind.into();
// Peek at the data. If it looks like it is armored
@ -1308,8 +1205,7 @@ fn main() -> Result<()> {
&& (want_kind.is_none() || want_kind == have_kind)
{
// It is already armored and has the correct kind.
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut output = command.output.create_safe(c.force)?;
io::copy(&mut input, &mut output)?;
return Ok(());
}
@ -1324,8 +1220,7 @@ fn main() -> Result<()> {
let want_kind = want_kind.expect("given or detected");
let mut output =
config.create_or_stdout_pgp(command.io.output.as_deref(),
false, want_kind)?;
command.output.create_pgp_safe(config.force, false, want_kind)?;
if already_armored {
// Dearmor and copy to change the type.
@ -1339,9 +1234,8 @@ fn main() -> Result<()> {
output.finalize()?;
},
SqSubcommands::Dearmor(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut input = command.input.open()?;
let mut output = command.output.create_safe(config.force)?;
let mut filter = armor::Reader::from_reader(&mut input, None);
io::copy(&mut filter, &mut output)?;
},
@ -1367,10 +1261,9 @@ fn main() -> Result<()> {
SqSubcommands::Packet(command) => match command.subcommand {
packet::Subcommands::Dump(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output = config.create_or_stdout_unsafe(
command.io.output.as_deref(),
)?;
let mut input = command.input.open()?;
let output_type = command.output;
let mut output = output_type.create_unsafe(config.force)?;
let session_key = command.session_key;
let width = if let Some((width, _)) = terminal_size() {
@ -1384,9 +1277,9 @@ fn main() -> Result<()> {
},
packet::Subcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output = config.create_or_stdout_pgp(
command.io.output.as_deref(),
let mut input = command.input.open()?;
let mut output = command.output.create_pgp_safe(
config.force,
command.binary,
armor::Kind::Message,
)?;
@ -1404,15 +1297,16 @@ fn main() -> Result<()> {
},
packet::Subcommands::Split(command) => {
let mut input = open_or_stdin(command.input.as_deref())?;
let mut input = command.input.open()?;
let prefix =
// The prefix is either specified explicitly...
command.prefix.unwrap_or(
// ... or we derive it from the input file...
command.input.and_then(|i| {
let p = PathBuf::from(i);
command.input.and_then(|x| {
// (but only use the filename)
p.file_name().map(|f| String::from(f.to_string_lossy()))
x.file_name().map(|f|
String::from(f.to_string_lossy())
)
})
// ... or we use a generic prefix...
.unwrap_or_else(|| String::from("output"))

View File

@ -1,6 +1,9 @@
use clap::Parser;
use crate::sq_cli::types::{ArmorKind, IoArgs};
use super::types::ArmorKind;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
// TODO?: Option<_> conflicts with default value
// TODO: use indoc to transparently (de-)indent static strings
@ -29,8 +32,20 @@ $ sq armor binary-message.pgp
"
)]
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
long = "label",
value_name = "LABEL",

View File

@ -1,6 +1,8 @@
use clap::{ValueEnum, Args, Parser, Subcommand};
use crate::sq_cli::types::IoArgs;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -47,8 +49,20 @@ $ sq autocrypt decode autocrypt.eml
"
)]
pub struct DecodeCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(short = 'B', long, help = "Emits binary data")]
pub binary: bool,
}
@ -81,8 +95,20 @@ $ sq autocrypt encode-sender --prefer-encrypt mutual juliet.pgp
"
)]
pub struct EncodeSenderCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
// TODO the help message looks like "primary userid" might be the default
// email. clarify
#[clap(

View File

@ -5,7 +5,9 @@ use clap::Parser;
use crate::sq_cli::THIRD_PARTY_CERTIFICATION_VALIDITY_DURATION;
use crate::sq_cli::THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS;
use super::types::ClapData;
use super::types::Expiry;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -49,12 +51,13 @@ $ sq certify --time 20130721 neal.pgp ada.pgp Ada
)]
pub struct Command {
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
short = 'B',
long,

View File

@ -1,9 +1,10 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::sq_cli::types::NetworkPolicy;
use super::types::ClapData;
use super::types::FileOrCertStore;
#[derive(Parser, Debug)]
#[clap(
name = "dane",
@ -63,10 +64,11 @@ pub struct GetCommand {
)]
pub binary: bool,
#[clap(
short,
default_value_t = FileOrCertStore::default(),
help = FileOrCertStore::HELP,
long,
value_name = "FILE",
help = "Writes to FILE instead of importing into the certificate store"
short,
value_name = FileOrCertStore::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrCertStore,
}

View File

@ -1,6 +1,8 @@
use clap::Parser;
use crate::sq_cli::types::IoArgs;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -28,6 +30,18 @@ $ sq dearmor ascii-message.pgp
",
)]
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
}

View File

@ -2,7 +2,10 @@ use std::path::PathBuf;
use clap::Parser;
use crate::sq_cli::types::{IoArgs, SessionKey};
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
use super::types::SessionKey;
#[derive(Parser, Debug)]
#[clap(
@ -44,8 +47,20 @@ $ sq decrypt ciphertext.pgp
)]
// TODO use usize
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
short = 'n',
long = "signatures",

View File

@ -6,7 +6,9 @@ use clap::{ValueEnum, Parser};
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use crate::sq_cli::types::IoArgs;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -39,8 +41,20 @@ $ sq encrypt --symmetric message.txt
",
)]
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
short = 'B',
long,

View File

@ -1,10 +1,11 @@
use std::path::PathBuf;
use clap::Parser;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use super::types::ClapData;
use super::types::FileOrStdin;
#[derive(Parser, Debug)]
#[clap(
name = "inspect",
@ -42,10 +43,11 @@ $ sq inspect --time 20130721 cert.pgp
)]
pub struct Command {
#[clap(
value_name = "FILE",
help = "Reads from FILE or stdin if omitted",
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: Option<PathBuf>,
pub input: FileOrStdin,
#[clap(
long = "cert",
value_name = "FINGERPRINT|KEYID",

View File

@ -4,7 +4,9 @@ use clap::{ValueEnum, ArgGroup, Args, Parser, Subcommand};
use sequoia_openpgp::cert::CipherSuite as SqCipherSuite;
use crate::sq_cli::types::IoArgs;
use crate::sq_cli::types::ClapData;
use crate::sq_cli::types::FileOrStdin;
use crate::sq_cli::types::FileOrStdout;
use crate::sq_cli::types::Expiry;
use crate::sq_cli::types::Time;
use crate::sq_cli::KEY_VALIDITY_DURATION;
@ -249,8 +251,20 @@ $ sq key password --clear < juliet.encrypted_key.pgp > juliet.decrypted_key.pgp
",
)]
pub struct PasswordCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
long = "clear",
help = "Emit a key with unencrypted secrets",
@ -286,8 +300,20 @@ $ sq key extract-cert --output juliet.cert.pgp juliet.key.pgp
",
)]
pub struct ExtractCertCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
short = 'B',
long,
@ -344,8 +370,20 @@ $ sq key userid add --userid \"Juliet\" --creation-time 20210628 \\
",
)]
pub struct UseridAddCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
value_name = "USERID",
short,
@ -406,8 +444,20 @@ $ sq key userid strip --userid \"<juliet@example.org>\" \\
",
)]
pub struct UseridStripCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
value_name = "USERID",
short,
@ -469,17 +519,19 @@ pub struct AdoptCommand {
)]
pub allow_broken_crypto: bool,
#[clap(
default_value_t = FileOrStdin::default(),
value_name = "TARGET-KEY",
help = "Adds keys to TARGET-KEY",
help = "Adds keys to TARGET-KEY or reads keys from stdin if omitted",
)]
pub certificate: Option<PathBuf>,
pub certificate: FileOrStdin,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
short = 'B',
long,
@ -529,17 +581,19 @@ pub struct AttestCertificationsCommand {
)]
pub all: bool,
#[clap(
default_value_t = FileOrStdin::default(),
value_name = "KEY",
help = "Changes attestations on KEY",
help = "Changes attestations on KEY or reads from stdin if omitted",
)]
pub key: Option<PathBuf>,
pub key: FileOrStdin,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
short = 'B',
long,
@ -618,8 +672,20 @@ $ sq key subkey add --output alice-new.key.pgp --can-sign --cipher-suite rsa3k -
#[clap(group(ArgGroup::new("sign-group").args(&["can_sign", "can_encrypt"])))]
#[clap(group(ArgGroup::new("required-group").args(&["can_authenticate", "can_sign", "can_encrypt"]).required(true)))]
pub struct SubkeyAddCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
long = "private-key-store",
value_name = "KEY_STORE",

View File

@ -2,6 +2,10 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
name = "keyring",
@ -79,12 +83,13 @@ pub struct FilterCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Vec<PathBuf>,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
long = "userid",
value_name = "USERID",
@ -174,15 +179,16 @@ $ sq keyring join juliet.pgp romeo.pgp alice.pgp
",
)]
pub struct JoinCommand {
#[clap(value_name = "FILE", help = "Sets the input files to use")]
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Vec<PathBuf>,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Sets the output file to use"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
short = 'B',
long = "binary",
@ -210,18 +216,16 @@ $ sq keyring merge certs.pgp romeo-updates.pgp
",
)]
pub struct MergeCommand {
#[clap(
value_name = "FILE",
help = "Reads from FILE",
)]
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Vec<PathBuf>,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
short = 'B',
long = "binary",
@ -251,10 +255,11 @@ $ sq keyring filter --domain example.org certs.pgp | sq keyring list
)]
pub struct ListCommand {
#[clap(
value_name = "FILE",
help = "Reads from FILE or stdin if omitted",
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: Option<PathBuf>,
pub input: FileOrStdin,
#[clap(
long = "all-userids",
help = "Lists all user ids",
@ -288,10 +293,11 @@ $ sq keyring merge certs.pgp | sq keyring split
)]
pub struct SplitCommand {
#[clap(
value_name = "FILE",
help = "Reads from FILE or stdin if omitted",
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: Option<PathBuf>,
pub input: FileOrStdin,
#[clap(
short = 'p',
long = "prefix",

View File

@ -1,9 +1,11 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::sq_cli::types::NetworkPolicy;
use super::types::ClapData;
use super::types::FileOrCertStore;
use super::types::FileOrStdin;
#[derive(Parser, Debug)]
#[clap(
name = "keyserver",
@ -62,12 +64,13 @@ the usual way.
)]
pub struct GetCommand {
#[clap(
short,
default_value_t = FileOrCertStore::default(),
help = FileOrCertStore::HELP,
long,
value_name = "FILE",
help = "Writes to FILE instead of importing into the certificate store"
short,
value_name = FileOrCertStore::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrCertStore,
#[clap(
short = 'B',
long,
@ -88,6 +91,10 @@ pub struct GetCommand {
about = "Sends a key",
)]
pub struct SendCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Option<PathBuf>,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
}

View File

@ -2,7 +2,11 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::sq_cli::types::{ArmorKind, IoArgs, SessionKey};
use super::types::ArmorKind;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
use super::types::SessionKey;
#[derive(Parser, Debug)]
#[clap(
@ -64,8 +68,20 @@ $ sq packet dump --session-key AAAABBBBCCCC... ciphertext.pgp
",
)]
pub struct DumpCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
long = "session-key",
value_name = "SESSION-KEY",
@ -102,8 +118,20 @@ $ sq packet decrypt --recipient-file juliet.pgp ciphertext.pgp
",
)]
pub struct DecryptCommand {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
short = 'B',
long,
@ -154,8 +182,12 @@ $ sq packet split juliet.pgp
",
)]
pub struct SplitCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Option<PathBuf>,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
short = 'p',
long = "prefix",
@ -191,12 +223,13 @@ pub struct JoinCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Vec<PathBuf>,
#[clap(
short,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrStdout,
#[clap(
long = "label",
value_name = "LABEL",

View File

@ -2,7 +2,9 @@ use std::path::PathBuf;
use clap::Parser;
use crate::sq_cli::types::IoArgs;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -36,8 +38,20 @@ $ sq sign --time 20020304 --detached --signer-file juliet.pgp message.txt
",
)]
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
// TODO: Why capital B?
#[clap(
short = 'B',

View File

@ -1,7 +1,14 @@
use std::fmt::Display;
use std::fmt::Formatter;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::stdin;
use std::io::stdout;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::SystemTime;
@ -9,28 +16,413 @@ use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use buffered_reader::BufferedReader;
use buffered_reader::File;
use buffered_reader::Generic;
use chrono::{offset::Utc, DateTime};
/// Common types for arguments of sq.
use clap::{ValueEnum, Args};
use clap::ValueEnum;
use openpgp::armor;
use openpgp::fmt::hex;
use openpgp::serialize::stream::Armorer;
use openpgp::serialize::stream::Message;
use openpgp::types::SymmetricAlgorithm;
use sequoia_openpgp as openpgp;
use terminal_size::terminal_size;
use crate::sq_cli::SECONDS_IN_DAY;
use crate::sq_cli::SECONDS_IN_YEAR;
#[derive(Debug, Args)]
pub struct IoArgs {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Option<PathBuf>,
#[clap(
short,
long,
value_name = "FILE",
help = "Writes to FILE or stdout if omitted"
)]
pub output: Option<PathBuf>,
/// A type wrapping an AtomicBool to guard emitting a CLI warning only once
struct CliWarningOnce(AtomicBool);
/// A static `CliWarningOnce` indicating whether a warning about the unstable
/// CLI has been emitted already
static UNSTABLE_CLI_WARNING: CliWarningOnce =
CliWarningOnce(AtomicBool::new(false));
impl CliWarningOnce {
/// Emit a warning message only once
pub fn warn(&self) {
if !self.0.swap(true, Ordering::Relaxed) {
// stdout is connected to a terminal, assume interactive use.
if terminal_size().is_none()
// For bash shells, we can use a very simple heuristic.
// We simply look at whether the COLUMNS variable is defined in
// our environment.
&& std::env::var_os("COLUMNS").is_none() {
eprintln!(
"\nWARNING: sq does not have a stable CLI interface. \
Use with caution in scripts.\n"
);
}
}
}
}
/// A trait to provide const &str for clap annotations for custom structs
pub trait ClapData {
/// The clap value name
const VALUE_NAME: &'static str;
/// The clap help text
const HELP: &'static str;
}
/// A type wrapping an optional PathBuf to use as stdin or file input
///
/// When creating `FileOrStdin` from `&str`, providing a `"-"` is interpreted
/// as `None`, i.e. read from stdin. Providing other strings is interpreted as
/// `Some(PathBuf)`, i.e. read from file.
/// Use this if a CLI should allow input from a file and if unset from stdin.
///
/// ## Examples
/// ```
/// use clap::Args;
///
/// #[derive(Debug, Args)]
/// #[clap(name = "example", about = "an example")]
/// pub struct Example {
/// #[clap(
/// default_value_t = FileOrStdin::default(),
/// help = FileOrStdin::HELP,
/// value_name = FileOrStdin::VALUE_NAME,
/// )]
/// pub input: FileOrStdin,
/// }
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileOrStdin(Option<PathBuf>);
impl ClapData for FileOrStdin {
const VALUE_NAME: &'static str = "FILE";
const HELP: &'static str = "Reads from FILE or stdin if omitted";
}
impl FileOrStdin {
pub fn new(path: Option<PathBuf>) -> Self {
FileOrStdin(path)
}
/// Return a reference to the inner type
pub fn inner(&self) -> Option<&PathBuf> {
self.0.as_ref()
}
/// Returns `None` if `self.0` is `None`, otherwise calls f with the wrapped
/// value and returns the result
pub fn and_then<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(PathBuf) -> Option<U>,
{
self.0.and_then(|x| f(x))
}
/// Get a boxed BufferedReader for the FileOrStdin
///
/// Opens a file if there is Some(PathBuf), else opens stdin.
pub fn open(&self) -> Result<Box<dyn BufferedReader<()>>> {
if let Some(path) = self.inner() {
Ok(Box::new(
File::open(path)
.with_context(|| format!("Failed to open {}", self))?))
} else {
Ok(Box::new(Generic::new(stdin(), None)))
}
}
}
impl Default for FileOrStdin {
fn default() -> Self {
FileOrStdin(None)
}
}
impl From<PathBuf> for FileOrStdin {
fn from(value: PathBuf) -> Self {
if value == PathBuf::from("-") {
FileOrStdin::default()
} else {
FileOrStdin::new(Some(value))
}
}
}
impl From<Option<PathBuf>> for FileOrStdin {
fn from(value: Option<PathBuf>) -> Self {
if let Some(path) = value {
FileOrStdin::from(path)
} else {
FileOrStdin::default()
}
}
}
impl From<&Path> for FileOrStdin {
fn from(value: &Path) -> Self {
if Path::new("-") == value {
FileOrStdin::default()
} else {
FileOrStdin::from(value.to_owned())
}
}
}
impl From<Option<&Path>> for FileOrStdin {
fn from(value: Option<&Path>) -> Self {
if let Some(path) = value {
FileOrStdin::from(path)
} else {
FileOrStdin::default()
}
}
}
impl FromStr for FileOrStdin {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
if "-" == s {
Ok(FileOrStdin(None))
} else {
Ok(FileOrStdin(Some(PathBuf::from(s))))
}
}
}
impl Display for FileOrStdin {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match &self.0 {
None => write!(f, "-"),
Some(path) => write!(f, "{}", path.display()),
}
}
}
/// A type wrapping an optional PathBuf to use as stdout or file output
///
/// Use this if a CLI should allow output to a file and if unset output to
/// a cert store.
///
/// ## Examples
/// ```
/// use clap::Args;
///
/// #[derive(Debug, Args)]
/// #[clap(name = "example", about = "an example")]
/// pub struct Example {
/// #[clap(
/// help = FileOrCertStore::HELP,
/// long,
/// short,
/// value_name = FileOrCertStore::VALUE_NAME,
/// )]
/// pub output: Option<FileOrCertStore>,
/// }
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileOrCertStore(Option<PathBuf>);
impl ClapData for FileOrCertStore {
const VALUE_NAME: &'static str = "FILE";
const HELP: &'static str
= "Writes to FILE instead of importing into the certificate store";
}
impl FileOrCertStore {
/// Consume self and return the inner PathBuf
pub fn into_inner(self) -> Option<PathBuf> {
self.0
}
/// Return a reference to the inner type
pub fn path(&self) -> Option<&PathBuf> {
self.0.as_ref()
}
}
impl Default for FileOrCertStore {
fn default() -> Self {
FileOrCertStore(None)
}
}
impl FromStr for FileOrCertStore {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
Ok(FileOrCertStore(Some(PathBuf::from(s))))
}
}
impl Display for FileOrCertStore {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match &self.0 {
Some(path) => write!(f, "{}", path.display()),
None => write!(f, "-"),
}
}
}
/// A type wrapping an optional PathBuf to use as stdout or file output
///
/// When creating `FileOrStdout` from `&str`, providing a `"-"` is interpreted
/// as `None`, i.e. output to stdout. Providing other strings is interpreted as
/// `Some(PathBuf)`, i.e. output to file.
/// Use this if a CLI should allow output to a file and if unset output to
/// stdout.
///
/// ## Examples
/// ```
/// use clap::Args;
///
/// #[derive(Debug, Args)]
/// #[clap(name = "example", about = "an example")]
/// pub struct Example {
/// #[clap(
/// default_value_t = FileOrStdout::default(),
/// help = FileOrStdout::HELP,
/// long,
/// short,
/// value_name = FileOrStdout::VALUE_NAME,
/// )]
/// pub output: FileOrStdout,
/// }
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileOrStdout(Option<PathBuf>);
impl ClapData for FileOrStdout {
const VALUE_NAME: &'static str = "FILE";
const HELP: &'static str = "Writes to FILE or stdout if omitted";
}
impl FileOrStdout {
pub fn new(path: Option<PathBuf>) -> Self {
FileOrStdout(path)
}
/// Return a reference to the optional PathBuf
pub fn path(&self) -> Option<&PathBuf> {
self.0.as_ref()
}
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use.
///
/// This is suitable for any kind of OpenPGP data, decrypted or
/// authenticated payloads.
pub fn create_safe(
&self,
force: bool,
) -> Result<Box<dyn Write + Sync + Send>> {
self.create(force)
}
/// Opens the file (or stdout) for writing data that is NOT safe
/// for non-interactive use.
///
/// If our heuristic detects non-interactive use, we will emit a
/// warning once.
pub fn create_unsafe(
&self,
force: bool,
) -> Result<Box<dyn Write + Sync + Send>> {
UNSTABLE_CLI_WARNING.warn();
self.create(force)
}
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use because it is an OpenPGP data stream.
pub fn create_pgp_safe(
&self,
force: bool,
binary: bool,
kind: armor::Kind,
) -> Result<Message> {
let sink = self.create_safe(force)?;
let mut message = Message::new(sink);
if ! binary {
message = Armorer::new(message).kind(kind).build()?;
}
Ok(message)
}
/// Helper function, do not use directly. Instead, use create_or_stdout_safe
/// or create_or_stdout_unsafe.
fn create(&self, force: bool) -> Result<Box<dyn Write + Sync + Send>> {
if let Some(path) = self.path() {
if !path.exists() || force {
Ok(Box::new(
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.context("Failed to create output file")?,
))
} else {
Err(anyhow::anyhow!(format!(
"File {:?} exists, use \"sq --force ...\" to overwrite",
self,
)))
}
} else {
Ok(Box::new(stdout()))
}
}
}
impl Default for FileOrStdout {
fn default() -> Self {
FileOrStdout(None)
}
}
impl From<FileOrCertStore> for FileOrStdout {
fn from(value: FileOrCertStore) -> Self {
FileOrStdout::new(value.into_inner())
}
}
impl From<PathBuf> for FileOrStdout {
fn from(value: PathBuf) -> Self {
if value == PathBuf::from("-") {
FileOrStdout::default()
} else {
FileOrStdout::new(Some(value))
}
}
}
impl From<Option<PathBuf>> for FileOrStdout {
fn from(value: Option<PathBuf>) -> Self {
if let Some(path) = value {
FileOrStdout::from(path)
} else {
FileOrStdout::default()
}
}
}
impl FromStr for FileOrStdout {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
if "-" == s {
Ok(FileOrStdout::default())
} else {
Ok(FileOrStdout(Some(PathBuf::from(s))))
}
}
}
impl Display for FileOrStdout {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match &self.0 {
Some(path) => write!(f, "{}", path.display()),
None => write!(f, "-"),
}
}
}
#[derive(ValueEnum, Debug, Clone)]

View File

@ -5,7 +5,9 @@ use clap::Parser;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use crate::sq_cli::types::IoArgs;
use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)]
#[clap(
@ -59,8 +61,20 @@ $ sq verify --time 20130721 msg.pgp
",
)]
pub struct Command {
#[clap(flatten)]
pub io: IoArgs,
#[clap(
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
long = "detached",
value_name = "SIG",

View File

@ -1,9 +1,11 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::sq_cli::types::NetworkPolicy;
use super::types::ClapData;
use super::types::FileOrCertStore;
use super::types::FileOrStdin;
#[derive(Parser, Debug)]
#[clap(
name = "wkd",
@ -89,12 +91,13 @@ pub struct GetCommand {
)]
pub binary: bool,
#[clap(
short,
default_value_t = FileOrCertStore::default(),
help = FileOrCertStore::HELP,
long,
value_name = "FILE",
help = "Writes to FILE instead of importing into the certificate store"
short,
value_name = FileOrCertStore::VALUE_NAME,
)]
pub output: Option<PathBuf>,
pub output: FileOrCertStore,
}
#[derive(Debug, Args)]
@ -138,10 +141,11 @@ pub struct GenerateCommand {
)]
pub domain: String,
#[clap(
default_value_t = FileOrStdin::default(),
value_name = "CERT-RING",
help = "Adds certificates from CERT-RING to the WKD",
help = "Adds certificates from CERT-RING (or stdin if omitted) to the WKD",
)]
pub input: Option<PathBuf>,
pub input: FileOrStdin,
#[clap(
short = 'd',
long = "direct-method",