summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLars Wirzenius <liw@sequoia-pgp.org>2022-07-19 15:35:19 +0300
committerLars Wirzenius <liw@sequoia-pgp.org>2022-07-20 19:35:31 +0300
commitc50f20f3553b835ec526f86e40c800d802c088be (patch)
tree65a7ec026e13f70ba306d1295715c9dccbe32037
parentf912db582870e27e5f2d6240d30c9fd776915397 (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.md18
-rw-r--r--sq/src/commands/keyring.rs186
-rw-r--r--sq/src/commands/net.rs100
-rw-r--r--sq/src/output.rs281
-rw-r--r--sq/src/sq.rs4
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<()>>> {