aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:18:50 +0200
committerRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:34:27 +0200
commitbb809992ad543ea4c0c31897fbf2d130394dd80e (patch)
treee22b26d7eb2236efa83bbc9de50065263a16c1c3
parent04b4262cdf4bbb4e2698d8ce51a261bf294a2da3 (diff)
downloadnitrocli-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.lock24
-rw-r--r--Cargo.toml3
-rw-r--r--src/arg_util.rs16
-rw-r--r--src/args.rs27
-rw-r--r--src/commands.rs33
-rw-r--r--src/output.rs35
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
@@ -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"
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<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")
+}