diff options
author | Lars Wirzenius <liw@sequoia-pgp.org> | 2022-07-19 15:35:19 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@sequoia-pgp.org> | 2022-07-20 19:35:31 +0300 |
commit | c50f20f3553b835ec526f86e40c800d802c088be (patch) | |
tree | 65a7ec026e13f70ba306d1295715c9dccbe32037 | |
parent | f912db582870e27e5f2d6240d30c9fd776915397 (diff) |
sq: move all output models to src/output.rs
This makes it easier to manage them in one place. Also, allows one
place where the output model version is picked.
Also add integration tests to sq-subplot.md.
Sponsored-by: NLnet Foundation; NGI Assure
-rw-r--r-- | sq/sq-subplot.md | 18 | ||||
-rw-r--r-- | sq/src/commands/keyring.rs | 186 | ||||
-rw-r--r-- | sq/src/commands/net.rs | 100 | ||||
-rw-r--r-- | sq/src/output.rs | 281 | ||||
-rw-r--r-- | sq/src/sq.rs | 4 |
5 files changed, 322 insertions, 267 deletions
diff --git a/sq/sq-subplot.md b/sq/sq-subplot.md index a9184528..e5abf228 100644 --- a/sq/sq-subplot.md +++ b/sq/sq-subplot.md @@ -1546,6 +1546,21 @@ email address, and a subdirectory named after the email domain. given an installed sq when I run sq wkd url me@example.com then stdout contains "https://openpgpkey.example.com/.well-known/openpgpkey/example.com/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me" + +when I run sq --output-format=json wkd url me@example.com +then stdout, as JSON, matches pattern wkd.json +~~~ + +~~~{#wkd.json .file .json .numberLines} +{ + "sq_output_version": { + "major": 0, + "minor": 0, + "patch": 0 + }, + "advanced_url": "https://openpgpkey.example.com/.well-known/openpgpkey/example.com/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me", + "direct_url": "https://example.com/.well-known/openpgpkey/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me" +} ~~~ ## Direct WKD URL @@ -1558,6 +1573,9 @@ The direct URL lacks the subdomain and subdirectory of an advanced one. given an installed sq when I run sq wkd direct-url me@example.com then stdout contains "https://example.com/.well-known/openpgpkey/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me" + +when I run sq --output-format=json wkd url me@example.com +then stdout, as JSON, matches pattern wkd.json ~~~ ## Email local part in original form in WKD URL diff --git a/sq/src/commands/keyring.rs b/sq/src/commands/keyring.rs index 141bc747..d0ad45f9 100644 --- a/sq/src/commands/keyring.rs +++ b/sq/src/commands/keyring.rs @@ -28,9 +28,9 @@ use openpgp::{ use crate::{ Config, - OutputFormat, - OutputVersion, + Model, open_or_stdin, + output::KeyringListItem, }; use crate::sq_cli::KeyringCommand; @@ -241,23 +241,14 @@ fn list(config: Config, list_all_uids: bool) -> Result<()> { - let mut list = keyring_output::List::new(config.output_version)?; + let mut certs = vec![]; let iter = CertParser::from_reader(input)? - .map(|item| keyring_output::ListItem::from_cert_with_config(item, &config)); + .map(|item| KeyringListItem::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); - } - } - OutputFormat::Json => { - serde_json::to_writer_pretty(std::io::stdout(), &list)?; - println!(); - } + certs.push(item); } + let list = Model::keyring_list(config.output_version, certs, list_all_uids)?; + list.write(config.output_format, &mut std::io::stdout())?; Ok(()) } @@ -407,166 +398,3 @@ 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() - } - } -} diff --git a/sq/src/commands/net.rs b/sq/src/commands/net.rs index 5d6e235a..715ced61 100644 --- a/sq/src/commands/net.rs +++ b/sq/src/commands/net.rs @@ -24,10 +24,10 @@ use net::{ use crate::{ Config, - OutputFormat, - OutputVersion, + Model, open_or_stdin, serialize_keyring, + output::WkdUrlVariant, }; use crate::sq_cli::KeyserverCommand; @@ -101,24 +101,20 @@ pub fn dispatch_wkd(config: Config, c: WkdCommand) -> Result<()> { match c.subcommand { WkdSubcommands::Url(c) => { - let output = wkd_output::WkdUrl::new(None, &c.email_address)?; - match config.output_format { - OutputFormat::HumanReadable => println!("{}", output.advanced_url()), - OutputFormat::Json => { - serde_json::to_writer_pretty(std::io::stdout(), &output)?; - println!(); - } - } + let wkd_url = wkd::Url::from(&c.email_address)?; + let advanced = wkd_url.to_url(None)?.to_string(); + let direct = wkd_url.to_url(wkd::Variant::Direct)?.to_string(); + let output = Model::wkd_url(config.output_version, + WkdUrlVariant::Advanced, advanced, direct)?; + output.write(config.output_format, &mut std::io::stdout())?; }, WkdSubcommands::DirectUrl(c) => { - let output = wkd_output::WkdUrl::new(None, &c.email_address)?; - match config.output_format { - OutputFormat::HumanReadable => println!("{}", output.direct_url()), - OutputFormat::Json => { - serde_json::to_writer_pretty(std::io::stdout(), &output)?; - println!(); - } - } + let wkd_url = wkd::Url::from(&c.email_address)?; + let advanced = wkd_url.to_url(None)?.to_string(); + let direct = wkd_url.to_url(wkd::Variant::Direct)?.to_string(); + let output = Model::wkd_url(config.output_version, + WkdUrlVariant::Direct, advanced, direct)?; + output.write(config.output_format, &mut std::io::stdout())?; }, WkdSubcommands::Get(c) => { // Check that the policy allows https. @@ -178,71 +174,3 @@ pub fn dispatch_wkd(config: Config, c: WkdCommand) -> Result<()> { Ok(()) } - -// Model output as a data type that can be serialized. -mod wkd_output { - use super::{wkd, OutputVersion, Result}; - use anyhow::anyhow; - use serde::Serialize; - - #[derive(Debug, Serialize)] - #[serde(untagged)] - pub(super) enum WkdUrl { - V0(WkdUrlV0), - } - - impl WkdUrl { - const DEFAULT_VERSION: OutputVersion = OutputVersion::new(0, 0, 0); - - pub(super) fn new(wanted: Option<OutputVersion>, email: &str) -> Result<Self> { - let wkd_url = wkd::Url::from(email)?; - let advanced_url = wkd_url.to_url(None)?.to_string(); - let direct_url = wkd_url.to_url(wkd::Variant::Direct)?.to_string(); - - match wanted { - None => Self::new(Some(Self::DEFAULT_VERSION), email), - Some(wanted) if WkdUrlV0::V.is_acceptable_for(wanted) => Ok(Self::V0(WkdUrlV0::new(advanced_url, direct_url))), - Some(wanted) => Err(anyhow!("version not supported: {}", wanted)), - } - } - - pub(super) fn advanced_url(&self) -> &str { - match self { - Self::V0(url) => url.advanced_url(), - } - } - - pub(super) fn direct_url(&self) -> &str { - match self { - Self::V0(url) => url.direct_url(), - } - } - } - - #[derive(Debug, Serialize)] - pub(super) struct WkdUrlV0 { - sq_output_version: OutputVersion, - advanced_url: String, - direct_url: String, - } - - impl WkdUrlV0 { - const V: OutputVersion = OutputVersion::new(0, 0, 0); - - fn new(advanced_url: String, direct_url: String) -> Self { - Self { - sq_output_version: Self::V, - advanced_url, - direct_url, - } - } - - pub(super) fn advanced_url(&self) -> &str { - &self.advanced_url - } - - pub(super) fn direct_url(&self) -> &str { - &self.direct_url - } - } -} diff --git a/sq/src/output.rs b/sq/src/output.rs index ac3f0e33..eb7f5f55 100644 --- a/sq/src/output.rs +++ b/sq/src/output.rs @@ -5,10 +5,14 @@ use std::fmt; use std::str::FromStr; +use std::io::Write; use anyhow::{anyhow, Result}; use serde::Serialize; +pub use keyring::ListItem as KeyringListItem; +pub use wkd::WkdUrlVariant; + /// What output format to prefer, when there's an option? #[derive(Clone)] pub enum OutputFormat { @@ -116,6 +120,283 @@ fn parse_component(s: &str) -> Result<usize> { Ok(FromStr::from_str(s)?) } +/// A model for the output of `sq` subcommands. +/// +/// This is for adding machine-readable output (such as JSON) to +/// subcommand. Every subcommand is represented as a variant, for each +/// version of the output. Versioning is global. We keep the latest +/// subversion of each major version. +/// +/// Each variant is created by a dedicated function. +pub enum Model { + KeyringListV0(keyring::ListV0), + WkdUrlV0(wkd::UrlV0), +} + +impl Model { + const DEFAULT_VERSION: OutputVersion = OutputVersion::new(0, 0, 0); + + fn version(v: Option<OutputVersion>) -> OutputVersion { + v.unwrap_or(Self::DEFAULT_VERSION) + } + + /// Create a model for the output of `sq wkd url` and `sq wkd + /// direct-url` subcommands. + pub fn wkd_url(version: Option<OutputVersion>, + variant: wkd::WkdUrlVariant, + advanced_url: String, + direct_url: String) -> Result<Self> { + let version = Self::version(version); + let result = match version { + wkd::UrlV0::V => Self::WkdUrlV0(wkd::UrlV0::new(variant, advanced_url, direct_url)), + _ => return Err(anyhow!("unknown output version {:?}", version)), + }; + Ok(result) + } + + /// Create a model for the output of the `sq keyring list` + /// subcommand. + pub fn keyring_list(version: Option<OutputVersion>, certs: Vec<keyring::ListItem>, all_uids: bool) -> Result<Self> { + let version = Self::version(version); + let result = match version { + keyring::ListV0::V => Self::KeyringListV0(keyring::ListV0::new(certs, all_uids)), + _ => return Err(anyhow!("unknown output version {:?}", version)), + }; + Ok(result) + } + + /// Write the output of a model to an open write handle in the + /// format requested by the user. + pub fn write(&self, format: OutputFormat, w: &mut dyn Write) -> Result<()> { + match self { + Self::KeyringListV0(x) => { + match format { + OutputFormat::HumanReadable => x.human_readable(w)?, + OutputFormat::Json => x.json(w)? + } + } + Self::WkdUrlV0(x) => { + match format { + OutputFormat::HumanReadable => x.human_readable(w)?, + OutputFormat::Json => x.json(w)? + } + } + } + Ok(()) + } +} + +// Model output as a data type that can be serialized. +mod keyring { + use sequoia_openpgp as openpgp; + use openpgp::{ + Result, + cert::Cert, + }; + use crate::Config; + use super::{OutputVersion, Write}; + use serde::Serialize; + + #[derive(Debug, Serialize)] + pub struct ListV0 { + #[serde(skip)] + all_uids: bool, + sq_output_version: OutputVersion, + keys: Vec<ListItem>, + } + + impl ListV0 { + pub const V: OutputVersion = OutputVersion::new(0, 0, 0); + + pub fn new(keys: Vec<ListItem>, all_uids: bool) -> Self { + Self { + all_uids, + sq_output_version: Self::V, + keys, + } + } + + pub fn human_readable(&self, w: &mut dyn Write) -> Result<()> { + for (i, item) in self.keys.iter().enumerate() { + match item { + ListItem::Error(e) => { + writeln!(w, "{}. {}", i, e)?; + }, + ListItem::Cert(cert) => { + let line = format!("{}. {}", i, cert.fingerprint); + let indent = line.chars().map(|_| ' ').collect::<String>(); + write!(w, "{}", line)?; + match &cert.primary_userid { + Some(uid) => writeln!(w, " {}", uid)?, + None => writeln!(w)?, + } + if self.all_uids { + for uid in &cert.userids { + writeln!(w, "{} {}", indent, uid)?; + } + } + } + } + } + Ok(()) + } + + pub fn json(&self, w: &mut dyn Write) -> Result<()> { + serde_json::to_writer_pretty(w, &self)?; +// writeln!(w)?; + Ok(()) + } + } + + #[derive(Debug, Serialize)] + #[serde(untagged)] + pub enum ListItem { + Error(String), + Cert(OutputCert), + } + + impl ListItem { + pub 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 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 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() + } + } +} + +// Model output as a data type that can be serialized. +pub mod wkd { + use super::{OutputVersion, Result, Write}; + use serde::Serialize; + + #[derive(Debug)] + pub enum WkdUrlVariant { + Advanced, + Direct, + } + + #[derive(Debug, Serialize)] + pub struct UrlV0 { + #[serde(skip)] + variant: WkdUrlVariant, + sq_output_version: OutputVersion, + advanced_url: String, + direct_url: String, + } + + impl UrlV0 { + pub const V: OutputVersion = OutputVersion::new(0, 0, 0); + + pub fn new(variant: WkdUrlVariant, advanced_url: String, direct_url: String) -> Self { + Self { + sq_output_version: Self::V, + variant, + advanced_url, + direct_url, + } + } + + pub fn human_readable(&self, w: &mut dyn Write) -> Result<()> { + match self.variant { + WkdUrlVariant::Advanced => writeln!(w, "{}", self.advanced_url)?, + WkdUrlVariant::Direct => writeln!(w, "{}", self.direct_url)?, + } + Ok(()) + } + + pub fn json(&self, w: &mut dyn Write) -> Result<()> { + serde_json::to_writer_pretty(w, self)?; +// writeln!(w)?; + Ok(()) + } + } +} #[cfg(test)] mod test { diff --git a/sq/src/sq.rs b/sq/src/sq.rs index 7e5cd29c..bf761f41 100644 --- a/sq/src/sq.rs +++ b/sq/src/sq.rs @@ -34,8 +34,8 @@ use sq_cli::SqSubcommands; mod sq_cli; mod commands; -mod output; -use output::{OutputFormat, OutputVersion}; +pub mod output; +pub use output::{wkd::WkdUrlVariant, Model, OutputFormat, OutputVersion}; fn open_or_stdin(f: Option<&str>) -> Result<Box<dyn BufferedReader<()>>> { |