summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Mueller <deso@posteo.net>2019-01-05 21:41:22 -0800
committerDaniel Mueller <deso@posteo.net>2019-01-05 21:41:22 -0800
commit6d07a0c9d9a9b39247a9727dea2d90eba4e1fe9e (patch)
tree843fb189a543e6bc95ff9e5498f5ca2d09bc67a0
parentb750c4b13272908a51b85072008554c344b25016 (diff)
downloadnitrocli-6d07a0c9d9a9b39247a9727dea2d90eba4e1fe9e.tar.gz
nitrocli-6d07a0c9d9a9b39247a9727dea2d90eba4e1fe9e.tar.bz2
Supply customizable stdio channels to argparse
In order to properly test the program we need to have a way to intercept data printed to the stdio channels. There are different ways to accomplish that task. While it is reasonably easy to just start the program as a dedicated process doing so properly may be problematic from inside a test because either the path to the binary has to be retrieved or cargo -- the entity which knows the path -- be invoked. None of these approaches is very appealing from a testing and code complexity point of view: an additional fork means additional sources of errors and flakiness, executing cargo has the potential to even cause rebuilds of parts of the program, and while we are already testing against a slow I/O device this additional code running is unlikely to go unnoticed in the long-term. Lastly, doing so also means that we leave Rust's type safety behind when dealing with errors that could be nicely match'ed on when the test invocation is just a function call. To avoid all this complexity we instead strive for basically just running the main function. This patch marks a first step towards achieving this goal. It introduces the infrastructure to supply custom Write objects to the argument parsing functionality. Once more we piggy-back on the command execution context and add objects representing stdout and stderr to it. We further ensure that this context is passed to the argument parser invocations.
-rw-r--r--nitrocli/src/args.rs98
-rw-r--r--nitrocli/src/main.rs25
2 files changed, 84 insertions, 39 deletions
diff --git a/nitrocli/src/args.rs b/nitrocli/src/args.rs
index e45c4b9..f689643 100644
--- a/nitrocli/src/args.rs
+++ b/nitrocli/src/args.rs
@@ -25,6 +25,7 @@ use std::str;
use crate::commands;
use crate::error::Error;
use crate::pinentry;
+use crate::RunCtx;
type Result<T> = result::Result<T, Error>;
@@ -60,13 +61,29 @@ impl str::FromStr for DeviceModel {
}
}
+trait Stdio {
+ fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write);
+}
+
+impl<'io> Stdio for RunCtx<'io> {
+ fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) {
+ (self.stdout, self.stderr)
+ }
+}
+
/// A command execution context that captures additional data pertaining
/// the command execution.
-#[derive(Debug)]
pub struct ExecCtx<'io> {
pub model: Option<DeviceModel>,
+ pub stdout: &'io mut dyn io::Write,
+ pub stderr: &'io mut dyn io::Write,
pub verbosity: u64,
- data: std::marker::PhantomData<&'io u64>,
+}
+
+impl<'io> Stdio for ExecCtx<'io> {
+ fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) {
+ (self.stdout, self.stderr)
+ }
}
/// A top-level command for nitrocli.
@@ -369,7 +386,7 @@ enum PinCommand {
impl PinCommand {
fn execute(&self, ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
match *self {
- PinCommand::Clear => pin_clear(args),
+ PinCommand::Clear => pin_clear(ctx, args),
PinCommand::Set => pin_set(ctx, args),
PinCommand::Unblock => pin_unblock(ctx, args),
}
@@ -451,8 +468,13 @@ impl str::FromStr for PwsCommand {
}
}
-fn parse(parser: &argparse::ArgumentParser<'_>, args: Vec<String>) -> Result<()> {
- if let Err(err) = parser.parse(args, &mut io::stdout(), &mut io::stderr()) {
+fn parse(
+ ctx: &mut impl Stdio,
+ parser: &argparse::ArgumentParser<'_>,
+ args: Vec<String>,
+) -> Result<()> {
+ let (stdout, stderr) = ctx.stdio();
+ if let Err(err) = parser.parse(args, stdout, stderr) {
Err(Error::ArgparseError(err))
} else {
Ok(())
@@ -463,7 +485,7 @@ fn parse(parser: &argparse::ArgumentParser<'_>, args: Vec<String>) -> Result<()>
fn status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Prints the status of the connected Nitrokey device");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::status(ctx)
}
@@ -529,7 +551,7 @@ fn storage(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
"The arguments for the subcommand",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli storage {}", subcommand));
@@ -540,7 +562,7 @@ fn storage(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn storage_open(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Opens the encrypted volume on a Nitrokey Storage");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::storage_open(ctx)
}
@@ -549,7 +571,7 @@ fn storage_open(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn storage_close(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Closes the encrypted volume on a Nitrokey Storage");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::storage_close(ctx)
}
@@ -558,7 +580,7 @@ fn storage_close(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn storage_status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Prints the status of the Nitrokey's storage");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::storage_status(ctx)
}
@@ -580,7 +602,7 @@ fn config(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
"The arguments for the subcommand",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli config {}", subcommand));
@@ -591,7 +613,7 @@ fn config(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn config_get(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Prints the Nitrokey configuration");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::config_get(ctx)
}
@@ -648,7 +670,7 @@ fn config_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreTrue,
"Allow one-time password generation without PIN",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
let numlock = ConfigOption::try_from(no_numlock, numlock, "numlock")?;
@@ -668,7 +690,7 @@ fn config_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn lock(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Locks the connected Nitrokey device");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::lock(ctx)
}
@@ -690,7 +712,7 @@ fn otp(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
"The arguments for the subcommand",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli otp {}", subcommand));
@@ -719,7 +741,7 @@ fn otp_get(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreOption,
"The time to use for TOTP generation (Unix timestamp, default: system time)",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::otp_get(ctx, slot, algorithm, time)
@@ -783,7 +805,7 @@ pub fn otp_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreOption,
"The format of the secret (ascii|base32|hex)",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
if ascii {
@@ -825,7 +847,7 @@ fn otp_clear(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::Store,
"The OTP algorithm to use (hotp|totp)",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::otp_clear(ctx, slot, algorithm)
@@ -841,7 +863,7 @@ fn otp_status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreTrue,
"Show slots that are not programmed",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::otp_status(ctx, all)
@@ -864,7 +886,7 @@ fn pin(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
"The arguments for the subcommand",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli pin {}", subcommand));
@@ -872,10 +894,10 @@ fn pin(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
}
/// Clear the PIN as cached by various other commands.
-fn pin_clear(args: Vec<String>) -> Result<()> {
+fn pin_clear(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Clears the cached PINs");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::pin_clear()
}
@@ -890,7 +912,7 @@ fn pin_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::Store,
"The PIN type to change (admin|user)",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::pin_set(ctx, pintype)
@@ -900,7 +922,7 @@ fn pin_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
fn pin_unblock(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Unblocks and resets the user PIN");
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
commands::pin_unblock(ctx)
}
@@ -922,7 +944,7 @@ fn pws(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
"The arguments for the subcommand",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli pws {}", subcommand));
@@ -963,7 +985,7 @@ fn pws_get(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreTrue,
"Print the stored data without description",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::pws_get(ctx, slot, name, login, password, quiet)
@@ -997,7 +1019,7 @@ fn pws_set(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::Store,
"The password to store on the slot",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::pws_set(ctx, slot, &name, &login, &password)
@@ -1013,7 +1035,7 @@ fn pws_clear(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::Store,
"The PWS slot to clear",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::pws_clear(ctx, slot)
@@ -1029,7 +1051,7 @@ fn pws_status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
argparse::StoreTrue,
"Show slots that are not programmed",
);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
commands::pws_status(ctx, all)
@@ -1037,7 +1059,10 @@ fn pws_status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
/// Parse the command-line arguments and return the selected command and
/// the remaining arguments for the command.
-fn parse_arguments<'io>(args: Vec<String>) -> Result<(Command, ExecCtx<'io>, Vec<String>)> {
+fn parse_arguments<'io, 'ctx: 'io>(
+ ctx: &'ctx mut RunCtx<'_>,
+ args: Vec<String>,
+) -> Result<(Command, ExecCtx<'io>, Vec<String>)> {
let mut model: Option<DeviceModel> = None;
let mut verbosity = 0;
let mut command = Command::Status;
@@ -1065,17 +1090,22 @@ fn parse_arguments<'io>(args: Vec<String>) -> Result<(Command, ExecCtx<'io>, Vec
"The arguments for the command",
);
parser.stop_on_first_argument(true);
- parse(&parser, args)?;
+ parse(ctx, &parser, args)?;
drop(parser);
subargs.insert(0, format!("nitrocli {}", command));
- let ctx = ExecCtx { model, verbosity, data: Default::default() };
+ let ctx = ExecCtx {
+ model,
+ stdout: ctx.stdout,
+ stderr: ctx.stderr,
+ verbosity,
+ };
Ok((command, ctx, subargs))
}
/// Parse the command-line arguments and execute the selected command.
-pub fn handle_arguments(args: Vec<String>) -> Result<()> {
- let (command, mut ctx, args) = parse_arguments(args)?;
+pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> Result<()> {
+ let (command, mut ctx, args) = parse_arguments(ctx, args)?;
command.execute(&mut ctx, args)
}
diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs
index 4f39fdb..ad79c6e 100644
--- a/nitrocli/src/main.rs
+++ b/nitrocli/src/main.rs
@@ -1,7 +1,7 @@
// main.rs
// *************************************************************************
-// * Copyright (C) 2017-2018 Daniel Mueller (deso@posteo.net) *
+// * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net) *
// * *
// * This program is free software: you can redistribute it and/or modify *
// * it under the terms of the GNU General Public License as published by *
@@ -73,6 +73,8 @@ mod commands;
mod error;
mod pinentry;
+use std::env;
+use std::io;
use std::process;
use std::result;
@@ -80,9 +82,16 @@ use crate::error::Error;
type Result<T> = result::Result<T, Error>;
-fn run() -> i32 {
- let args = std::env::args().collect();
- match args::handle_arguments(args) {
+/// The context used when running the program.
+pub(crate) struct RunCtx<'io> {
+ /// The `Write` object used as standard output throughout the program.
+ pub stdout: &'io mut dyn io::Write,
+ /// The `Write` object used as standard error throughout the program.
+ pub stderr: &'io mut dyn io::Write,
+}
+
+fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec<String>) -> i32 {
+ match args::handle_arguments(ctx, args) {
Ok(()) => 0,
Err(err) => match err {
Error::ArgparseError(err) => match err {
@@ -100,5 +109,11 @@ fn run() -> i32 {
}
fn main() {
- process::exit(run());
+ let args = env::args().collect::<Vec<_>>();
+ let ctx = &mut RunCtx {
+ stdout: &mut io::stdout(),
+ stderr: &mut io::stderr(),
+ };
+
+ process::exit(run(ctx, args));
}