summaryrefslogtreecommitdiffstats
path: root/sq
diff options
context:
space:
mode:
authorLars Wirzenius <liw@sequoia-pgp.org>2022-07-06 11:13:45 +0300
committerLars Wirzenius <liw@sequoia-pgp.org>2022-07-15 15:49:17 +0300
commitaf1a9b57b7efcff3c5bb26a66d43f1145da371db (patch)
treeb2fef7a0729096265dbfea55150386e7d3f7676c /sq
parent0ced4e7c78c6c7bbde19f97917d568ce24b9a6e6 (diff)
sq: add JSON output for 'sq keyring list'
This heavily changes the subcommand implementation. The original code mixed concerns (iterating over certificates, making decisions about primary UserID, and formatting output). This changes things so that the concerns are more separate. Formatting of JSON output is done using the serde_json crate. This makes it easy to add other formats supported by serde later. Unfortunately, using serde means the whole parsed keyring must be kept in memory at once, because serde doesn't know how to serialize an iterator: a serde serializer gets an immutable reference to the data being serialized, and an iterator must be mutable. We may need to fix that later, but all my attempts to do so failed. Sponsored-by: NLnet Foundation; NGI Assure
Diffstat (limited to 'sq')
-rw-r--r--sq/src/commands/keyring.rs236
1 files changed, 177 insertions, 59 deletions
diff --git a/sq/src/commands/keyring.rs b/sq/src/commands/keyring.rs
index 97b65343..141bc747 100644
--- a/sq/src/commands/keyring.rs
+++ b/sq/src/commands/keyring.rs
@@ -28,6 +28,8 @@ use openpgp::{
use crate::{
Config,
+ OutputFormat,
+ OutputVersion,
open_or_stdin,
};
@@ -239,69 +241,22 @@ fn list(config: Config,
list_all_uids: bool)
-> Result<()>
{
- for (i, cert) in CertParser::from_reader(input)?.enumerate() {
- let cert = match cert {
- Ok(cert) => cert,
- Err(e) => {
- println!("{}. {}", i, e);
- continue;
- },
- };
- let line = format!("{}. {:X}", i, cert.fingerprint());
- let indent = line.chars().map(|_| ' ').collect::<String>();
- print!("{}", line);
-
- // Try to be more helpful by including a User ID in the
- // listing. We'd like it to be the primary one. Use
- // decreasingly strict policies.
- let mut primary_uid = None;
-
- // First, apply our policy.
- if let Ok(vcert) = cert.with_policy(&config.policy, None) {
- if let Ok(primary) = vcert.primary_userid() {
- println!(" {}", String::from_utf8_lossy(primary.value()));
- primary_uid = Some(primary.value().to_vec());
- }
- }
-
- // Second, apply the null policy.
- if primary_uid.is_none() {
- let null = openpgp::policy::NullPolicy::new();
- if let Ok(vcert) = cert.with_policy(&null, None) {
- if let Ok(primary) = vcert.primary_userid() {
- println!(" {}", String::from_utf8_lossy(primary.value()));
- primary_uid = Some(primary.value().to_vec());
- }
- }
- }
-
- // As a last resort, pick the first user id.
- if primary_uid.is_none() {
- if let Some(primary) = cert.userids().next() {
- println!(" {}", String::from_utf8_lossy(primary.value()));
- primary_uid = Some(primary.value().to_vec());
+ let mut list = keyring_output::List::new(config.output_version)?;
+ let iter = CertParser::from_reader(input)?
+ .map(|item| keyring_output::ListItem::from_cert_with_config(item, &config));
+ for item in iter {
+ list.push(item);
+ }
+ match config.output_format {
+ OutputFormat::HumanReadable => {
+ for (i, item) in list.items().iter().enumerate() {
+ item.write(i, list_all_uids);
}
}
-
- if primary_uid.is_none() {
- // No dice.
+ OutputFormat::Json => {
+ serde_json::to_writer_pretty(std::io::stdout(), &list)?;
println!();
}
-
- if list_all_uids {
- // List all user ids independently of their validity.
- for u in cert.userids() {
- if primary_uid.as_ref()
- .map(|p| &p[..] == u.value()).unwrap_or(false)
- {
- // Skip the user id we already printed.
- continue;
- }
-
- println!("{} {}", indent,
- String::from_utf8_lossy(u.value()));
- }
- }
}
Ok(())
}
@@ -452,3 +407,166 @@ fn to_filename_fragment<S: AsRef<str>>(s: S) -> Option<String> {
None
}
}
+
+// Model output as a data type that can be serialized.
+mod keyring_output {
+ use super::{openpgp, Cert, Config, OutputVersion, Result};
+ use anyhow::anyhow;
+ use serde::Serialize;
+
+ #[derive(Debug, Serialize)]
+ #[serde(untagged)]
+ pub(super) enum List {
+ V0(ListV0),
+ }
+
+ impl List {
+ const DEFAULT_VERSION: OutputVersion = OutputVersion::new(0, 0, 0);
+
+ pub(super) fn new(wanted: Option<OutputVersion>) -> Result<Self> {
+ match wanted {
+ None => Self::new(Some(Self::DEFAULT_VERSION)),
+ Some(wanted) if ListV0::V.is_acceptable_for(wanted) => Ok(Self::V0(ListV0::new())),
+ Some(wanted) => Err(anyhow!("version not supported: {}", wanted)),
+ }
+ }
+
+ pub(super) fn push(&mut self, item: ListItem) {
+ match self {
+ Self::V0(list) => list.push(item),
+ }
+ }
+
+ pub(super) fn items(&mut self) -> &[ListItem] {
+ match self {
+ Self::V0(list) => list.items(),
+ }
+ }
+ }
+
+ #[derive(Debug, Serialize)]
+ pub(super) struct ListV0 {
+ sq_output_version: OutputVersion,
+ keys: Vec<ListItem>,
+ }
+
+ impl ListV0 {
+ const V: OutputVersion = OutputVersion::new(0, 0, 0);
+
+ fn new() -> Self {
+ Self {
+ sq_output_version: Self::V,
+ keys: vec![],
+ }
+ }
+
+ pub(super) fn push(&mut self, item: ListItem) {
+ self.keys.push(item);
+ }
+
+ pub(super) fn items(&self) -> &[ListItem] {
+ &self.keys
+ }
+ }
+
+ #[derive(Debug, Serialize)]
+ #[serde(untagged)]
+ pub(super) enum ListItem {
+ Error(String),
+ Cert(OutputCert),
+ }
+
+ impl ListItem {
+ pub(super) fn write(&self, i: usize, list_all_userids: bool) {
+ match self {
+ ListItem::Error(e) => {
+ println!("{}. {}", i, e);
+ },
+ ListItem::Cert(cert) => {
+ let line = format!("{}. {}", i, cert.fingerprint);
+ let indent = line.chars().map(|_| ' ').collect::<String>();
+ print!("{}", line);
+ match &cert.primary_userid {
+ Some(uid) => println!(" {}", uid),
+ None => println!(),
+ }
+ if list_all_userids {
+ for uid in &cert.userids {
+ println!("{} {}", indent, uid);
+ }
+ }
+ }
+ }
+ }
+
+ pub(super) fn from_cert_with_config(item: Result<Cert>, config: &Config) -> Self {
+ match item {
+ Ok(cert) => ListItem::Cert(OutputCert::from_cert_with_config(cert, config)),
+ Err(e) => ListItem::Error(format!("{}", e)),
+ }
+ }
+ }
+
+ #[derive(Debug, Serialize)]
+ pub(super) struct OutputCert {
+ fingerprint: String,
+ primary_userid: Option<String>,
+ userids: Vec<String>,
+ }
+
+ impl OutputCert {
+ fn from_cert_with_config(cert: Cert, config: &Config) -> Self {
+ // Try to be more helpful by including a User ID in the
+ // listing. We'd like it to be the primary one. Use
+ // decreasingly strict policies.
+ let mut primary_uid: Option<Vec<u8>> = None;
+
+ // First, apply our policy.
+ if let Ok(vcert) = cert.with_policy(&config.policy, None) {
+ if let Ok(primary) = vcert.primary_userid() {
+ primary_uid = Some(primary.value().to_vec());
+ }
+ }
+
+ // Second, apply the null policy.
+ if primary_uid.is_none() {
+ let null = openpgp::policy::NullPolicy::new();
+ if let Ok(vcert) = cert.with_policy(&null, None) {
+ if let Ok(primary) = vcert.primary_userid() {
+ primary_uid = Some(primary.value().to_vec());
+ }
+ }
+ }
+
+ // As a last resort, pick the first user id.
+ if primary_uid.is_none() {
+ if let Some(primary) = cert.userids().next() {
+ primary_uid = Some(primary.value().to_vec());
+ }
+ }
+
+ // List all user ids independently of their validity.
+ let mut userids = vec![];
+ for u in cert.userids() {
+ if primary_uid.as_ref()
+ .map(|p| &p[..] == u.value()).unwrap_or(false)
+ {
+ // Skip the user id we already handled.
+ continue;
+ }
+
+ userids.push(Self::userid(u.value()));
+ }
+
+ Self {
+ fingerprint: format!("{:X}", cert.fingerprint()),
+ primary_userid: primary_uid.map(|id| Self::userid(&id)),
+ userids,
+ }
+ }
+
+ fn userid(bytes: &[u8]) -> String {
+ String::from_utf8_lossy(bytes).into()
+ }
+ }
+}