From bb809992ad543ea4c0c31897fbf2d130394dd80e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 5 Sep 2020 13:18:50 +0200 Subject: Add json output format This patch adds a new output format, JSON. It uses serde to serialize the output types. todo: man page, changelog --- Cargo.lock | 24 ++++++++++++++++++++++++ Cargo.toml | 3 +++ src/arg_util.rs | 16 +++++++++++++++- src/args.rs | 27 +-------------------------- src/commands.rs | 33 ++++++++++++++++++++++----------- src/output.rs | 35 +++++++++++++++++++++++++++-------- 6 files changed, 92 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf55b89..2115b2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + [[package]] name = "lazy_static" version = "1.4.0" @@ -206,6 +212,7 @@ dependencies = [ "nitrokey-test-state", "regex", "serde", + "serde_json", "structopt", "toml", ] @@ -357,6 +364,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "serde" version = "1.0.114" @@ -377,6 +390,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "structopt" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index ad2d2f7..a1c0e3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ version = "0.7.1" version = "1.0" features = ["derive"] +[dependencies.serde_json] +version = "1.0" + [dependencies.structopt] version = "0.3.7" default-features = false diff --git a/src/arg_util.rs b/src/arg_util.rs index b1dd60f..86c5f48 100644 --- a/src/arg_util.rs +++ b/src/arg_util.rs @@ -54,7 +54,8 @@ macro_rules! Command { macro_rules! Enum { ( $(#[$docs:meta])* $name:ident, [ $( $var:ident => $str:expr, ) *] ) => { $(#[$docs])* - #[derive(Clone, Copy, Debug, PartialEq)] + #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)] + #[serde(rename_all = "lowercase")] pub enum $name { $( $var, @@ -126,6 +127,19 @@ macro_rules! enum_int { } } } + + impl<'de> ::serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result<$name, D::Error> + where + D: ::serde::Deserializer<'de>, + { + use ::serde::de::Error as _; + use ::std::str::FromStr as _; + + let s = String::deserialize(deserializer)?; + $name::from_str(&s).map_err(D::Error::custom) + } + } }; } diff --git a/src/args.rs b/src/args.rs index 758af8c..3936738 100644 --- a/src/args.rs +++ b/src/args.rs @@ -58,39 +58,14 @@ impl From for nitrokey::Model { } } -impl<'de> serde::Deserialize<'de> for DeviceModel { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error as _; - use std::str::FromStr as _; - - let s = String::deserialize(deserializer)?; - DeviceModel::from_str(&s).map_err(D::Error::custom) - } -} - Enum! { /// The format for the nitrocli output. OutputFormat, [ + Json => "json", Text => "text", ] } -impl<'de> serde::Deserialize<'de> for OutputFormat { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error as _; - use std::str::FromStr as _; - - let s = String::deserialize(deserializer)?; - OutputFormat::from_str(&s).map_err(D::Error::custom) - } -} - Command! { /// A top-level command for nitrocli. Command, [ diff --git a/src/commands.rs b/src/commands.rs index 808da97..7176017 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -250,6 +250,7 @@ where }) } +#[derive(serde::Serialize)] struct Status { model: args::DeviceModel, serial_number: String, @@ -259,6 +260,7 @@ struct Status { storage_status: Option, } +#[derive(serde::Serialize)] struct StorageStatus { card_id: u32, firmware_locked: bool, @@ -268,6 +270,7 @@ struct StorageStatus { hidden_volume: VolumeStatus, } +#[derive(serde::Serialize)] struct VolumeStatus { read_only: bool, active: bool, @@ -390,10 +393,11 @@ pub fn status(ctx: &mut Context<'_>) -> anyhow::Result<()> { None }, }; - output::Value::new(status).print(ctx) + output::Value::new("status", status).print(ctx) }) } +#[derive(serde::Serialize)] struct DeviceInfo { path: String, model: Option, @@ -439,7 +443,7 @@ impl output::TableItem for DeviceInfo { pub fn list(ctx: &mut Context<'_>, no_connect: bool) -> anyhow::Result<()> { set_log_level(ctx); - let mut table = output::Table::new("No Nitrokey device connected"); + let mut table = output::Table::new("devices", "No Nitrokey device connected"); let device_infos = nitrokey::list_devices().context("Failed to list connected Nitrokey devices")?; @@ -622,6 +626,7 @@ fn format_option(option: Option) -> String { } } +#[derive(serde::Serialize)] struct Config { numlock: Option, capslock: Option, @@ -664,7 +669,7 @@ pub fn config_get(ctx: &mut Context<'_>) -> anyhow::Result<()> { .get_config() .context("Failed to get configuration")? .into(); - output::Value::new(config).print(ctx) + output::Value::new("config", config).print(ctx) }) } @@ -754,7 +759,7 @@ pub fn otp_get( } else { get_otp(slot, algorithm, &mut device) }?; - output::Value::new(otp).print(ctx) + output::Value::new("code", otp).print(ctx) }) } @@ -843,6 +848,7 @@ pub fn otp_clear( }) } +#[derive(serde::Serialize)] struct OtpSlot { algorithm: args::OtpAlgorithm, slot: u8, @@ -912,7 +918,7 @@ fn get_otp_slots( /// Print the status of the OTP slots. pub fn otp_status(ctx: &mut Context<'_>, all: bool) -> anyhow::Result<()> { with_device(ctx, |ctx, device| { - let mut table = output::Table::new("No OTP slots programmed."); + let mut table = output::Table::new("otp-slots", "No OTP slots programmed."); table.append(&mut get_otp_slots(args::OtpAlgorithm::Hotp, &device, all)?); table.append(&mut get_otp_slots(args::OtpAlgorithm::Totp, &device, all)?); table.print(ctx) @@ -1003,6 +1009,7 @@ pub fn pin_unblock(ctx: &mut Context<'_>) -> anyhow::Result<()> { }) } +#[derive(serde::Serialize)] struct PwsSlotData { name: Option, login: Option, @@ -1085,11 +1092,14 @@ pub fn pws_get( None }; - output::Value::new(PwsSlotData { - name, - login, - password, - }) + output::Value::new( + "pws-slot", + PwsSlotData { + name, + login, + password, + }, + ) .print(ctx) }) } @@ -1116,6 +1126,7 @@ pub fn pws_clear(ctx: &mut Context<'_>, slot: u8) -> anyhow::Result<()> { }) } +#[derive(serde::Serialize)] struct PwsSlot { slot: u8, name: Option, @@ -1159,7 +1170,7 @@ fn get_pws_slot( /// Print the status of all PWS slots. pub fn pws_status(ctx: &mut Context<'_>, all: bool) -> anyhow::Result<()> { with_password_safe(ctx, |ctx, pws| { - let mut table = output::Table::new("No PWS slots programmed"); + let mut table = output::Table::new("otp-slots", "No PWS slots programmed"); let slots = pws .get_slot_status() .context("Failed to read PWS slot status")?; diff --git a/src/output.rs b/src/output.rs index 7c85a82..37daf92 100644 --- a/src/output.rs +++ b/src/output.rs @@ -5,8 +5,11 @@ //! Defines data types that can be formatted in different output formats. +use std::collections; use std::fmt; +use anyhow::Context as _; + use crate::args; use crate::Context; @@ -33,17 +36,21 @@ pub trait Output { } /// A single object. -pub struct Value(T); +pub struct Value { + key: String, + value: T, +} /// A list of objects of the same type that is displayed as a table with a fallback message for an /// empty list. pub struct Table { + key: String, items: Vec, empty_message: String, } /// A trait for objects that can be displayed in a table. -pub trait TableItem { +pub trait TableItem: serde::Serialize { /// Returns the column headers for this type of table items. fn headers() -> Vec<&'static str>; /// Returns the values of the column for this table item. @@ -56,23 +63,28 @@ pub struct TextObject { items: Vec<(usize, String, String)>, } -impl Value { - pub fn new(value: T) -> Value { - Value(value) +impl Value { + pub fn new(key: impl Into, value: T) -> Value { + Value { + key: key.into(), + value, + } } } -impl Output for Value { +impl Output for Value { fn format(&self, format: args::OutputFormat) -> anyhow::Result { match format { - args::OutputFormat::Text => Ok(self.0.to_string()), + args::OutputFormat::Json => get_json(&self.key, &self.value), + args::OutputFormat::Text => Ok(self.value.to_string()), } } } impl Table { - pub fn new(empty_message: impl Into) -> Table { + pub fn new(key: impl Into, empty_message: impl Into) -> Table { Table { + key: key.into(), items: Vec::new(), empty_message: empty_message.into(), } @@ -90,6 +102,7 @@ impl Table { impl Output for Table { fn format(&self, format: args::OutputFormat) -> anyhow::Result { match format { + args::OutputFormat::Json => get_json(&self.key, &self.items), args::OutputFormat::Text => { if self.items.is_empty() { Ok(self.empty_message.clone()) @@ -173,3 +186,9 @@ impl fmt::Display for TextObject { Ok(()) } } + +fn get_json(key: &str, value: &T) -> anyhow::Result { + let mut map = collections::HashMap::new(); + let _ = map.insert(key, value); + serde_json::to_string_pretty(&map).context("Could not serialize output to JSON") +} -- cgit v1.2.1