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" anyhow = "1.0.18"
chrono = "0.4.10" chrono = "0.4.10"
# For an MSRV of 1.63: 4.0.32. # 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" itertools = "0.10"
once_cell = "1.17" once_cell = "1.17"
sequoia-cert-store = "0.3" sequoia-cert-store = "0.3"
@ -53,8 +53,9 @@ terminal_size = "0.2.6"
[build-dependencies] [build-dependencies]
anyhow = "1.0.18" 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 # 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. # For an MSRV of 1.63: 4.0.7.
clap_complete = "4" clap_complete = "4"
# For an MSRV of 1.63: 0.2.6 # 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 } sequoia-net = { version = "0.27", default-features = false }
subplot-build = { version = "0.7.0", optional = true } subplot-build = { version = "0.7.0", optional = true }
cfg-if = "1" cfg-if = "1"
terminal_size = "0.2.6"
[dev-dependencies] [dev-dependencies]
subplotlib = "0.7.0" subplotlib = "0.7.0"

View File

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

View File

@ -150,8 +150,8 @@ pub fn certify(config: Config, c: certify::Command)
// And export it. // And export it.
let mut message = config.create_or_stdout_pgp( let mut message = c.output.create_pgp_safe(
c.output.as_deref(), config.force,
c.binary, c.binary,
sequoia_openpgp::armor::Kind::PublicKey, 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;
use cert_store::store::UserIDQueryParams; use cert_store::store::UserIDQueryParams;
use crate::sq_cli::types::FileOrStdout;
use crate::{ use crate::{
Config, Config,
print_error_chain, print_error_chain,
@ -76,8 +77,12 @@ pub fn dispatch(config: Config, cmd: export::Command) -> Result<()> {
return Err(anyhow::anyhow!("Invalid arguments.")); return Err(anyhow::anyhow!("Invalid arguments."));
} }
let mut sink = config.create_or_stdout_pgp( let output = FileOrStdout::default();
None, cmd.binary, armor::Kind::PublicKey)?; let mut sink = output.create_pgp_safe(
config.force,
cmd.binary,
armor::Kind::PublicKey,
)?;
let mut exported_something = false; 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::LazyCert;
use cert_store::StoreUpdate; use cert_store::StoreUpdate;
use crate::{ use crate::sq_cli::types::FileOrStdin;
Config, use crate::Config;
open_or_stdin,
};
use crate::sq_cli::import; use crate::sq_cli::import;
@ -32,8 +30,7 @@ pub fn dispatch<'store>(mut config: Config<'store>, cmd: import::Command)
let inner = || -> Result<()> { let inner = || -> Result<()> {
for input in inputs.into_iter() { for input in inputs.into_iter() {
let input = open_or_stdin( let input = FileOrStdin::from(input).open()?;
if input == PathBuf::from("-") { None } else { Some(&input) })?;
let raw_certs = RawCertParser::from_reader(input)?; let raw_certs = RawCertParser::from_reader(input)?;
let cert_store = config.cert_store_mut_or_else()?; 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::SECONDS_IN_DAY;
use crate::sq_cli::inspect; 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<()> -> Result<()>
{ {
// sq inspect does not have --output, but commands::inspect does. // sq inspect does not have --output, but commands::inspect does.
// Work around this mismatch by always creating a stdout output. // 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 policy = &config.policy;
let time = Some(config.time); let time = Some(config.time);
let print_certifications = c.certifications; let print_certifications = c.certifications;
let input = c.input.as_deref(); let input = c.input;
let input_name = if let Some(input) = c.input.as_ref() { write!(output, "{}: ", input)?;
format!("{}", input.display())
} else {
"-".to_string()
};
write!(output, "{}: ", input_name)?;
let mut type_called = false; // Did we print the type yet? let mut type_called = false; // Did we print the type yet?
let mut encrypted = false; // Is it an encrypted message? 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 bytes: Vec<u8> = Vec::new();
let mut ppr = if c.cert.is_empty() { let mut ppr = if c.cert.is_empty() {
if let Some(input) = input { if let Some(path) = input.inner() {
if ! input.exists() && if ! path.exists() &&
format!("{}", input.display()).parse::<KeyHandle>().is_ok() { format!("{}", input).parse::<KeyHandle>().is_ok() {
eprintln!("The file {} does not exist, \ eprintln!("The file {} does not exist, \
did you mean \"sq inspect --cert {}\"?", 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 { } else {
let cert_store = config.cert_store_or_else()?; let cert_store = config.cert_store_or_else()?;
for cert in c.cert.into_iter() { 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, " Cert: {}", is_cert.unwrap_err())?;
writeln!(output, " Keyring: {}", is_keyring.unwrap_err())?; writeln!(output, " Keyring: {}", is_keyring.unwrap_err())?;
writeln!(output)?; writeln!(output)?;
writeln!(output, "Hint: Try 'sq packet dump {}'", input_name)?; writeln!(output, "Hint: Try 'sq packet dump {}'", input)?;
} }
} else { } else {
unreachable!() unreachable!()

View File

@ -20,12 +20,11 @@ use openpgp::Result;
use sequoia_openpgp as openpgp; use sequoia_openpgp as openpgp;
use crate::decrypt_key; use crate::decrypt_key;
use crate::open_or_stdin;
use crate::sq_cli; use crate::sq_cli;
use crate::Config; use crate::Config;
pub fn adopt(config: Config, command: sq_cli::key::AdoptCommand) -> Result<()> { 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 cert = Cert::from_reader(input)?;
let mut wanted: Vec<( let mut wanted: Vec<(
KeyHandle, 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 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 { if command.binary {
cert.as_tsk().serialize(&mut sink)?; cert.as_tsk().serialize(&mut sink)?;
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

180
src/sq.rs
View File

@ -7,10 +7,10 @@
#![doc = include_str!(concat!(env!("OUT_DIR"), "/sq-usage.md"))] #![doc = include_str!(concat!(env!("OUT_DIR"), "/sq-usage.md"))]
use anyhow::Context as _; use anyhow::Context as _;
use sq_cli::types::FileOrStdin;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::OpenOptions;
use std::io; use std::io;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -21,7 +21,7 @@ use once_cell::unsync::OnceCell;
use terminal_size::terminal_size; 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 sequoia_openpgp as openpgp;
use openpgp::{ use openpgp::{
@ -36,7 +36,7 @@ use openpgp::packet::prelude::*;
use openpgp::parse::{Parse, PacketParser, PacketParserResult}; use openpgp::parse::{Parse, PacketParser, PacketParserResult};
use openpgp::packet::signature::subpacket::NotationData; use openpgp::packet::signature::subpacket::NotationData;
use openpgp::packet::signature::subpacket::NotationDataFlags; use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::{Serialize, stream::{Message, Armorer}}; use openpgp::serialize::Serialize;
use openpgp::cert::prelude::*; use openpgp::cert::prelude::*;
use openpgp::policy::StandardPolicy as P; use openpgp::policy::StandardPolicy as P;
use openpgp::serialize::SerializeInto; use openpgp::serialize::SerializeInto;
@ -70,17 +70,6 @@ mod commands;
pub mod output; pub mod output;
pub use output::{wkd::WkdUrlVariant, Model, OutputFormat, OutputVersion}; 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. /// Loads one TSK from every given file.
fn load_keys<'a, I>(files: I) -> openpgp::Result<Vec<Cert>> fn load_keys<'a, I>(files: I) -> openpgp::Result<Vec<Cert>>
where I: Iterator<Item=&'a Path> 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> { pub struct Config<'a> {
force: bool, force: bool,
output_format: OutputFormat, output_format: OutputFormat,
output_version: Option<OutputVersion>, output_version: Option<OutputVersion>,
policy: P<'a>, policy: P<'a>,
time: SystemTime, time: SystemTime,
/// Have we emitted the warning yet?
unstable_cli_warning_emitted: bool,
// --no-cert-store // --no-cert-store
no_rw_cert_store: bool, no_rw_cert_store: bool,
cert_store_path: Option<PathBuf>, cert_store_path: Option<PathBuf>,
@ -297,73 +261,6 @@ pub struct Config<'a> {
} }
impl<'store> Config<'store> { 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. /// Returns the cert store's base directory, if it is enabled.
fn cert_store_base(&self) -> Option<PathBuf> { fn cert_store_base(&self) -> Option<PathBuf> {
if self.no_rw_cert_store { if self.no_rw_cert_store {
@ -1118,13 +1015,12 @@ fn main() -> Result<()> {
None None
}; };
let mut config = Config { let config = Config {
force, force,
output_format, output_format,
output_version, output_version,
policy: policy.clone(), policy: policy.clone(),
time, time,
unstable_cli_warning_emitted: false,
no_rw_cert_store: c.no_cert_store, no_rw_cert_store: c.no_cert_store,
cert_store_path: c.cert_store.clone(), cert_store_path: c.cert_store.clone(),
pep_cert_store_path: c.pep_cert_store.clone(), pep_cert_store_path: c.pep_cert_store.clone(),
@ -1147,9 +1043,8 @@ fn main() -> Result<()> {
SqSubcommands::Decrypt(command) => { SqSubcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut input = command.input.open()?;
let mut output = let mut output = command.output.create_safe(config.force)?;
config.create_or_stdout_safe(command.io.output.as_deref())?;
let certs = load_certs( let certs = load_certs(
command.sender_cert_file.iter().map(|s| s.as_ref()), command.sender_cert_file.iter().map(|s| s.as_ref()),
@ -1197,10 +1092,10 @@ fn main() -> Result<()> {
recipients.extend( recipients.extend(
config.lookup_by_userid(&command.recipients_userid, false) config.lookup_by_userid(&command.recipients_userid, false)
.context("--recipient-userid")?); .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( let output = command.output.create_pgp_safe(
command.io.output.as_deref(), config.force,
command.binary, command.binary,
armor::Kind::Message, armor::Kind::Message,
)?; )?;
@ -1224,8 +1119,8 @@ fn main() -> Result<()> {
})?; })?;
}, },
SqSubcommands::Sign(command) => { SqSubcommands::Sign(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut input = command.input.open()?;
let output = command.io.output.as_deref(); let output = &command.output;
let detached = command.detached; let detached = command.detached;
let binary = command.binary; let binary = command.binary;
let append = command.append; let append = command.append;
@ -1238,12 +1133,16 @@ fn main() -> Result<()> {
let notations = parse_notations(command.notation)?; let notations = parse_notations(command.notation)?;
if let Some(merge) = command.merge { if let Some(merge) = command.merge {
let output = config.create_or_stdout_pgp(output, binary, let output = output.create_pgp_safe(
armor::Kind::Message)?; config.force,
let mut input2 = open_or_stdin(Some(&merge))?; binary,
armor::Kind::Message,
)?;
let data: FileOrStdin = merge.into();
let mut input2 = data.open()?;
commands::merge_signatures(&mut input, &mut input2, output)?; commands::merge_signatures(&mut input, &mut input2, output)?;
} else if command.clearsign { } 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, commands::sign::clearsign(config, private_key_store, input, output, secrets,
time, &notations)?; time, &notations)?;
} else { } else {
@ -1263,10 +1162,8 @@ fn main() -> Result<()> {
} }
}, },
SqSubcommands::Verify(command) => { SqSubcommands::Verify(command) => {
// TODO: Fix interface of open_or_stdin, create_or_stdout_safe, etc. let mut input = command.input.open()?;
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut output = command.output.create_safe(config.force)?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut detached = if let Some(f) = command.detached { let mut detached = if let Some(f) = command.detached {
Some(File::open(f)?) Some(File::open(f)?)
} else { } else {
@ -1289,7 +1186,7 @@ fn main() -> Result<()> {
// TODO: Extract body to commands/armor.rs // TODO: Extract body to commands/armor.rs
SqSubcommands::Armor(command) => { 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(); let mut want_kind: Option<armor::Kind> = command.kind.into();
// Peek at the data. If it looks like it is armored // 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) && (want_kind.is_none() || want_kind == have_kind)
{ {
// It is already armored and has the correct kind. // It is already armored and has the correct kind.
let mut output = let mut output = command.output.create_safe(c.force)?;
config.create_or_stdout_safe(command.io.output.as_deref())?;
io::copy(&mut input, &mut output)?; io::copy(&mut input, &mut output)?;
return Ok(()); return Ok(());
} }
@ -1324,8 +1220,7 @@ fn main() -> Result<()> {
let want_kind = want_kind.expect("given or detected"); let want_kind = want_kind.expect("given or detected");
let mut output = let mut output =
config.create_or_stdout_pgp(command.io.output.as_deref(), command.output.create_pgp_safe(config.force, false, want_kind)?;
false, want_kind)?;
if already_armored { if already_armored {
// Dearmor and copy to change the type. // Dearmor and copy to change the type.
@ -1339,9 +1234,8 @@ fn main() -> Result<()> {
output.finalize()?; output.finalize()?;
}, },
SqSubcommands::Dearmor(command) => { SqSubcommands::Dearmor(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut input = command.input.open()?;
let mut output = let mut output = command.output.create_safe(config.force)?;
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut filter = armor::Reader::from_reader(&mut input, None); let mut filter = armor::Reader::from_reader(&mut input, None);
io::copy(&mut filter, &mut output)?; io::copy(&mut filter, &mut output)?;
}, },
@ -1367,10 +1261,9 @@ fn main() -> Result<()> {
SqSubcommands::Packet(command) => match command.subcommand { SqSubcommands::Packet(command) => match command.subcommand {
packet::Subcommands::Dump(command) => { packet::Subcommands::Dump(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut input = command.input.open()?;
let mut output = config.create_or_stdout_unsafe( let output_type = command.output;
command.io.output.as_deref(), let mut output = output_type.create_unsafe(config.force)?;
)?;
let session_key = command.session_key; let session_key = command.session_key;
let width = if let Some((width, _)) = terminal_size() { let width = if let Some((width, _)) = terminal_size() {
@ -1384,9 +1277,9 @@ fn main() -> Result<()> {
}, },
packet::Subcommands::Decrypt(command) => { packet::Subcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?; let mut input = command.input.open()?;
let mut output = config.create_or_stdout_pgp( let mut output = command.output.create_pgp_safe(
command.io.output.as_deref(), config.force,
command.binary, command.binary,
armor::Kind::Message, armor::Kind::Message,
)?; )?;
@ -1404,15 +1297,16 @@ fn main() -> Result<()> {
}, },
packet::Subcommands::Split(command) => { packet::Subcommands::Split(command) => {
let mut input = open_or_stdin(command.input.as_deref())?; let mut input = command.input.open()?;
let prefix = let prefix =
// The prefix is either specified explicitly... // The prefix is either specified explicitly...
command.prefix.unwrap_or( command.prefix.unwrap_or(
// ... or we derive it from the input file... // ... or we derive it from the input file...
command.input.and_then(|i| { command.input.and_then(|x| {
let p = PathBuf::from(i);
// (but only use the filename) // (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... // ... or we use a generic prefix...
.unwrap_or_else(|| String::from("output")) .unwrap_or_else(|| String::from("output"))

View File

@ -1,6 +1,9 @@
use clap::Parser; 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?: Option<_> conflicts with default value
// TODO: use indoc to transparently (de-)indent static strings // TODO: use indoc to transparently (de-)indent static strings
@ -29,8 +32,20 @@ $ sq armor binary-message.pgp
" "
)] )]
pub struct Command { pub struct Command {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
long = "label", long = "label",
value_name = "LABEL", value_name = "LABEL",

View File

@ -1,6 +1,8 @@
use clap::{ValueEnum, Args, Parser, Subcommand}; 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)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -47,8 +49,20 @@ $ sq autocrypt decode autocrypt.eml
" "
)] )]
pub struct DecodeCommand { pub struct DecodeCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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")] #[clap(short = 'B', long, help = "Emits binary data")]
pub binary: bool, pub binary: bool,
} }
@ -81,8 +95,20 @@ $ sq autocrypt encode-sender --prefer-encrypt mutual juliet.pgp
" "
)] )]
pub struct EncodeSenderCommand { pub struct EncodeSenderCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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 // TODO the help message looks like "primary userid" might be the default
// email. clarify // email. clarify
#[clap( #[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_DURATION;
use crate::sq_cli::THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS; use crate::sq_cli::THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS;
use super::types::ClapData;
use super::types::Expiry; use super::types::Expiry;
use super::types::FileOrStdout;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -49,12 +51,13 @@ $ sq certify --time 20130721 neal.pgp ada.pgp Ada
)] )]
pub struct Command { pub struct Command {
#[clap( #[clap(
short, default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long, long,
value_name = "FILE", short,
help = "Writes to FILE or stdout if omitted" value_name = FileOrStdout::VALUE_NAME,
)] )]
pub output: Option<PathBuf>, pub output: FileOrStdout,
#[clap( #[clap(
short = 'B', short = 'B',
long, long,

View File

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

View File

@ -1,6 +1,8 @@
use clap::Parser; use clap::Parser;
use crate::sq_cli::types::IoArgs; use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -28,6 +30,18 @@ $ sq dearmor ascii-message.pgp
", ",
)] )]
pub struct Command { pub struct Command {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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 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)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -44,8 +47,20 @@ $ sq decrypt ciphertext.pgp
)] )]
// TODO use usize // TODO use usize
pub struct Command { pub struct Command {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
short = 'n', short = 'n',
long = "signatures", long = "signatures",

View File

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

View File

@ -1,10 +1,11 @@
use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use sequoia_openpgp as openpgp; use sequoia_openpgp as openpgp;
use openpgp::KeyHandle; use openpgp::KeyHandle;
use super::types::ClapData;
use super::types::FileOrStdin;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap( #[clap(
name = "inspect", name = "inspect",
@ -42,10 +43,11 @@ $ sq inspect --time 20130721 cert.pgp
)] )]
pub struct Command { pub struct Command {
#[clap( #[clap(
value_name = "FILE", default_value_t = FileOrStdin::default(),
help = "Reads from FILE or stdin if omitted", help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)] )]
pub input: Option<PathBuf>, pub input: FileOrStdin,
#[clap( #[clap(
long = "cert", long = "cert",
value_name = "FINGERPRINT|KEYID", 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 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::Expiry;
use crate::sq_cli::types::Time; use crate::sq_cli::types::Time;
use crate::sq_cli::KEY_VALIDITY_DURATION; 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 { pub struct PasswordCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
long = "clear", long = "clear",
help = "Emit a key with unencrypted secrets", 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 { pub struct ExtractCertCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
short = 'B', short = 'B',
long, long,
@ -344,8 +370,20 @@ $ sq key userid add --userid \"Juliet\" --creation-time 20210628 \\
", ",
)] )]
pub struct UseridAddCommand { pub struct UseridAddCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
value_name = "USERID", value_name = "USERID",
short, short,
@ -406,8 +444,20 @@ $ sq key userid strip --userid \"<juliet@example.org>\" \\
", ",
)] )]
pub struct UseridStripCommand { pub struct UseridStripCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
value_name = "USERID", value_name = "USERID",
short, short,
@ -469,17 +519,19 @@ pub struct AdoptCommand {
)] )]
pub allow_broken_crypto: bool, pub allow_broken_crypto: bool,
#[clap( #[clap(
default_value_t = FileOrStdin::default(),
value_name = "TARGET-KEY", 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( #[clap(
short, default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long, long,
value_name = "FILE", short,
help = "Writes to FILE or stdout if omitted" value_name = FileOrStdout::VALUE_NAME,
)] )]
pub output: Option<PathBuf>, pub output: FileOrStdout,
#[clap( #[clap(
short = 'B', short = 'B',
long, long,
@ -529,17 +581,19 @@ pub struct AttestCertificationsCommand {
)] )]
pub all: bool, pub all: bool,
#[clap( #[clap(
default_value_t = FileOrStdin::default(),
value_name = "KEY", 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( #[clap(
short, default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long, long,
value_name = "FILE", short,
help = "Writes to FILE or stdout if omitted" value_name = FileOrStdout::VALUE_NAME,
)] )]
pub output: Option<PathBuf>, pub output: FileOrStdout,
#[clap( #[clap(
short = 'B', short = 'B',
long, 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("sign-group").args(&["can_sign", "can_encrypt"])))]
#[clap(group(ArgGroup::new("required-group").args(&["can_authenticate", "can_sign", "can_encrypt"]).required(true)))] #[clap(group(ArgGroup::new("required-group").args(&["can_authenticate", "can_sign", "can_encrypt"]).required(true)))]
pub struct SubkeyAddCommand { pub struct SubkeyAddCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
long = "private-key-store", long = "private-key-store",
value_name = "KEY_STORE", value_name = "KEY_STORE",

View File

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

View File

@ -1,9 +1,11 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use crate::sq_cli::types::NetworkPolicy; use crate::sq_cli::types::NetworkPolicy;
use super::types::ClapData;
use super::types::FileOrCertStore;
use super::types::FileOrStdin;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap( #[clap(
name = "keyserver", name = "keyserver",
@ -62,12 +64,13 @@ the usual way.
)] )]
pub struct GetCommand { pub struct GetCommand {
#[clap( #[clap(
short, default_value_t = FileOrCertStore::default(),
help = FileOrCertStore::HELP,
long, long,
value_name = "FILE", short,
help = "Writes to FILE instead of importing into the certificate store" value_name = FileOrCertStore::VALUE_NAME,
)] )]
pub output: Option<PathBuf>, pub output: FileOrCertStore,
#[clap( #[clap(
short = 'B', short = 'B',
long, long,
@ -88,6 +91,10 @@ pub struct GetCommand {
about = "Sends a key", about = "Sends a key",
)] )]
pub struct SendCommand { pub struct SendCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")] #[clap(
pub input: Option<PathBuf>, 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 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)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -64,8 +68,20 @@ $ sq packet dump --session-key AAAABBBBCCCC... ciphertext.pgp
", ",
)] )]
pub struct DumpCommand { pub struct DumpCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
long = "session-key", long = "session-key",
value_name = "SESSION-KEY", value_name = "SESSION-KEY",
@ -102,8 +118,20 @@ $ sq packet decrypt --recipient-file juliet.pgp ciphertext.pgp
", ",
)] )]
pub struct DecryptCommand { pub struct DecryptCommand {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
short = 'B', short = 'B',
long, long,
@ -154,8 +182,12 @@ $ sq packet split juliet.pgp
", ",
)] )]
pub struct SplitCommand { pub struct SplitCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")] #[clap(
pub input: Option<PathBuf>, default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap( #[clap(
short = 'p', short = 'p',
long = "prefix", long = "prefix",
@ -191,12 +223,13 @@ pub struct JoinCommand {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")] #[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]
pub input: Vec<PathBuf>, pub input: Vec<PathBuf>,
#[clap( #[clap(
short, default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long, long,
value_name = "FILE", short,
help = "Writes to FILE or stdout if omitted" value_name = FileOrStdout::VALUE_NAME,
)] )]
pub output: Option<PathBuf>, pub output: FileOrStdout,
#[clap( #[clap(
long = "label", long = "label",
value_name = "LABEL", value_name = "LABEL",

View File

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

View File

@ -1,7 +1,14 @@
use std::fmt::Display; use std::fmt::Display;
use std::fmt::Formatter; 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::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
use std::time::SystemTime; use std::time::SystemTime;
@ -9,28 +16,413 @@ use anyhow::anyhow;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use buffered_reader::BufferedReader;
use buffered_reader::File;
use buffered_reader::Generic;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
/// Common types for arguments of sq. /// Common types for arguments of sq.
use clap::{ValueEnum, Args}; use clap::ValueEnum;
use openpgp::armor;
use openpgp::fmt::hex; use openpgp::fmt::hex;
use openpgp::serialize::stream::Armorer;
use openpgp::serialize::stream::Message;
use openpgp::types::SymmetricAlgorithm; use openpgp::types::SymmetricAlgorithm;
use sequoia_openpgp as openpgp; use sequoia_openpgp as openpgp;
use terminal_size::terminal_size;
use crate::sq_cli::SECONDS_IN_DAY; use crate::sq_cli::SECONDS_IN_DAY;
use crate::sq_cli::SECONDS_IN_YEAR; use crate::sq_cli::SECONDS_IN_YEAR;
#[derive(Debug, Args)] /// A type wrapping an AtomicBool to guard emitting a CLI warning only once
pub struct IoArgs { struct CliWarningOnce(AtomicBool);
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")] /// A static `CliWarningOnce` indicating whether a warning about the unstable
pub input: Option<PathBuf>, /// CLI has been emitted already
#[clap( static UNSTABLE_CLI_WARNING: CliWarningOnce =
short, CliWarningOnce(AtomicBool::new(false));
long,
value_name = "FILE", impl CliWarningOnce {
help = "Writes to FILE or stdout if omitted" /// Emit a warning message only once
)] pub fn warn(&self) {
pub output: Option<PathBuf>, 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)] #[derive(ValueEnum, Debug, Clone)]

View File

@ -5,7 +5,9 @@ use clap::Parser;
use sequoia_openpgp as openpgp; use sequoia_openpgp as openpgp;
use openpgp::KeyHandle; use openpgp::KeyHandle;
use crate::sq_cli::types::IoArgs; use super::types::ClapData;
use super::types::FileOrStdin;
use super::types::FileOrStdout;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap( #[clap(
@ -59,8 +61,20 @@ $ sq verify --time 20130721 msg.pgp
", ",
)] )]
pub struct Command { pub struct Command {
#[clap(flatten)] #[clap(
pub io: IoArgs, 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( #[clap(
long = "detached", long = "detached",
value_name = "SIG", value_name = "SIG",

View File

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