summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nitrocli/CHANGELOG.md2
-rw-r--r--nitrocli/Cargo.lock104
-rw-r--r--nitrocli/Cargo.toml3
-rw-r--r--nitrocli/doc/nitrocli.130
-rw-r--r--nitrocli/doc/nitrocli.1.pdfbin18442 -> 20399 bytes
-rw-r--r--nitrocli/src/args.rs204
-rw-r--r--nitrocli/src/error.rs8
-rw-r--r--nitrocli/src/main.rs8
-rw-r--r--nitrocli/src/tests/extension_model_test.py52
-rw-r--r--nitrocli/src/tests/mod.rs12
-rw-r--r--nitrocli/src/tests/run.rs112
11 files changed, 530 insertions, 5 deletions
diff --git a/nitrocli/CHANGELOG.md b/nitrocli/CHANGELOG.md
index e042cbc..dd95c6d 100644
--- a/nitrocli/CHANGELOG.md
+++ b/nitrocli/CHANGELOG.md
@@ -1,5 +1,7 @@
Unreleased
----------
+- Added support for user provided extensions through lookup via the
+ `PATH` environment variable
- Bumped required Rust version to `1.32`
diff --git a/nitrocli/Cargo.lock b/nitrocli/Cargo.lock
index 45f8090..9ae9299 100644
--- a/nitrocli/Cargo.lock
+++ b/nitrocli/Cargo.lock
@@ -22,6 +22,15 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "c2-chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.2.0",
+ "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "cc"
version = "1.0.40"
@@ -53,6 +62,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "getrandom"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.62",
+ "wasi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "lazy_static"
version = "1.2.0"
@@ -80,6 +99,7 @@ dependencies = [
"nitrokey-test 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -116,6 +136,11 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "ppv-lite86"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "proc-macro2"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -132,10 +157,47 @@ dependencies = [
]
[[package]]
+name = "rand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.62",
+ "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "rand_core"
version = "0.3.0"
[[package]]
+name = "rand_core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "rand_os"
version = "0.1.1"
dependencies = [
@@ -156,6 +218,11 @@ dependencies = [
]
[[package]]
+name = "redox_syscall"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "regex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -176,6 +243,14 @@ dependencies = [
]
[[package]]
+name = "remove_dir_all"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "syn"
version = "0.15.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -186,6 +261,19 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.62",
+ "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -209,6 +297,11 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "wasi"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "winapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -230,23 +323,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e"
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
+"checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101"
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+"checksum getrandom 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "fc344b02d3868feb131e8b5fe2b9b0a1cc42942679af493061fc13b853243872"
"checksum memchr 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e1dd4eaac298c32ce07eb6ed9242eda7d82955b9170b7d6db59b2e02cc63fcb8"
"checksum nitrokey-test 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e7e81b55db51769209e88a63cdbb4f2dc7ee9cd20ccaf32fbb940a3b0c50259"
"checksum nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a59b732ed6d5212424ed31ec9649f05652bcbc38f45f2292b27a6044e7098803"
+"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
"checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978"
"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
+"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c"
+"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
+"checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca"
+"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
+"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
+"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
+"checksum wasi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fd5442abcac6525a045cc8c795aedb60da7a2e5e89c7bf18a0d5357849bb23c7"
"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/nitrocli/Cargo.toml b/nitrocli/Cargo.toml
index add2f77..f48e7ca 100644
--- a/nitrocli/Cargo.toml
+++ b/nitrocli/Cargo.toml
@@ -64,6 +64,9 @@ version = "0.1"
[dev-dependencies.regex]
version = "1"
+[dev-dependencies.tempfile]
+version = "3"
+
[patch.crates-io]
argparse = { path = "../argparse" }
base32 = { path = "../base32" }
diff --git a/nitrocli/doc/nitrocli.1 b/nitrocli/doc/nitrocli.1
index 75f5960..4301bea 100644
--- a/nitrocli/doc/nitrocli.1
+++ b/nitrocli/doc/nitrocli.1
@@ -16,8 +16,8 @@ and the password safe.
.TP
\fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR
Restrict connections to the given device model.
-If this option is not set, nitrocli will connect to any connected Nitrokey Pro
-or Nitrokey Storage device.
+If this option is not set, \fBnitrocli\fR will connect to any connected Nitrokey
+Pro or Nitrokey Storage device.
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Enable additional logging and control its verbosity. Logging enabled through
@@ -264,6 +264,32 @@ The admin PIN cannot be unblocked.
This operation is equivalent to the unblock PIN option provided by \fBgpg\fR(1)
(using the \fB\-\-change\-pin\fR option).
+.SS Extensions
+In addition to the above built-in commands, \fBnitrocli\fR supports
+user-provided functionality in the form of extensions. An extension can be any
+executable file whose filename starts with "nitrocli-" and that is discoverable
+through lookup via the \fBPATH\fR environment variable.
+
+An extension should honor the following set of options, which are supplied to
+the extension by \fBnitrocli\fR itself:
+.TP
+\fB\-\-nitrocli\fR \fIpath\fR
+The path to the \fBnitrocli\fR binary. This path can be used to recursively
+invoke \fBnitrocli\fR to implement certain functionality. This option is
+guaranteed to be supplied.
+.TP
+\fB\-\-model pro\fR|\fBstorage\fR
+Restrict connections to the given device model (see the Options section for more
+details). This option is supplied only if it was provided by the user to the
+invocation of \fBnitrocli\fR itself.
+.TP
+\fB\-\-verbosity\fR \fIlevel\fR
+Control the logging verbosity by setting the log level to \fIlevel\fR. The
+default level is 0, which corresponds to an invocation of \fBnitrocli\fR
+without additional logging related options. Each additional occurrence of
+\fB\-v\fR/\fB\-\-verbose\fR increments the log level accordingly. This option is
+guaranteed to be supplied.
+
.SH ENVIRONMENT
The program honors a set of environment variables that can be used to
suppress interactive PIN entry through \fBpinentry\fR(1). The following
diff --git a/nitrocli/doc/nitrocli.1.pdf b/nitrocli/doc/nitrocli.1.pdf
index cd15a66..4cb75da 100644
--- a/nitrocli/doc/nitrocli.1.pdf
+++ b/nitrocli/doc/nitrocli.1.pdf
Binary files differ
diff --git a/nitrocli/src/args.rs b/nitrocli/src/args.rs
index f47f1a0..2e56e9e 100644
--- a/nitrocli/src/args.rs
+++ b/nitrocli/src/args.rs
@@ -17,8 +17,14 @@
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
// *************************************************************************
+use std::collections;
+use std::env;
use std::ffi;
+use std::fmt;
+use std::fs;
use std::io;
+use std::path;
+use std::process;
use std::result;
use std::str;
@@ -28,6 +34,7 @@ use crate::pinentry;
use crate::RunCtx;
type Result<T> = result::Result<T, Error>;
+type Extensions = collections::BTreeMap<String, path::PathBuf>;
/// Wraps a writer and buffers its output.
///
@@ -141,6 +148,87 @@ Enum! {Builtin, [
Unencrypted => ("unencrypted", unencrypted),
]}
+#[derive(Debug)]
+enum Command {
+ Builtin(Builtin),
+ Extension(String),
+}
+
+impl Command {
+ pub fn execute(
+ &self,
+ ctx: &mut ExecCtx<'_>,
+ args: Vec<String>,
+ extensions: Extensions,
+ ) -> Result<()> {
+ match self {
+ Command::Builtin(command) => command.execute(ctx, args),
+ Command::Extension(extension) => {
+ match extensions.get(extension) {
+ Some(path) => {
+ // Note that theoretically we could just exec the extension
+ // and be done. However, the problem with that approach is
+ // that it makes testing extension support much more nasty,
+ // because the test process would be overwritten in the
+ // process, requiring us to essentially fork & exec nitrocli
+ // beforehand -- which is much more involved from a cargo
+ // test context.
+ let mut cmd = process::Command::new(path);
+
+ if let Some(model) = ctx.model {
+ let _ = cmd.args(&["--model", model.as_ref()]);
+ };
+
+ let out = cmd
+ // TODO: We may want to take this path from the command
+ // execution context.
+ .args(&["--nitrocli", &env::current_exe()?.to_string_lossy()])
+ .args(&["--verbosity", &ctx.verbosity.to_string()])
+ .args(&args[1..])
+ .output()
+ .map_err(Into::<Error>::into)?;
+ ctx.stdout.write_all(&out.stdout)?;
+ ctx.stderr.write_all(&out.stderr)?;
+ if out.status.success() {
+ Ok(())
+ } else {
+ Err(Error::ExtensionFailed(
+ extension.to_string(),
+ out.status.code(),
+ ))
+ }
+ }
+ None => Err(Error::Error(format!("Unknown command: {}", extension))),
+ }
+ }
+ }
+ }
+}
+
+impl fmt::Display for Command {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Command::Builtin(cmd) => write!(f, "{}", cmd),
+ Command::Extension(ext) => write!(f, "{}", ext),
+ }
+ }
+}
+
+impl str::FromStr for Command {
+ type Err = ();
+
+ fn from_str(s: &str) -> result::Result<Self, Self::Err> {
+ Ok(match Builtin::from_str(s) {
+ Ok(cmd) => Command::Builtin(cmd),
+ // Note that at this point we cannot know whether the extension
+ // exists or not and so we always return success. However, if we
+ // fail looking up the corresponding command an error will be
+ // emitted later on.
+ Err(()) => Command::Extension(s.to_string()),
+ })
+ }
+}
+
Enum! {ConfigCommand, [
Get => ("get", config_get),
Set => ("set", config_set),
@@ -913,6 +1001,60 @@ fn pws_status(ctx: &mut ExecCtx<'_>, args: Vec<String>) -> Result<()> {
commands::pws_status(ctx, all)
}
+/// Find all the available extensions. Extensions are (executable) files
+/// that have the "nitrocli-" prefix and are discoverable via the `PATH`
+/// environment variable.
+// Note that we use a BTreeMap here to have a stable ordering among
+// extensions. That makes for a nicer user experience over a HashMap as
+// they appear in the help text and random changes in position are
+// confusing.
+fn find_extensions(path: &ffi::OsStr) -> Result<Extensions> {
+ // The std::env module has several references to the PATH environment
+ // variable, indicating that this name is considered platform
+ // independent from their perspective. We do the same.
+ let dirs = env::split_paths(path);
+ let mut commands = Extensions::new();
+ let prefix = format!("{}-", crate::NITROCLI);
+
+ for dir in dirs {
+ match fs::read_dir(&path::Path::new(&dir)) {
+ Ok(entries) => {
+ for entry in entries {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_file() {
+ let file = String::from(entry.file_name().to_str().unwrap());
+ // Note that we deliberately do not check whether the file
+ // we found is executable. If it is not we will just fail
+ // later on with a permission denied error. The reasons for
+ // this behavior are two fold:
+ // 1) Checking whether a file is executable in Rust is
+ // painful (as of 1.37 there exists the PermissionsExt
+ // trait but it is available only for Unix based
+ // systems).
+ // 2) It is considered a better user experience to show an
+ // extension that we found (we list them in the help
+ // text) even if it later turned out to be not usable
+ // over not showing it and silently doing nothing --
+ // mostly because anything residing in PATH should be
+ // executable anyway and given that its name also starts
+ // with nitrocli- we are pretty sure that's a bug on the
+ // user's side.
+ if file.starts_with(&prefix) {
+ let mut file = file;
+ file.replace_range(..prefix.len(), "");
+ assert!(commands.insert(file, path).is_none());
+ }
+ }
+ }
+ }
+ Err(ref err) if err.kind() == io::ErrorKind::NotFound => (),
+ x => x.map(|_| ())?,
+ }
+ }
+ Ok(commands)
+}
+
/// Parse the command-line arguments and execute the selected command.
pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> Result<()> {
use std::io::Write;
@@ -924,8 +1066,20 @@ pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> Resul
fmt_enum!(DeviceModel::all_variants())
);
let mut verbosity = 0;
- let mut command = Builtin::Status;
- let cmd_help = cmd_help!(command);
+ let path = ctx.path.take().unwrap_or_else(ffi::OsString::new);
+ let extensions = find_extensions(&path)?;
+ let mut command = Command::Builtin(Builtin::Status);
+ let commands = Builtin::all_variants()
+ .iter()
+ .map(AsRef::as_ref)
+ .map(ToOwned::to_owned)
+ .chain(extensions.keys().cloned())
+ .collect::<Vec<_>>()
+ .join("|");
+ // argparse's help text formatting is pretty bad for our intents and
+ // purposes. In particular, line breaks are just ignored by its custom
+ // line wrapping algorithm.
+ let cmd_help = format!("The command to execute ({})", commands);
let mut subargs = vec![];
let mut parser = argparse::ArgumentParser::new();
let _ = parser.refer(&mut version).add_option(
@@ -981,6 +1135,50 @@ pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> Resul
no_cache: ctx.no_cache,
verbosity,
};
- command.execute(&mut ctx, subargs)
+ command.execute(&mut ctx, subargs, extensions)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn no_extensions_available() -> Result<()> {
+ let exts = find_extensions(&ffi::OsString::new())?;
+ assert!(exts.is_empty(), "{:?}", exts);
+ Ok(())
+ }
+
+ #[test]
+ fn discover_extensions() -> Result<()> {
+ let dir1 = tempfile::tempdir()?;
+ let dir2 = tempfile::tempdir()?;
+
+ {
+ let ext1_path = dir1.path().join("nitrocli-ext1");
+ let ext2_path = dir1.path().join("nitrocli-ext2");
+ let ext3_path = dir2.path().join("nitrocli-super-1337-extensions111one");
+ let _ext1 = fs::File::create(&ext1_path)?;
+ let _ext2 = fs::File::create(&ext2_path)?;
+ let _ext3 = fs::File::create(&ext3_path)?;
+
+ let path = env::join_paths(&[dir1.path(), dir2.path()])
+ .map_err(|err| Error::Error(err.to_string()))?;
+ let exts = find_extensions(&path)?;
+
+ let mut it = exts.iter();
+ // Because we control the file names and the order of directories
+ // in `PATH` we can safely assume a fixed order in which
+ // extensions should be discovered.
+ assert_eq!(it.next(), Some((&"ext1".to_string(), &ext1_path)));
+ assert_eq!(it.next(), Some((&"ext2".to_string(), &ext2_path)));
+ assert_eq!(
+ it.next(),
+ Some((&"super-1337-extensions111one".to_string(), &ext3_path))
+ );
+ assert_eq!(it.next(), None);
+ }
+ Ok(())
}
}
diff --git a/nitrocli/src/error.rs b/nitrocli/src/error.rs
index 819bed8..1f6424e 100644
--- a/nitrocli/src/error.rs
+++ b/nitrocli/src/error.rs
@@ -45,6 +45,7 @@ pub enum Error {
NitrokeyError(Option<&'static str>, nitrokey::Error),
Utf8Error(str::Utf8Error),
Error(String),
+ ExtensionFailed(String, Option<i32>),
}
impl TryInto<nitrokey::Error> for Error {
@@ -99,6 +100,13 @@ impl fmt::Display for Error {
Error::Utf8Error(_) => write!(f, "Encountered UTF-8 conversion error"),
Error::IoError(ref e) => write!(f, "IO error: {}", e),
Error::Error(ref e) => write!(f, "{}", e),
+ Error::ExtensionFailed(ref ext, rc) => {
+ write!(f, "Extension {} failed", ext)?;
+ if let Some(rc) = rc {
+ write!(f, " with exit code {}", rc)?;
+ }
+ Ok(())
+ }
}
}
}
diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs
index bb4b007..92ecfe6 100644
--- a/nitrocli/src/main.rs
+++ b/nitrocli/src/main.rs
@@ -101,6 +101,8 @@ pub(crate) struct RunCtx<'io> {
pub stdout: &'io mut dyn io::Write,
/// The `Write` object used as standard error throughout the program.
pub stderr: &'io mut dyn io::Write,
+ /// The content of the `PATH` environment variable.
+ pub path: Option<ffi::OsString>,
/// The admin PIN, if provided through an environment variable.
pub admin_pin: Option<ffi::OsString>,
/// The user PIN, if provided through an environment variable.
@@ -129,6 +131,11 @@ fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec<String>) -> i32 {
// argparse printed an error message
_ => 1,
},
+ Error::ExtensionFailed(_, rc) => {
+ // We let the extension itself deal with error reporting, we
+ // just mirror its exit code (if any).
+ rc.unwrap_or(1)
+ }
_ => {
let _ = eprintln!(ctx, "{}", err);
1
@@ -146,6 +153,7 @@ fn main() {
let ctx = &mut RunCtx {
stdout: &mut stdout,
stderr: &mut stderr,
+ path: env::var_os("PATH"),
admin_pin: env::var_os(NITROCLI_ADMIN_PIN),
user_pin: env::var_os(NITROCLI_USER_PIN),
new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN),
diff --git a/nitrocli/src/tests/extension_model_test.py b/nitrocli/src/tests/extension_model_test.py
new file mode 100644
index 0000000..651c8e7
--- /dev/null
+++ b/nitrocli/src/tests/extension_model_test.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+
+from argparse import (
+ ArgumentParser,
+)
+from enum import (
+ Enum,
+)
+from sys import (
+ argv,
+ exit,
+)
+
+
+class Action(Enum):
+ """An action to perform."""
+ NITROCLI = "nitrocli"
+ MODEL = "model"
+ VERBOSITY = "verbosity"
+
+ @classmethod
+ def all(cls):
+ """Return the list of all the enum members' values."""
+ return [x.value for x in cls.__members__.values()]
+
+
+def main(args):
+ """The extension's main function."""
+ parser = ArgumentParser()
+ parser.add_argument(choices=Action.all(), dest="what")
+ parser.add_argument("--nitrocli", action="store", default=None)
+ parser.add_argument("--model", action="store", default=None)
+ # We deliberately store the argument to this option as a string
+ # because we can differentiate between None and a valid value, in
+ # order to verify that it really is supplied.
+ parser.add_argument("--verbosity", action="store", default=None)
+
+ namespace = parser.parse_args(args[1:])
+ if namespace.what == Action.NITROCLI.value:
+ print(namespace.nitrocli)
+ elif namespace.what == Action.MODEL.value:
+ print(namespace.model)
+ elif namespace.what == Action.VERBOSITY.value:
+ print(namespace.verbosity)
+ else:
+ return 1
+
+ return 0
+
+
+if __name__ == "__main__":
+ exit(main(argv))
diff --git a/nitrocli/src/tests/mod.rs b/nitrocli/src/tests/mod.rs
index dc15117..ce3f861 100644
--- a/nitrocli/src/tests/mod.rs
+++ b/nitrocli/src/tests/mod.rs
@@ -85,6 +85,15 @@ impl Builder {
self
}
+ /// Set the `PATH` used for looking up extensions.
+ fn path<P>(mut self, path: P) -> Self
+ where
+ P: Into<ffi::OsString>,
+ {
+ self.0.path = Some(path.into());
+ self
+ }
+
/// Set the password to use for certain operations.
fn password<P>(mut self, password: P) -> Self
where
@@ -102,6 +111,7 @@ impl Builder {
struct Nitrocli {
model: Option<nitrokey::Model>,
+ path: Option<ffi::OsString>,
admin_pin: Option<ffi::OsString>,
user_pin: Option<ffi::OsString>,
new_admin_pin: Option<ffi::OsString>,
@@ -113,6 +123,7 @@ impl Nitrocli {
pub fn new() -> Self {
Self {
model: None,
+ path: None,
admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()),
user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()),
new_admin_pin: None,
@@ -166,6 +177,7 @@ impl Nitrocli {
let ctx = &mut crate::RunCtx {
stdout: &mut stdout,
stderr: &mut stderr,
+ path: self.path.clone(),
admin_pin: self.admin_pin.clone(),
user_pin: self.user_pin.clone(),
new_admin_pin: self.new_admin_pin.clone(),
diff --git a/nitrocli/src/tests/run.rs b/nitrocli/src/tests/run.rs
index c59c660..2212577 100644
--- a/nitrocli/src/tests/run.rs
+++ b/nitrocli/src/tests/run.rs
@@ -17,6 +17,11 @@
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
// *************************************************************************
+use std::fs;
+use std::io::Write;
+use std::os::unix::fs::OpenOptionsExt;
+use std::path;
+
use super::*;
#[test]
@@ -101,3 +106,110 @@ fn version_option() {
test(&re, "--version");
test(&re, "-V");
}
+
+#[test]
+fn extension() -> crate::Result<()> {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(
+ br#"#!/usr/bin/env python
+print("success")
+"#,
+ )?;
+ }
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let out = Nitrocli::make().path(path).build().handle(&["ext"])?;
+ assert_eq!(out, "success\n");
+ Ok(())
+}
+
+#[test]
+fn extension_failure() -> crate::Result<()> {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(
+ br#"#!/usr/bin/env python
+import sys
+sys.exit(42);
+"#,
+ )?;
+ }
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let err = Nitrocli::make()
+ .path(path)
+ .build()
+ .handle(&["ext"])
+ .unwrap_err();
+
+ match err {
+ crate::Error::ExtensionFailed(ext, rc) => {
+ assert_eq!(&ext, "ext");
+ assert_eq!(rc, Some(42));
+ }
+ _ => panic!("Unexpected error variant found: {:?}", err),
+ };
+ Ok(())
+}
+
+#[test_device]
+fn extension_arguments(model: nitrokey::Model) -> crate::Result<()> {
+ fn test<F>(model: nitrokey::Model, what: &str, args: &[&str], check: F) -> crate::Result<()>
+ where
+ F: FnOnce(&str) -> bool,
+ {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(include_bytes!("extension_model_test.py"))?;
+ }
+
+ let mut args = args.to_vec();
+ args.append(&mut vec!["ext", what]);
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let out = Nitrocli::make()
+ .model(model)
+ .path(path)
+ .build()
+ .handle(&args)?;
+
+ assert!(check(&out), out);
+ Ok(())
+ }
+
+ test(model, "model", &[], |out| {
+ out == model.to_string().to_lowercase() + "\n"
+ })?;
+ test(model, "nitrocli", &[], |out| {
+ path::Path::new(out)
+ .file_stem()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .trim()
+ .contains("nitrocli")
+ })?;
+ test(model, "verbosity", &[], |out| out == "0\n")?;
+ test(model, "verbosity", &["-v"], |out| out == "1\n")?;
+ test(model, "verbosity", &["-v", "--verbose"], |out| out == "2\n")?;
+ Ok(())
+}