diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2020-09-05 13:18:50 +0200 |
---|---|---|
committer | Robin Krahl <robin.krahl@ireas.org> | 2020-09-05 13:34:27 +0200 |
commit | bb809992ad543ea4c0c31897fbf2d130394dd80e (patch) | |
tree | e22b26d7eb2236efa83bbc9de50065263a16c1c3 | |
parent | 04b4262cdf4bbb4e2698d8ce51a261bf294a2da3 (diff) | |
download | nitrocli-bb809992ad543ea4c0c31897fbf2d130394dd80e.tar.gz nitrocli-bb809992ad543ea4c0c31897fbf2d130394dd80e.tar.bz2 |
Add json output format
This patch adds a new output format, JSON. It uses serde to serialize
the output types.
todo: man page, changelog
-rw-r--r-- | Cargo.lock | 24 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/arg_util.rs | 16 | ||||
-rw-r--r-- | src/args.rs | 27 | ||||
-rw-r--r-- | src/commands.rs | 33 | ||||
-rw-r--r-- | src/output.rs | 35 |
6 files changed, 92 insertions, 46 deletions
@@ -152,6 +152,12 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -206,6 +212,7 @@ dependencies = [ "nitrokey-test-state", "regex", "serde", + "serde_json", "structopt", "toml", ] @@ -358,6 +365,12 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -378,6 +391,17 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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<D>(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<DeviceModel> for nitrokey::Model { } } -impl<'de> serde::Deserialize<'de> for DeviceModel { - fn deserialize<D>(deserializer: D) -> Result<DeviceModel, D::Error> - 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<D>(deserializer: D) -> Result<OutputFormat, D::Error> - 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<StorageStatus>, } +#[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<args::DeviceModel>, @@ -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<T: fmt::Display>(option: Option<T>) -> String { } } +#[derive(serde::Serialize)] struct Config { numlock: Option<u8>, capslock: Option<u8>, @@ -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<String>, login: Option<String>, @@ -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<String>, @@ -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: fmt::Display>(T); +pub struct Value<T: fmt::Display + serde::Serialize> { + 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<T: TableItem> { + key: String, items: Vec<T>, 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<T: fmt::Display> Value<T> { - pub fn new(value: T) -> Value<T> { - Value(value) +impl<T: fmt::Display + serde::Serialize> Value<T> { + pub fn new(key: impl Into<String>, value: T) -> Value<T> { + Value { + key: key.into(), + value, + } } } -impl<T: fmt::Display> Output for Value<T> { +impl<T: fmt::Display + serde::Serialize> Output for Value<T> { fn format(&self, format: args::OutputFormat) -> anyhow::Result<String> { 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<T: TableItem> Table<T> { - pub fn new(empty_message: impl Into<String>) -> Table<T> { + pub fn new(key: impl Into<String>, empty_message: impl Into<String>) -> Table<T> { Table { + key: key.into(), items: Vec::new(), empty_message: empty_message.into(), } @@ -90,6 +102,7 @@ impl<T: TableItem> Table<T> { impl<T: TableItem> Output for Table<T> { fn format(&self, format: args::OutputFormat) -> anyhow::Result<String> { 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<T: serde::Serialize + ?Sized>(key: &str, value: &T) -> anyhow::Result<String> { + let mut map = collections::HashMap::new(); + let _ = map.insert(key, value); + serde_json::to_string_pretty(&map).context("Could not serialize output to JSON") +} |