From 6d07a0c9d9a9b39247a9727dea2d90eba4e1fe9e Mon Sep 17 00:00:00 2001 From: Daniel Mueller Date: Sat, 5 Jan 2019 21:41:22 -0800 Subject: 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. --- nitrocli/src/args.rs | 98 ++++++++++++++++++++++++++++++++++------------------ nitrocli/src/main.rs | 25 +++++++++++--- 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 = result::Result; @@ -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, + 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) -> 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) -> 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, +) -> 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) -> Result<()> fn status(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> Result<()> { fn storage_open(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> Result<()> { fn storage_close(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> Result<()> { fn storage_status(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> Result<()> { fn config_get(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> Result<()> { fn lock(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Result<()> { } /// Clear the PIN as cached by various other commands. -fn pin_clear(args: Vec) -> Result<()> { +fn pin_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> Result<()> { fn pin_unblock(ctx: &mut ExecCtx<'_>, args: Vec) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Result<()> { /// Parse the command-line arguments and return the selected command and /// the remaining arguments for the command. -fn parse_arguments<'io>(args: Vec) -> Result<(Command, ExecCtx<'io>, Vec)> { +fn parse_arguments<'io, 'ctx: 'io>( + ctx: &'ctx mut RunCtx<'_>, + args: Vec, +) -> Result<(Command, ExecCtx<'io>, Vec)> { let mut model: Option = None; let mut verbosity = 0; let mut command = Command::Status; @@ -1065,17 +1090,22 @@ fn parse_arguments<'io>(args: Vec) -> 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) -> Result<()> { - let (command, mut ctx, args) = parse_arguments(args)?; +pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec) -> 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 = result::Result; -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) -> 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::>(); + let ctx = &mut RunCtx { + stdout: &mut io::stdout(), + stderr: &mut io::stderr(), + }; + + process::exit(run(ctx, args)); } -- cgit v1.2.3