diff options
author | Jack Lloyd <[email protected]> | 2017-10-22 12:39:45 -0400 |
---|---|---|
committer | Jack Lloyd <[email protected]> | 2017-10-24 11:51:28 -0400 |
commit | 3feae2f7893090b263e762c79b47b99f7f4d07ba (patch) | |
tree | 09f0bac281886ff0003d8d07ee90c7aa2e729985 /src | |
parent | da0a1124242e3a108819df58054e8dd909268a00 (diff) |
Refactor option parsing in cli and test code
Allows cleaning up header includes, also somewhat smaller binaries.
Diffstat (limited to 'src')
-rw-r--r-- | src/cli/argparse.h | 278 | ||||
-rw-r--r-- | src/cli/cli.cpp | 250 | ||||
-rw-r--r-- | src/cli/cli.h | 479 | ||||
-rw-r--r-- | src/cli/cli_exceptions.h | 44 | ||||
-rw-r--r-- | src/cli/cli_rng.cpp | 89 | ||||
-rw-r--r-- | src/cli/compress.cpp | 13 | ||||
-rw-r--r-- | src/cli/credentials.h | 1 | ||||
-rw-r--r-- | src/cli/encryption.cpp | 8 | ||||
-rw-r--r-- | src/cli/main.cpp | 10 | ||||
-rw-r--r-- | src/cli/speed.cpp | 1 | ||||
-rw-r--r-- | src/cli/timing_tests.cpp | 2 | ||||
-rw-r--r-- | src/cli/tls_client.cpp | 2 | ||||
-rw-r--r-- | src/cli/tls_server.cpp | 1 | ||||
-rw-r--r-- | src/cli/tls_utils.cpp | 9 | ||||
-rw-r--r-- | src/cli/utils.cpp | 79 | ||||
-rw-r--r-- | src/tests/main.cpp | 439 | ||||
-rw-r--r-- | src/tests/test_runner.cpp | 332 | ||||
-rw-r--r-- | src/tests/test_runner.h | 43 |
18 files changed, 1133 insertions, 947 deletions
diff --git a/src/cli/argparse.h b/src/cli/argparse.h new file mode 100644 index 000000000..457716095 --- /dev/null +++ b/src/cli/argparse.h @@ -0,0 +1,278 @@ +/* +* (C) 2015,2017 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_ARGPARSE_H_ +#define BOTAN_ARGPARSE_H_ + +#include <string> +#include <map> +#include <set> +#include <vector> +#include <botan/parsing.h> +#include "cli_exceptions.h" + +namespace Botan_CLI { + +class Argument_Parser + { + public: + Argument_Parser(const std::string& spec, + const std::vector<std::string>& extra_flags = {}, + const std::vector<std::string>& extra_opts = {}); + + void parse_args(const std::vector<std::string>& params); + + bool flag_set(const std::string& flag) const; + + bool has_arg(const std::string& opt_name) const; + std::string get_arg(const std::string& option) const; + + std::string get_arg_or(const std::string& option, const std::string& otherwise) const; + + size_t get_arg_sz(const std::string& option) const; + + std::vector<std::string> get_arg_list(const std::string& what) const; + + private: + // set in constructor + std::vector<std::string> m_spec_args; + std::set<std::string> m_spec_flags; + std::map<std::string, std::string> m_spec_opts; + std::string m_spec_rest; + + // set in parse_args() + std::map<std::string, std::string> m_user_args; + std::set<std::string> m_user_flags; + std::vector<std::string> m_user_rest; + }; + +bool Argument_Parser::flag_set(const std::string& flag_name) const + { + return m_user_flags.count(flag_name) > 0; + } + +bool Argument_Parser::has_arg(const std::string& opt_name) const + { + return m_user_args.count(opt_name) > 0; + } + +std::string Argument_Parser::get_arg(const std::string& opt_name) const + { + auto i = m_user_args.find(opt_name); + if(i == m_user_args.end()) + { + // this shouldn't occur unless you passed the wrong thing to get_arg + throw CLI_Error("Unknown option " + opt_name + " used (program bug)"); + } + return i->second; + } + +std::string Argument_Parser::get_arg_or(const std::string& opt_name, const std::string& otherwise) const + { + auto i = m_user_args.find(opt_name); + if(i == m_user_args.end() || i->second.empty()) + { + return otherwise; + } + return i->second; + } + +size_t Argument_Parser::get_arg_sz(const std::string& opt_name) const + { + const std::string s = get_arg(opt_name); + + try + { + return static_cast<size_t>(std::stoul(s)); + } + catch(std::exception&) + { + throw CLI_Usage_Error("Invalid integer value '" + s + "' for option " + opt_name); + } + } + +std::vector<std::string> Argument_Parser::get_arg_list(const std::string& what) const + { + if(what != m_spec_rest) + { + throw CLI_Error("Unexpected list name '" + what + "'"); + } + return m_user_rest; + } + +void Argument_Parser::parse_args(const std::vector<std::string>& params) + { + std::vector<std::string> args; + for(auto const& param : params) + { + if(param.find("--") == 0) + { + // option + const auto eq = param.find('='); + + if(eq == std::string::npos) + { + const std::string opt_name = param.substr(2, std::string::npos); + + if(m_spec_flags.count(opt_name) == 0) + { + if(m_spec_opts.count(opt_name)) + { + throw CLI_Usage_Error("Invalid usage of option --" + opt_name + + " without value"); + } + else + { + throw CLI_Usage_Error("Unknown flag --" + opt_name); + } + } + m_user_flags.insert(opt_name); + } + else + { + const std::string opt_name = param.substr(2, eq - 2); + const std::string opt_val = param.substr(eq + 1, std::string::npos); + + if(m_spec_opts.count(opt_name) == 0) + { + throw CLI_Usage_Error("Unknown option --" + opt_name); + } + + m_user_args.insert(std::make_pair(opt_name, opt_val)); + } + } + else + { + // argument + args.push_back(param); + } + } + + bool seen_stdin_flag = false; + size_t arg_i = 0; + for(auto const& arg : m_spec_args) + { + if(arg_i >= args.size()) + { + // not enough arguments + throw CLI_Usage_Error("Invalid argument count, got " + + std::to_string(args.size()) + + " expected " + + std::to_string(m_spec_args.size())); + } + + m_user_args.insert(std::make_pair(arg, args[arg_i])); + + if(args[arg_i] == "-") + { + if(seen_stdin_flag) + { + throw CLI_Usage_Error("Cannot specify '-' (stdin) more than once"); + } + seen_stdin_flag = true; + } + + ++arg_i; + } + + if(m_spec_rest.empty()) + { + if(arg_i != args.size()) + { + throw CLI_Usage_Error("Too many arguments"); + } + } + else + { + m_user_rest.assign(args.begin() + arg_i, args.end()); + } + + // Now insert any defaults for options not supplied by the user + for(auto const& opt : m_spec_opts) + { + if(m_user_args.count(opt.first) == 0) + { + m_user_args.insert(opt); + } + } + } + +Argument_Parser::Argument_Parser(const std::string& spec, + const std::vector<std::string>& extra_flags, + const std::vector<std::string>& extra_opts) + { + class CLI_Error_Invalid_Spec : public CLI_Error + { + public: + explicit CLI_Error_Invalid_Spec(const std::string& spec) + : CLI_Error("Invalid command spec '" + spec + "'") {} + }; + + const std::vector<std::string> parts = Botan::split_on(spec, ' '); + + if(parts.size() == 0) + { + throw CLI_Error_Invalid_Spec(spec); + } + + for(size_t i = 1; i != parts.size(); ++i) + { + const std::string s = parts[i]; + + if(s.empty()) // ?!? (shouldn't happen) + { + throw CLI_Error_Invalid_Spec(spec); + } + + if(s.size() > 2 && s[0] == '-' && s[1] == '-') + { + // option or flag + + auto eq = s.find('='); + + if(eq == std::string::npos) + { + m_spec_flags.insert(s.substr(2, std::string::npos)); + } + else + { + m_spec_opts.insert(std::make_pair(s.substr(2, eq - 2), s.substr(eq + 1, std::string::npos))); + } + } + else if(s[0] == '*') + { + // rest argument + if(m_spec_rest.empty() && s.size() > 2) + { + m_spec_rest = s.substr(1, std::string::npos); + } + else + { + throw CLI_Error_Invalid_Spec(spec); + } + } + else + { + // named argument + if(!m_spec_rest.empty()) // rest arg wasn't last + { + throw CLI_Error_Invalid_Spec(spec); + } + + m_spec_args.push_back(s); + } + } + + for(std::string flag : extra_flags) + m_spec_flags.insert(flag); + for(std::string opt : extra_opts) + m_spec_opts.insert(std::make_pair(opt, "")); + } + + +} + +#endif diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp new file mode 100644 index 000000000..f23c3574d --- /dev/null +++ b/src/cli/cli.cpp @@ -0,0 +1,250 @@ +/* +* (C) 2015,2017 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "cli.h" +#include "argparse.h" +#include <botan/rng.h> +#include <botan/parsing.h> +#include <iostream> +#include <fstream> + +namespace Botan_CLI { + +Command::Command(const std::string& cmd_spec) : m_spec(cmd_spec) + { + // for checking all spec strings at load time + //m_args.reset(new Argument_Parser(m_spec)); + } + +Command::~Command() { /* for unique_ptr */ } + +std::string Command::help_text() const + { + return "Usage: " + m_spec + + "\n\nAll commands support --verbose --help --output= --error-output= --rng-type= --drbg-seed="; + } + +int Command::run(const std::vector<std::string>& params) + { + try + { + m_args.reset(new Argument_Parser(m_spec, + {"verbose", "help"}, + {"output", "error-output", "rng-type", "drbg-seed"})); + + m_args->parse_args(params); + + if(m_args->has_arg("output")) + { + const std::string output_file = get_arg("output"); + + if(output_file != "") + { + m_output_stream.reset(new std::ofstream(output_file, std::ios::binary)); + if(!m_output_stream->good()) + throw CLI_IO_Error("opening", output_file); + } + } + + if(m_args->has_arg("error-output")) + { + const std::string output_file = get_arg("error-output"); + + if(output_file != "") + { + m_error_output_stream.reset(new std::ofstream(output_file, std::ios::binary)); + if(!m_error_output_stream->good()) + throw CLI_IO_Error("opening", output_file); + } + } + + if(flag_set("help")) + { + output() << help_text() << "\n"; + return 2; + } + + this->go(); + return 0; + } + catch(CLI_Usage_Error& e) + { + error_output() << "Usage error: " << e.what() << "\n"; + error_output() << help_text() << "\n"; + return 1; + } + catch(std::exception& e) + { + error_output() << "Error: " << e.what() << "\n"; + return 2; + } + catch(...) + { + error_output() << "Error: unknown exception\n"; + return 2; + } + } + +bool Command::flag_set(const std::string& flag_name) const + { + return m_args->flag_set(flag_name); + } + +std::string Command::get_arg(const std::string& opt_name) const + { + return m_args->get_arg(opt_name); + } + +/* +* Like get_arg() but if the argument was not specified or is empty, returns otherwise +*/ +std::string Command::get_arg_or(const std::string& opt_name, const std::string& otherwise) const + { + return m_args->get_arg_or(opt_name, otherwise); + } + +size_t Command::get_arg_sz(const std::string& opt_name) const + { + return m_args->get_arg_sz(opt_name); + } + +std::vector<std::string> Command::get_arg_list(const std::string& what) const + { + return m_args->get_arg_list(what); + } + +std::ostream& Command::output() + { + if(m_output_stream.get()) + { + return *m_output_stream; + } + return std::cout; + } + +std::ostream& Command::error_output() + { + if(m_error_output_stream.get()) + { + return *m_error_output_stream; + } + return std::cerr; + } + +std::vector<uint8_t> Command::slurp_file(const std::string& input_file, + size_t buf_size) const + { + std::vector<uint8_t> buf; + auto insert_fn = [&](const uint8_t b[], size_t l) + { + buf.insert(buf.end(), b, b + l); + }; + this->read_file(input_file, insert_fn, buf_size); + return buf; + } + +std::string Command::slurp_file_as_str(const std::string& input_file, + size_t buf_size) const + { + std::string str; + auto insert_fn = [&](const uint8_t b[], size_t l) + { + str.append(reinterpret_cast<const char*>(b), l); + }; + this->read_file(input_file, insert_fn, buf_size); + return str; + } + +void Command::read_file(const std::string& input_file, + std::function<void (uint8_t[], size_t)> consumer_fn, + size_t buf_size) const + { + if(input_file == "-") + { + do_read_file(std::cin, consumer_fn, buf_size); + } + else + { + std::ifstream in(input_file, std::ios::binary); + if(!in) + { + throw CLI_IO_Error("reading file", input_file); + } + do_read_file(in, consumer_fn, buf_size); + } + } + +void Command::do_read_file(std::istream& in, + std::function<void (uint8_t[], size_t)> consumer_fn, + size_t buf_size) const + { + // Avoid an infinite loop on --buf-size=0 + std::vector<uint8_t> buf(buf_size == 0 ? 4096 : buf_size); + + while(in.good()) + { + in.read(reinterpret_cast<char*>(buf.data()), buf.size()); + const size_t got = static_cast<size_t>(in.gcount()); + consumer_fn(buf.data(), got); + } + } + +Botan::RandomNumberGenerator& Command::rng() + { + if(m_rng == nullptr) + { + m_rng = cli_make_rng(get_arg("rng-type"), get_arg("drbg-seed")); + } + + return *m_rng.get(); + } + +// Registration code + +Command::Registration::Registration(const std::string& name, Command::cmd_maker_fn maker_fn) + { + std::map<std::string, Command::cmd_maker_fn>& reg = Command::global_registry(); + + if(reg.count(name) > 0) + { + throw CLI_Error("Duplicated registration of command " + name); + } + + reg.insert(std::make_pair(name, maker_fn)); + } + +//static +std::map<std::string, Command::cmd_maker_fn>& Command::global_registry() + { + static std::map<std::string, Command::cmd_maker_fn> g_cmds; + return g_cmds; + } + +//static +std::vector<std::string> Command::registered_cmds() + { + std::vector<std::string> cmds; + for(auto& cmd : Command::global_registry()) + cmds.push_back(cmd.first); + return cmds; + } + +//static +std::unique_ptr<Command> Command::get_cmd(const std::string& name) + { + const std::map<std::string, Command::cmd_maker_fn>& reg = Command::global_registry(); + + std::unique_ptr<Command> r; + auto i = reg.find(name); + if(i != reg.end()) + { + r.reset(i->second()); + } + + return r; + } + +} diff --git a/src/cli/cli.h b/src/cli/cli.h index 1e81c2ff6..75de33515 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -8,64 +8,39 @@ #define BOTAN_CLI_H_ #include <botan/build.h> -#include <botan/parsing.h> -#include <botan/rng.h> - -#include <fstream> -#include <iostream> #include <functional> +#include <ostream> #include <map> #include <memory> -#include <set> #include <string> #include <vector> +#include "cli_exceptions.h" + +namespace Botan { + +class RandomNumberGenerator; + +} namespace Botan_CLI { -/* Declared in utils.cpp */ +class Argument_Parser; + +/* Declared in cli_rng.cpp */ std::unique_ptr<Botan::RandomNumberGenerator> cli_make_rng(const std::string& type, const std::string& hex_drbg_seed); -class CLI_Error : public std::runtime_error - { - public: - explicit CLI_Error(const std::string& s) : std::runtime_error(s) {} - }; - -class CLI_IO_Error : public CLI_Error +class Command { public: - CLI_IO_Error(const std::string& op, const std::string& who) : - CLI_Error("Error " + op + " " + who) {} - }; -class CLI_Usage_Error : public CLI_Error - { - public: - explicit CLI_Usage_Error(const std::string& what) : CLI_Error(what) {} - }; + /** + * Get a registered command + */ + static std::unique_ptr<Command> get_cmd(const std::string& name); -/* Thrown eg when a requested feature was compiled out of the library - or is not available, eg hashing with -*/ -class CLI_Error_Unsupported : public CLI_Error - { - public: - CLI_Error_Unsupported(const std::string& what, - const std::string& who) - : CLI_Error(what + " with '" + who + "' unsupported or not available") {} - }; + static std::vector<std::string> registered_cmds(); -class CLI_Error_Invalid_Spec : public CLI_Error - { - public: - explicit CLI_Error_Invalid_Spec(const std::string& spec) - : CLI_Error("Invalid command spec '" + spec + "'") {} - }; - -class Command - { - public: /** * The spec string specifies the format of the command line, eg for * a somewhat complicated command: @@ -105,156 +80,13 @@ class Command * Use of --help is captured in run() and returns help_text(). * Use of --verbose can be checked with verbose() or flag_set("verbose") */ - explicit Command(const std::string& cmd_spec) : m_spec(cmd_spec) - { - // for checking all spec strings at load time - //parse_spec(); - } - virtual ~Command() = default; + explicit Command(const std::string& cmd_spec); - int run(const std::vector<std::string>& params) - { - try - { - // avoid parsing specs except for the command actually running - parse_spec(); - - std::vector<std::string> args; - for(auto const& param : params) - { - if(param.find("--") == 0) - { - // option - const auto eq = param.find('='); - - if(eq == std::string::npos) - { - const std::string opt_name = param.substr(2, std::string::npos); - - if(m_spec_flags.count(opt_name) == 0) - { - if(m_spec_opts.count(opt_name)) - { - throw CLI_Usage_Error("Invalid usage of option --" + opt_name + - " without value"); - } - else - { - throw CLI_Usage_Error("Unknown flag --" + opt_name); - } - } - m_user_flags.insert(opt_name); - } - else - { - const std::string opt_name = param.substr(2, eq - 2); - const std::string opt_val = param.substr(eq + 1, std::string::npos); - - if(m_spec_opts.count(opt_name) == 0) - { - throw CLI_Usage_Error("Unknown option --" + opt_name); - } - - m_user_args.insert(std::make_pair(opt_name, opt_val)); - } - } - else - { - // argument - args.push_back(param); - } - } - - bool seen_stdin_flag = false; - size_t arg_i = 0; - for(auto const& arg : m_spec_args) - { - if(arg_i >= args.size()) - { - // not enough arguments - throw CLI_Usage_Error("Invalid argument count, got " + - std::to_string(args.size()) + - " expected " + - std::to_string(m_spec_args.size())); - } - - m_user_args.insert(std::make_pair(arg, args[arg_i])); - - if(args[arg_i] == "-") - { - if(seen_stdin_flag) - { - throw CLI_Usage_Error("Cannot specifiy '-' (stdin) more than once"); - } - seen_stdin_flag = true; - } - - ++arg_i; - } - - if(m_spec_rest.empty()) - { - if(arg_i != args.size()) - { - throw CLI_Usage_Error("Too many arguments"); - } - } - else - { - m_user_rest.assign(args.begin() + arg_i, args.end()); - } - - if(flag_set("help")) - { - output() << help_text() << "\n"; - return 2; - } - - if(m_user_args.count("output")) - { - m_output_stream.reset(new std::ofstream(get_arg("output"), std::ios::binary)); - } - - if(m_user_args.count("error-output")) - { - m_error_output_stream.reset(new std::ofstream(get_arg("error-output"), std::ios::binary)); - } - - // Now insert any defaults for options not supplied by the user - for(auto const& opt : m_spec_opts) - { - if(m_user_args.count(opt.first) == 0) - { - m_user_args.insert(opt); - } - } - - this->go(); - return 0; - } - catch(CLI_Usage_Error& e) - { - error_output() << "Usage error: " << e.what() << "\n"; - error_output() << help_text() << "\n"; - return 1; - } - catch(std::exception& e) - { - error_output() << "Error: " << e.what() << "\n"; - return 2; - } - catch(...) - { - error_output() << "Error: unknown exception\n"; - return 2; - } - } + virtual ~Command(); - virtual std::string help_text() const - { - return "Usage: " + m_spec + - "\n\nAll commands support --verbose --help --output= --error-output= --rng-type= --drbg-seed="; - } + int run(const std::vector<std::string>& params); + + virtual std::string help_text() const; const std::string& cmd_spec() const { @@ -268,230 +100,53 @@ class Command protected: - void parse_spec() - { - const std::vector<std::string> parts = Botan::split_on(m_spec, ' '); - - if(parts.size() == 0) - { - throw CLI_Error_Invalid_Spec(m_spec); - } - - for(size_t i = 1; i != parts.size(); ++i) - { - const std::string s = parts[i]; - - if(s.empty()) // ?!? (shouldn't happen) - { - throw CLI_Error_Invalid_Spec(m_spec); - } - - if(s.size() > 2 && s[0] == '-' && s[1] == '-') - { - // option or flag - - auto eq = s.find('='); - - if(eq == std::string::npos) - { - m_spec_flags.insert(s.substr(2, std::string::npos)); - } - else - { - m_spec_opts.insert(std::make_pair(s.substr(2, eq - 2), s.substr(eq + 1, std::string::npos))); - } - } - else if(s[0] == '*') - { - // rest argument - if(m_spec_rest.empty() && s.size() > 2) - { - m_spec_rest = s.substr(1, std::string::npos); - } - else - { - throw CLI_Error_Invalid_Spec(m_spec); - } - } - else - { - // named argument - if(!m_spec_rest.empty()) // rest arg wasn't last - { - throw CLI_Error("Invalid command spec " + m_spec); - } - - m_spec_args.push_back(s); - } - } - - m_spec_flags.insert("verbose"); - m_spec_flags.insert("help"); - m_spec_opts.insert(std::make_pair("output", "")); - m_spec_opts.insert(std::make_pair("error-output", "")); - m_spec_opts.insert(std::make_pair("rng-type", "")); - m_spec_opts.insert(std::make_pair("drbg-seed", "")); - } - /* * The actual functionality of the cli command implemented in subclas */ virtual void go() = 0; - std::ostream& output() - { - if(m_output_stream.get()) - { - return *m_output_stream; - } - return std::cout; - } + std::ostream& output(); - std::ostream& error_output() - { - if(m_error_output_stream.get()) - { - return *m_error_output_stream; - } - return std::cerr; - } + std::ostream& error_output(); bool verbose() const { return flag_set("verbose"); } - bool flag_set(const std::string& flag_name) const - { - return m_user_flags.count(flag_name) > 0; - } + bool flag_set(const std::string& flag_name) const; - std::string get_arg(const std::string& opt_name) const - { - auto i = m_user_args.find(opt_name); - if(i == m_user_args.end()) - { - // this shouldn't occur unless you passed the wrong thing to get_arg - throw CLI_Error("Unknown option " + opt_name + " used (program bug)"); - } - return i->second; - } + std::string get_arg(const std::string& opt_name) const; /* * Like get_arg() but if the argument was not specified or is empty, returns otherwise */ - std::string get_arg_or(const std::string& opt_name, const std::string& otherwise) const - { - auto i = m_user_args.find(opt_name); - if(i == m_user_args.end() || i->second.empty()) - { - return otherwise; - } - return i->second; - } + std::string get_arg_or(const std::string& opt_name, const std::string& otherwise) const; - size_t get_arg_sz(const std::string& opt_name) const - { - const std::string s = get_arg(opt_name); - - try - { - return static_cast<size_t>(std::stoul(s)); - } - catch(std::exception&) - { - throw CLI_Usage_Error("Invalid integer value '" + s + "' for option " + opt_name); - } - } + size_t get_arg_sz(const std::string& opt_name) const; - std::vector<std::string> get_arg_list(const std::string& what) const - { - if(what != m_spec_rest) - { - throw CLI_Error("Unexpected list name '" + what + "'"); - } - - return m_user_rest; - } + std::vector<std::string> get_arg_list(const std::string& what) const; /* * Read an entire file into memory and return the contents */ std::vector<uint8_t> slurp_file(const std::string& input_file, - size_t buf_size = 0) const - { - std::vector<uint8_t> buf; - auto insert_fn = [&](const uint8_t b[], size_t l) - { - buf.insert(buf.end(), b, b + l); - }; - this->read_file(input_file, insert_fn, buf_size); - return buf; - } - - /* - * Read an entire file into memory and return the contents - */ - Botan::secure_vector<uint8_t> slurp_file_locked(const std::string& input_file, - size_t buf_size = 0) const - { - Botan::secure_vector<uint8_t> buf; - auto insert_fn = [&](const uint8_t b[], size_t l) - { - buf.insert(buf.end(), b, b + l); - }; - this->read_file(input_file, insert_fn, buf_size); - return buf; - } + size_t buf_size = 0) const; std::string slurp_file_as_str(const std::string& input_file, - size_t buf_size = 0) const - { - std::string str; - auto insert_fn = [&](const uint8_t b[], size_t l) - { - str.append(reinterpret_cast<const char*>(b), l); - }; - this->read_file(input_file, insert_fn, buf_size); - return str; - } + size_t buf_size = 0) const; /* * Read a file calling consumer_fn() with the inputs */ void read_file(const std::string& input_file, std::function<void (uint8_t[], size_t)> consumer_fn, - size_t buf_size = 0) const - { - if(input_file == "-") - { - do_read_file(std::cin, consumer_fn, buf_size); - } - else - { - std::ifstream in(input_file, std::ios::binary); - if(!in) - { - throw CLI_IO_Error("reading file", input_file); - } - do_read_file(in, consumer_fn, buf_size); - } - } + size_t buf_size = 0) const; + void do_read_file(std::istream& in, std::function<void (uint8_t[], size_t)> consumer_fn, - size_t buf_size = 0) const - { - // Avoid an infinite loop on --buf-size=0 - std::vector<uint8_t> buf(buf_size == 0 ? 4096 : buf_size); - - while(in.good()) - { - in.read(reinterpret_cast<char*>(buf.data()), buf.size()); - const size_t got = static_cast<size_t>(in.gcount()); - consumer_fn(buf.data(), got); - } - } + size_t buf_size = 0) const; template<typename Alloc> void write_output(const std::vector<uint8_t, Alloc>& vec) @@ -499,75 +154,31 @@ class Command output().write(reinterpret_cast<const char*>(vec.data()), vec.size()); } - Botan::RandomNumberGenerator& rng() - { - if(m_rng == nullptr) - { - m_rng = cli_make_rng(get_arg("rng-type"), get_arg("drbg-seed")); - } - - return *m_rng.get(); - } + Botan::RandomNumberGenerator& rng(); private: - // set in constructor - std::string m_spec; + void parse_spec(); - // set in parse_spec() from m_spec - std::vector<std::string> m_spec_args; - std::set<std::string> m_spec_flags; - std::map<std::string, std::string> m_spec_opts; - std::string m_spec_rest; + typedef std::function<Command* ()> cmd_maker_fn; - // set in run() from user args - std::map<std::string, std::string> m_user_args; - std::set<std::string> m_user_flags; - std::vector<std::string> m_user_rest; + static std::map<std::string, cmd_maker_fn>& global_registry(); - std::unique_ptr<std::ofstream> m_output_stream; - std::unique_ptr<std::ofstream> m_error_output_stream; + // set in constructor + std::string m_spec; + + std::unique_ptr<Argument_Parser> m_args; + std::unique_ptr<std::ostream> m_output_stream; + std::unique_ptr<std::ostream> m_error_output_stream; std::unique_ptr<Botan::RandomNumberGenerator> m_rng; public: // the registry interface: - typedef std::function<Command* ()> cmd_maker_fn; - - static std::map<std::string, cmd_maker_fn>& global_registry() - { - static std::map<std::string, cmd_maker_fn> g_cmds; - return g_cmds; - } - - static std::unique_ptr<Command> get_cmd(const std::string& name) - { - auto& reg = Command::global_registry(); - - std::unique_ptr<Command> r; - auto i = reg.find(name); - if(i != reg.end()) - { - r.reset(i->second()); - } - - return r; - } - class Registration final { public: - Registration(const std::string& name, cmd_maker_fn maker_fn) - { - auto& reg = Command::global_registry(); - - if(reg.count(name) > 0) - { - throw CLI_Error("Duplicated registration of command " + name); - } - - Command::global_registry().insert(std::make_pair(name, maker_fn)); - } + Registration(const std::string& name, cmd_maker_fn maker_fn); }; }; diff --git a/src/cli/cli_exceptions.h b/src/cli/cli_exceptions.h new file mode 100644 index 000000000..ed7be3137 --- /dev/null +++ b/src/cli/cli_exceptions.h @@ -0,0 +1,44 @@ +/* +* (C) 2015 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_CLI_EXCEPTIONS_H_ +#define BOTAN_CLI_EXCEPTIONS_H_ + +namespace Botan_CLI { + +class CLI_Error : public std::runtime_error + { + public: + explicit CLI_Error(const std::string& s) : std::runtime_error(s) {} + }; + +class CLI_IO_Error : public CLI_Error + { + public: + CLI_IO_Error(const std::string& op, const std::string& who) : + CLI_Error("Error " + op + " " + who) {} + }; + +class CLI_Usage_Error : public CLI_Error + { + public: + explicit CLI_Usage_Error(const std::string& what) : CLI_Error(what) {} + }; + +/* Thrown eg when a requested feature was compiled out of the library + or is not available, eg hashing with +*/ +class CLI_Error_Unsupported : public CLI_Error + { + public: + CLI_Error_Unsupported(const std::string& what, + const std::string& who) + : CLI_Error(what + " with '" + who + "' unsupported or not available") {} + }; + +} + +#endif diff --git a/src/cli/cli_rng.cpp b/src/cli/cli_rng.cpp new file mode 100644 index 000000000..1e3ecb141 --- /dev/null +++ b/src/cli/cli_rng.cpp @@ -0,0 +1,89 @@ +/* +* (C) 2015,2017 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "cli.h" +#include <botan/rng.h> +#include <botan/entropy_src.h> +#include <botan/cpuid.h> +#include <botan/hex.h> + +#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) + #include <botan/auto_rng.h> +#endif + +#if defined(BOTAN_HAS_SYSTEM_RNG) + #include <botan/system_rng.h> +#endif + +#if defined(BOTAN_HAS_RDRAND_RNG) + #include <botan/rdrand_rng.h> +#endif + +#if defined(BOTAN_HAS_HMAC_DRBG) + #include <botan/hmac_drbg.h> +#endif + +namespace Botan_CLI { + +std::unique_ptr<Botan::RandomNumberGenerator> +cli_make_rng(const std::string& rng_type, const std::string& hex_drbg_seed) + { +#if defined(BOTAN_HAS_SYSTEM_RNG) + if(rng_type == "system" || rng_type.empty()) + { + return std::unique_ptr<Botan::RandomNumberGenerator>(new Botan::System_RNG); + } +#endif + +#if defined(BOTAN_HAS_RDRAND_RNG) + if(rng_type == "rdrand") + { + if(Botan::CPUID::has_rdrand()) + return std::unique_ptr<Botan::RandomNumberGenerator>(new Botan::RDRAND_RNG); + else + throw CLI_Error("RDRAND instruction not supported on this processor"); + } +#endif + + const std::vector<uint8_t> drbg_seed = Botan::hex_decode(hex_drbg_seed); + +#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) + if(rng_type == "auto" || rng_type == "entropy" || rng_type.empty()) + { + std::unique_ptr<Botan::RandomNumberGenerator> rng; + + if(rng_type == "entropy") + rng.reset(new Botan::AutoSeeded_RNG(Botan::Entropy_Sources::global_sources())); + else + rng.reset(new Botan::AutoSeeded_RNG); + + if(drbg_seed.size() > 0) + rng->add_entropy(drbg_seed.data(), drbg_seed.size()); + return rng; + } +#endif + +#if defined(BOTAN_HAS_HMAC_DRBG) && defined(BOTAN_AUTO_RNG_HMAC) + if(rng_type == "drbg") + { + std::unique_ptr<Botan::MessageAuthenticationCode> mac = + Botan::MessageAuthenticationCode::create(BOTAN_AUTO_RNG_HMAC); + std::unique_ptr<Botan::Stateful_RNG> rng(new Botan::HMAC_DRBG(std::move(mac))); + rng->add_entropy(drbg_seed.data(), drbg_seed.size()); + + if(rng->is_seeded() == false) + throw CLI_Error("For " + rng->name() + " a seed of at least " + + std::to_string(rng->security_level()/8) + + " bytes must be provided"); + + return std::unique_ptr<Botan::RandomNumberGenerator>(rng.release()); + } +#endif + + throw CLI_Error_Unsupported("RNG", rng_type); + } + +} diff --git a/src/cli/compress.cpp b/src/cli/compress.cpp index c60a33921..e9cd02290 100644 --- a/src/cli/compress.cpp +++ b/src/cli/compress.cpp @@ -8,6 +8,7 @@ #if defined(BOTAN_HAS_COMPRESSION) #include <botan/compression.h> + #include <fstream> #endif namespace Botan_CLI { @@ -75,17 +76,17 @@ class Compress final : public Command while(in.good()) { buf.resize(buf_size); - in.read(reinterpret_cast<char*>(&buf[0]), buf.size()); + in.read(reinterpret_cast<char*>(buf.data()), buf.size()); buf.resize(in.gcount()); compress->update(buf); - out.write(reinterpret_cast<const char*>(&buf[0]), buf.size()); + out.write(reinterpret_cast<const char*>(buf.data()), buf.size()); } buf.clear(); compress->finish(buf); - out.write(reinterpret_cast<const char*>(&buf[0]), buf.size()); + out.write(reinterpret_cast<const char*>(buf.data()), buf.size()); out.close(); } }; @@ -147,17 +148,17 @@ class Decompress final : public Command while(in.good()) { buf.resize(buf_size); - in.read(reinterpret_cast<char*>(&buf[0]), buf.size()); + in.read(reinterpret_cast<char*>(buf.data()), buf.size()); buf.resize(in.gcount()); decompress->update(buf); - out.write(reinterpret_cast<const char*>(&buf[0]), buf.size()); + out.write(reinterpret_cast<const char*>(buf.data()), buf.size()); } buf.clear(); decompress->finish(buf); - out.write(reinterpret_cast<const char*>(&buf[0]), buf.size()); + out.write(reinterpret_cast<const char*>(buf.data()), buf.size()); out.close(); } }; diff --git a/src/cli/credentials.h b/src/cli/credentials.h index 38f91141e..3b46c239c 100644 --- a/src/cli/credentials.h +++ b/src/cli/credentials.h @@ -11,7 +11,6 @@ #include <botan/credentials_manager.h> #include <botan/x509self.h> #include <botan/data_src.h> -#include <fstream> #include <memory> inline bool value_exists(const std::vector<std::string>& vec, diff --git a/src/cli/encryption.cpp b/src/cli/encryption.cpp index e8d5622c2..ffc1d0b48 100644 --- a/src/cli/encryption.cpp +++ b/src/cli/encryption.cpp @@ -9,7 +9,6 @@ #if defined(BOTAN_HAS_AES) && defined(BOTAN_HAS_AEAD_MODES) #include <botan/aead.h> -#include <iterator> #include <sstream> using namespace Botan; @@ -39,7 +38,7 @@ bool is_aead(const std::string &cipher) } secure_vector<byte> do_crypt(const std::string &cipher, - const secure_vector<byte> &input, + const std::vector<byte> &input, const SymmetricKey &key, const InitializationVector &iv, const OctetString &ad, @@ -97,7 +96,7 @@ class Encryption final : public Command const std::string ad_hex = get_arg_or("ad", ""); const size_t buf_size = get_arg_sz("buf-size"); - Botan::secure_vector<uint8_t> input = this->slurp_file_locked("-", buf_size); + const std::vector<uint8_t> input = this->slurp_file("-", buf_size); if (verbose()) { @@ -109,8 +108,7 @@ class Encryption final : public Command auto ad = OctetString(ad_hex); auto direction = flag_set("decrypt") ? Cipher_Dir::DECRYPTION : Cipher_Dir::ENCRYPTION; - auto data = do_crypt(VALID_MODES[mode], input, key, iv, ad, direction); - std::copy(data.begin(), data.end(), std::ostreambuf_iterator<char>(output())); + write_output(do_crypt(VALID_MODES[mode], input, key, iv, ad, direction)); } }; diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 5fbaf085e..6b0024465 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -7,25 +7,21 @@ #include "cli.h" #include <botan/version.h> -#include <botan/internal/stl_util.h> -#include <iterator> #include <sstream> +#include <iostream> namespace { std::string main_help() { - const std::set<std::string> avail_commands = - Botan::map_keys_as_set(Botan_CLI::Command::global_registry()); - std::ostringstream oss; oss << "Usage: botan <cmd> <cmd-options>\n"; oss << "Available commands:\n"; - for(auto& cmd_name : avail_commands) + for(const auto& cmd_name : Botan_CLI::Command::registered_cmds()) { - auto cmd = Botan_CLI::Command::get_cmd(cmd_name); + std::unique_ptr<Botan_CLI::Command> cmd = Botan_CLI::Command::get_cmd(cmd_name); oss << cmd->cmd_spec() << "\n"; } diff --git a/src/cli/speed.cpp b/src/cli/speed.cpp index 518adb857..e075e752d 100644 --- a/src/cli/speed.cpp +++ b/src/cli/speed.cpp @@ -23,6 +23,7 @@ #include <botan/mac.h> #include <botan/cipher_mode.h> #include <botan/entropy_src.h> +#include <botan/parsing.h> #include <botan/cpuid.h> #include <botan/internal/os_utils.h> #include <botan/version.h> diff --git a/src/cli/timing_tests.cpp b/src/cli/timing_tests.cpp index 258780845..d95df026c 100644 --- a/src/cli/timing_tests.cpp +++ b/src/cli/timing_tests.cpp @@ -20,6 +20,8 @@ #include "cli.h" #include <botan/hex.h> #include <sstream> +#include <fstream> + #include <botan/internal/os_utils.h> #if defined(BOTAN_HAS_SYSTEM_RNG) diff --git a/src/cli/tls_client.cpp b/src/cli/tls_client.cpp index d773cd81f..db0453a72 100644 --- a/src/cli/tls_client.cpp +++ b/src/cli/tls_client.cpp @@ -15,6 +15,8 @@ #include <botan/x509path.h> #include <botan/ocsp.h> #include <botan/hex.h> +#include <botan/parsing.h> +#include <fstream> #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER) #include <botan/tls_session_manager_sqlite.h> diff --git a/src/cli/tls_server.cpp b/src/cli/tls_server.cpp index 703967641..02c15bf7b 100644 --- a/src/cli/tls_server.cpp +++ b/src/cli/tls_server.cpp @@ -17,6 +17,7 @@ #include "credentials.h" #include <list> +#include <fstream> #if defined(BOTAN_TARGET_OS_IS_WINDOWS) #include <winsock2.h> diff --git a/src/cli/tls_utils.cpp b/src/cli/tls_utils.cpp index f62781af9..7b2474a69 100644 --- a/src/cli/tls_utils.cpp +++ b/src/cli/tls_utils.cpp @@ -114,13 +114,8 @@ class TLS_Ciphersuites final : public Command } else { - std::ifstream policy_file(policy_type); - if(!policy_file.good()) - { - throw CLI_Error("Error TLS policy '" + policy_type + "' is neither a file nor a known policy type"); - } - - policy.reset(new Botan::TLS::Text_Policy(policy_file)); + const std::string policy_txt = slurp_file_as_str(policy_type); + policy.reset(new Botan::TLS::Text_Policy(policy_txt)); } for(uint16_t suite_id : policy->ciphersuite_list(version, with_srp)) diff --git a/src/cli/utils.cpp b/src/cli/utils.cpp index 6b108eb43..1e2ddf42e 100644 --- a/src/cli/utils.cpp +++ b/src/cli/utils.cpp @@ -10,30 +10,15 @@ #include <botan/version.h> #include <botan/hash.h> #include <botan/mac.h> +#include <botan/rng.h> #include <botan/cpuid.h> #include <botan/hex.h> -#include <botan/entropy_src.h> +#include <botan/parsing.h> #if defined(BOTAN_HAS_BASE64_CODEC) #include <botan/base64.h> #endif -#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) - #include <botan/auto_rng.h> -#endif - -#if defined(BOTAN_HAS_SYSTEM_RNG) - #include <botan/system_rng.h> -#endif - -#if defined(BOTAN_HAS_RDRAND_RNG) - #include <botan/rdrand_rng.h> -#endif - -#if defined(BOTAN_HAS_HMAC_DRBG) - #include <botan/hmac_drbg.h> -#endif - #if defined(BOTAN_HAS_HTTP_UTIL) #include <botan/http_util.h> #endif @@ -44,64 +29,6 @@ namespace Botan_CLI { -std::unique_ptr<Botan::RandomNumberGenerator> -cli_make_rng(const std::string& rng_type, const std::string& hex_drbg_seed) - { -#if defined(BOTAN_HAS_SYSTEM_RNG) - if(rng_type == "system" || rng_type.empty()) - { - return std::unique_ptr<Botan::RandomNumberGenerator>(new Botan::System_RNG); - } -#endif - -#if defined(BOTAN_HAS_RDRAND_RNG) - if(rng_type == "rdrand") - { - if(Botan::CPUID::has_rdrand()) - return std::unique_ptr<Botan::RandomNumberGenerator>(new Botan::RDRAND_RNG); - else - throw CLI_Error("RDRAND instruction not supported on this processor"); - } -#endif - - const std::vector<uint8_t> drbg_seed = Botan::hex_decode(hex_drbg_seed); - -#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) - if(rng_type == "auto" || rng_type == "entropy" || rng_type.empty()) - { - std::unique_ptr<Botan::RandomNumberGenerator> rng; - - if(rng_type == "entropy") - rng.reset(new Botan::AutoSeeded_RNG(Botan::Entropy_Sources::global_sources())); - else - rng.reset(new Botan::AutoSeeded_RNG); - - if(drbg_seed.size() > 0) - rng->add_entropy(drbg_seed.data(), drbg_seed.size()); - return rng; - } -#endif - -#if defined(BOTAN_HAS_HMAC_DRBG) && defined(BOTAN_AUTO_RNG_HMAC) - if(rng_type == "drbg") - { - std::unique_ptr<Botan::MessageAuthenticationCode> mac = - Botan::MessageAuthenticationCode::create(BOTAN_AUTO_RNG_HMAC); - std::unique_ptr<Botan::Stateful_RNG> rng(new Botan::HMAC_DRBG(std::move(mac))); - rng->add_entropy(drbg_seed.data(), drbg_seed.size()); - - if(rng->is_seeded() == false) - throw CLI_Error("For " + rng->name() + " a seed of at least " + - std::to_string(rng->security_level()/8) + - " bytes must be provided"); - - return std::unique_ptr<Botan::RandomNumberGenerator>(rng.release()); - } -#endif - - throw CLI_Error_Unsupported("RNG", rng_type); - } - class Config_Info final : public Command { public: @@ -245,7 +172,7 @@ class RNG final : public Command } const std::string drbg_seed = get_arg("drbg-seed"); - std::unique_ptr<Botan::RNG> rng = cli_make_rng(type, drbg_seed); + std::unique_ptr<Botan::RandomNumberGenerator> rng = cli_make_rng(type, drbg_seed); for(const std::string& req : get_arg_list("bytes")) { diff --git a/src/tests/main.cpp b/src/tests/main.cpp index 41009ed19..3144b957d 100644 --- a/src/tests/main.cpp +++ b/src/tests/main.cpp @@ -4,445 +4,62 @@ * Botan is released under the Simplified BSD License (see license.txt) */ -#include "../cli/cli.h" -#include "tests.h" +#include "../cli/argparse.h" +#include "test_runner.h" #include <iostream> -#include <sstream> +#include <vector> #include <string> -#include <set> -#include <deque> #include <botan/version.h> -#include <botan/loadstor.h> - -#if defined(BOTAN_HAS_HMAC_DRBG) - #include <botan/hmac_drbg.h> -#endif - -#if defined(BOTAN_HAS_SYSTEM_RNG) - #include <botan/system_rng.h> -#endif - -#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) - #include <botan/auto_rng.h> -#endif #if defined(BOTAN_HAS_OPENSSL) #include <botan/internal/openssl.h> #endif -#if defined(BOTAN_TARGET_OS_HAS_THREADS) - #include <thread> - #include <future> -#endif - -namespace { - -class Test_Runner final : public Botan_CLI::Command +int main(int argc, char* argv[]) { - public: - Test_Runner() - : Command("test --threads=0 --run-long-tests --run-online-tests --test-runs=1 --drbg-seed= --data-dir=" - " --pkcs11-lib= --provider= --log-success *suites") {} - - std::unique_ptr<Botan::RandomNumberGenerator> - create_test_rng(const std::string& drbg_seed) - { - std::unique_ptr<Botan::RandomNumberGenerator> rng; - -#if defined(BOTAN_HAS_HMAC_DRBG) && defined(BOTAN_AUTO_RNG_HMAC) - - std::vector<uint8_t> seed = Botan::hex_decode(drbg_seed); - if(seed.empty()) - { - const uint64_t ts = Botan_Tests::Test::timestamp(); - seed.resize(8); - Botan::store_be(ts, seed.data()); - } - - output() << " rng:HMAC_DRBG(" << BOTAN_AUTO_RNG_HMAC << ") with seed '" << Botan::hex_encode(seed) << "'\n"; - - // Expand out the seed with a hash to make the DRBG happy - std::unique_ptr<Botan::MessageAuthenticationCode> mac = - Botan::MessageAuthenticationCode::create(BOTAN_AUTO_RNG_HMAC); - - mac->set_key(seed); - seed.resize(mac->output_length()); - mac->final(seed.data()); - - std::unique_ptr<Botan::HMAC_DRBG> drbg(new Botan::HMAC_DRBG(std::move(mac))); - drbg->initialize_with(seed.data(), seed.size()); - -#if defined(BOTAN_TARGET_OS_HAS_THREADS) - rng.reset(new Botan::Serialized_RNG(drbg.release())); -#else - rng = std::move(drbg); -#endif - -#endif - - if(!rng && drbg_seed != "") - throw Botan_Tests::Test_Error("HMAC_DRBG disabled in build, cannot specify DRBG seed"); - -#if defined(BOTAN_HAS_SYSTEM_RNG) - if(!rng) - { - output() << " rng:system\n"; - rng.reset(new Botan::System_RNG); - } -#endif - -#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) - if(!rng) - { - output() << " rng:autoseeded\n"; -#if defined(BOTAN_TARGET_OS_HAS_THREADS) - rng.reset(new Botan::Serialized_RNG(new Botan::AutoSeeded_RNG)); -#else - rng.reset(new Botan::AutoSeeded_RNG); -#endif - - } -#endif - - if(!rng) - { - // last ditch fallback for RNG-less build - class Bogus_Fallback_RNG final : public Botan::RandomNumberGenerator - { - public: - std::string name() const override { return "Bogus_Fallback_RNG"; } - - void clear() override { /* ignored */ } - void add_entropy(const uint8_t[], size_t) override { /* ignored */ } - bool is_seeded() const override { return true; } - - void randomize(uint8_t out[], size_t len) override - { - for(size_t i = 0; i != len; ++i) - { - m_x = (m_x * 31337 + 42); - out[i] = static_cast<uint8_t>(m_x >> 7); - } - } - - Bogus_Fallback_RNG() : m_x(1) {} - private: - uint32_t m_x; - }; - - output() << " rng:bogus\n"; - rng.reset(new Bogus_Fallback_RNG); - } - - return rng; - } - - std::string help_text() const override - { - std::ostringstream err; - - const std::string& spec = cmd_spec(); - - err << "Usage: botan-test" - << spec.substr(spec.find_first_of(' '), std::string::npos) - << "\n\nAvailable test suites\n" - << "----------------\n"; - - size_t line_len = 0; - - for(auto const& test : Botan_Tests::Test::registered_tests()) - { - err << test << " "; - line_len += test.size() + 1; - - if(line_len > 64) - { - err << "\n"; - line_len = 0; - } - } - - if(line_len > 0) - { - err << "\n"; - } - - return err.str(); - } - - void go() override - { - const size_t threads = get_arg_sz("threads"); - const std::string drbg_seed = get_arg("drbg-seed"); - const bool log_success = flag_set("log-success"); - const bool run_online_tests = flag_set("run-online-tests"); - const bool run_long_tests = flag_set("run-long-tests"); - const std::string data_dir = get_arg_or("data-dir", "src/tests/data"); - const std::string pkcs11_lib = get_arg("pkcs11-lib"); - const std::string provider = get_arg("provider"); - const size_t runs = get_arg_sz("test-runs"); - - std::vector<std::string> req = get_arg_list("suites"); - - if(req.empty()) - { - /* - If nothing was requested on the command line, run everything. First - run the "essentials" to smoke test, then everything else in - alphabetical order. - */ - req = {"block", "stream", "hash", "mac", "modes", "aead" - "kdf", "pbkdf", "hmac_drbg", "x931_rng", "util" - }; - - std::set<std::string> all_others = Botan_Tests::Test::registered_tests(); - - if(pkcs11_lib.empty()) - { - // do not run pkcs11 tests by default unless pkcs11-lib set - for(std::set<std::string>::iterator iter = all_others.begin(); iter != all_others.end();) - { - if((*iter).find("pkcs11") != std::string::npos) - { - iter = all_others.erase(iter); - } - else - { - ++iter; - } - } - } + std::cerr << Botan::runtime_version_check(BOTAN_VERSION_MAJOR, BOTAN_VERSION_MINOR, BOTAN_VERSION_PATCH); - for(auto f : req) - { - all_others.erase(f); - } + try + { + Botan_CLI::Argument_Parser parser("test --data-dir= --pkcs11-lib= --provider= --log-success " + "--verbose --help --run-long-tests --run-online-tests --test-runs=1 --drbg-seed= " + "*suites"); - req.insert(req.end(), all_others.begin(), all_others.end()); - } - else if(req.size() == 1 && req.at(0) == "pkcs11") - { - req = {"pkcs11-manage", "pkcs11-module", "pkcs11-slot", "pkcs11-session", "pkcs11-object", "pkcs11-rsa", - "pkcs11-ecdsa", "pkcs11-ecdh", "pkcs11-rng", "pkcs11-x509" - }; - } - else - { - std::set<std::string> all = Botan_Tests::Test::registered_tests(); - for(auto const& r : req) - { - if(all.find(r) == all.end()) - { - throw Botan_CLI::CLI_Usage_Error("Unknown test suite: " + r); - } - } - } + parser.parse_args(std::vector<std::string>(argv + 1, argv + argc)); - output() << "Testing " << Botan::version_string() << "\n"; - output() << "Starting tests"; + const std::string data_dir = parser.get_arg_or("data-dir", "src/tests/data"); + const std::string pkcs11_lib = parser.get_arg("pkcs11-lib"); + const std::string provider = parser.get_arg("provider"); + const std::string drbg_seed = parser.get_arg("drbg-seed"); - if(threads > 1) - { - output() << " threads:" << threads; - } + const bool log_success = parser.flag_set("log-success"); + const bool run_long_tests = parser.flag_set("run-long-tests"); + const bool run_online_tests = parser.flag_set("run-online-tests"); + const size_t test_runs = parser.get_arg_sz("test-runs"); - if(!pkcs11_lib.empty()) - { - output() << " pkcs11 library:" << pkcs11_lib; - } + const std::vector<std::string> suites = parser.get_arg_list("suites"); - Botan_Tests::Provider_Filter pf; - if(!provider.empty()) - { - output() << " provider:" << provider; - pf.set(provider); - } #if defined(BOTAN_HAS_OPENSSL) - if(provider.empty() || provider == "openssl") - { - ERR_load_crypto_strings(); - } -#endif - - std::unique_ptr<Botan::RandomNumberGenerator> rng = create_test_rng(drbg_seed); - - Botan_Tests::Test::setup_tests(log_success, run_online_tests, run_long_tests, - data_dir, pkcs11_lib, pf, rng.get()); - - for(size_t i = 0; i != runs; ++i) - { - const size_t failed = run_tests(req, output(), threads); - - // Throw so main returns an error - if(failed) - { - throw Botan_Tests::Test_Error("Test suite failure"); - } - } - } - - private: - - std::string report_out(const std::vector<Botan_Tests::Test::Result>& results, - size_t& tests_failed, - size_t& tests_ran) + if(provider.empty() || provider == "openssl") { - std::ostringstream out; - - std::map<std::string, Botan_Tests::Test::Result> combined; - for(auto const& result : results) - { - const std::string who = result.who(); - auto i = combined.find(who); - if(i == combined.end()) - { - combined.insert(std::make_pair(who, Botan_Tests::Test::Result(who))); - i = combined.find(who); - } - - i->second.merge(result); - } - - for(auto const& result : combined) - { - out << result.second.result_string(verbose()); - tests_failed += result.second.tests_failed(); - tests_ran += result.second.tests_run(); - } - - return out.str(); + ::ERR_load_crypto_strings(); } - - - size_t run_tests(const std::vector<std::string>& tests_to_run, - std::ostream& out, - size_t threads) - { - size_t tests_ran = 0, tests_failed = 0; - - const uint64_t start_time = Botan_Tests::Test::timestamp(); - - if(threads <= 1) - { - for(auto const& test_name : tests_to_run) - { - try - { - out << test_name << ':' << std::endl; - const auto results = Botan_Tests::Test::run_test(test_name, false); - out << report_out(results, tests_failed, tests_ran) << std::flush; - } - catch(std::exception& e) - { - out << "Test " << test_name << " failed with exception " << e.what() << std::flush; - } - } - } - else - { - -#if defined(BOTAN_TARGET_OS_HAS_THREADS) - /* - We're not doing this in a particularly nice way, and variance in time is - high so commonly we'll 'run dry' by blocking on the first future. But - plain C++11 <thread> is missing a lot of tools we'd need (like - wait_for_any on a set of futures) and there is no point pulling in an - additional dependency just for this. In any case it helps somewhat - (50-100% speedup) and provides a proof of concept for parallel testing. - */ - - typedef std::future<std::vector<Botan_Tests::Test::Result>> FutureResults; - std::deque<FutureResults> fut_results; - - for(auto const& test_name : tests_to_run) - { - auto run_it = [test_name]() -> std::vector<Botan_Tests::Test::Result> - { - try - { - return Botan_Tests::Test::run_test(test_name, false); - } - catch(std::exception& e) - { - Botan_Tests::Test::Result r(test_name); - r.test_failure("Exception thrown", e.what()); - return std::vector<Botan_Tests::Test::Result> {r}; - } - }; - - fut_results.push_back(std::async(std::launch::async, run_it)); - - while(fut_results.size() > threads) - { - out << report_out(fut_results[0].get(), tests_failed, tests_ran) << std::flush; - fut_results.pop_front(); - } - } - - while(fut_results.size() > 0) - { - out << report_out(fut_results[0].get(), tests_failed, tests_ran) << std::flush; - fut_results.pop_front(); - } -#else - out << "Threading support disabled\n"; - return 1; #endif - } - - const uint64_t total_ns = Botan_Tests::Test::timestamp() - start_time; - out << "Tests complete ran " << tests_ran << " tests in " - << Botan_Tests::Test::format_time(total_ns) << " "; - - if(tests_failed > 0) - { - out << tests_failed << " tests failed"; - } - else if(tests_ran > 0) - { - out << "all tests ok"; - } - - out << std::endl; - - return tests_failed; - } - - - }; - -BOTAN_REGISTER_COMMAND("test", Test_Runner); -} + Botan_Tests::Test_Runner tests(std::cout); -int main(int argc, char* argv[]) - { - std::cerr << Botan::runtime_version_check(BOTAN_VERSION_MAJOR, BOTAN_VERSION_MINOR, BOTAN_VERSION_PATCH); - - try - { - std::unique_ptr<Botan_CLI::Command> cmd(Botan_CLI::Command::get_cmd("test")); - - if(!cmd) - { - std::cerr << "Unable to retrieve testing helper (program bug)\n"; // WTF - return 1; - } - - std::vector<std::string> args(argv + 1, argv + argc); - return cmd->run(args); - } - catch(Botan::Exception& e) - { - std::cerr << "Exiting with library exception " << e.what() << std::endl; + return tests.run(suites, data_dir, pkcs11_lib, provider, + log_success, run_online_tests, run_long_tests, + drbg_seed, test_runs); } catch(std::exception& e) { - std::cerr << "Exiting with std exception " << e.what() << std::endl; + std::cerr << "Exiting with error: " << e.what() << std::endl; } catch(...) { std::cerr << "Exiting with unknown exception" << std::endl; } + return 2; } diff --git a/src/tests/test_runner.cpp b/src/tests/test_runner.cpp new file mode 100644 index 000000000..8d3fe7558 --- /dev/null +++ b/src/tests/test_runner.cpp @@ -0,0 +1,332 @@ +/* +* (C) 2017 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "test_runner.h" +#include "tests.h" + +#include <botan/version.h> +#include <botan/loadstor.h> +#include <botan/mac.h> + +#if defined(BOTAN_HAS_HMAC_DRBG) + #include <botan/hmac_drbg.h> +#endif + +#if defined(BOTAN_HAS_SYSTEM_RNG) + #include <botan/system_rng.h> +#endif + +#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) + #include <botan/auto_rng.h> +#endif + +namespace Botan_Tests { + +Test_Runner::Test_Runner(std::ostream& out) : m_output(out) {} + +namespace { + +std::unique_ptr<Botan::RandomNumberGenerator> +create_test_rng(const std::string& drbg_seed, std::ostream& output) + { + std::unique_ptr<Botan::RandomNumberGenerator> rng; + +#if defined(BOTAN_HAS_HMAC_DRBG) && defined(BOTAN_AUTO_RNG_HMAC) + + std::vector<uint8_t> seed = Botan::hex_decode(drbg_seed); + if(seed.empty()) + { + const uint64_t ts = Botan_Tests::Test::timestamp(); + seed.resize(8); + Botan::store_be(ts, seed.data()); + } + + output << " rng:HMAC_DRBG(" << BOTAN_AUTO_RNG_HMAC << ") with seed '" << Botan::hex_encode(seed) << "'\n"; + + // Expand out the seed with a hash to make the DRBG happy + std::unique_ptr<Botan::MessageAuthenticationCode> mac = + Botan::MessageAuthenticationCode::create(BOTAN_AUTO_RNG_HMAC); + + mac->set_key(seed); + seed.resize(mac->output_length()); + mac->final(seed.data()); + + std::unique_ptr<Botan::HMAC_DRBG> drbg(new Botan::HMAC_DRBG(std::move(mac))); + drbg->initialize_with(seed.data(), seed.size()); + +#if defined(BOTAN_TARGET_OS_HAS_THREADS) + rng.reset(new Botan::Serialized_RNG(drbg.release())); +#else + rng = std::move(drbg); +#endif + +#endif + + if(!rng && drbg_seed != "") + throw Botan_Tests::Test_Error("HMAC_DRBG disabled in build, cannot specify DRBG seed"); + +#if defined(BOTAN_HAS_SYSTEM_RNG) + if(!rng) + { + output << " rng:system\n"; + rng.reset(new Botan::System_RNG); + } +#endif + +#if defined(BOTAN_HAS_AUTO_SEEDING_RNG) + if(!rng) + { + output << " rng:autoseeded\n"; +#if defined(BOTAN_TARGET_OS_HAS_THREADS) + rng.reset(new Botan::Serialized_RNG(new Botan::AutoSeeded_RNG)); +#else + rng.reset(new Botan::AutoSeeded_RNG); +#endif + + } +#endif + + if(!rng) + { + // last ditch fallback for RNG-less build + class Bogus_Fallback_RNG final : public Botan::RandomNumberGenerator + { + public: + std::string name() const override { return "Bogus_Fallback_RNG"; } + + void clear() override { /* ignored */ } + void add_entropy(const uint8_t[], size_t) override { /* ignored */ } + bool is_seeded() const override { return true; } + + void randomize(uint8_t out[], size_t len) override + { + for(size_t i = 0; i != len; ++i) + { + m_x = (m_x * 31337 + 42); + out[i] = static_cast<uint8_t>(m_x >> 7); + } + } + + Bogus_Fallback_RNG() : m_x(1) {} + private: + uint32_t m_x; + }; + + output << " rng:bogus\n"; + rng.reset(new Bogus_Fallback_RNG); + } + + return rng; + } + +} + +std::string Test_Runner::help_text() const + { + std::ostringstream err; + + err << "Usage: botan-test " + << "--run-long-tests --run-online-tests --test-runs=1 --drbg-seed= --data-dir= --pkcs11-lib= --provider= --log-success" + << "\n\nAvailable test suites\n" + << "----------------\n"; + + size_t line_len = 0; + + for(auto const& test : Botan_Tests::Test::registered_tests()) + { + err << test << " "; + line_len += test.size() + 1; + + if(line_len > 64) + { + err << "\n"; + line_len = 0; + } + } + + if(line_len > 0) + { + err << "\n"; + } + + return err.str(); + } + +int Test_Runner::run(const std::vector<std::string>& requested_tests, + const std::string& data_dir, + const std::string& pkcs11_lib, + const std::string& provider, + bool log_success, + bool run_online_tests, + bool run_long_tests, + const std::string& drbg_seed, + size_t runs) + { + std::vector<std::string> req = requested_tests; + + if(req.empty()) + { + /* + If nothing was requested on the command line, run everything. First + run the "essentials" to smoke test, then everything else in + alphabetical order. + */ + req = {"block", "stream", "hash", "mac", "modes", "aead" + "kdf", "pbkdf", "hmac_drbg", "util" + }; + + std::set<std::string> all_others = Botan_Tests::Test::registered_tests(); + + if(pkcs11_lib.empty()) + { + // do not run pkcs11 tests by default unless pkcs11-lib set + for(std::set<std::string>::iterator iter = all_others.begin(); iter != all_others.end();) + { + if((*iter).find("pkcs11") != std::string::npos) + { + iter = all_others.erase(iter); + } + else + { + ++iter; + } + } + } + + for(auto f : req) + { + all_others.erase(f); + } + + req.insert(req.end(), all_others.begin(), all_others.end()); + } + else if(req.size() == 1 && req.at(0) == "pkcs11") + { + req = {"pkcs11-manage", "pkcs11-module", "pkcs11-slot", "pkcs11-session", "pkcs11-object", "pkcs11-rsa", + "pkcs11-ecdsa", "pkcs11-ecdh", "pkcs11-rng", "pkcs11-x509" + }; + } + else + { + std::set<std::string> all = Botan_Tests::Test::registered_tests(); + for(auto const& r : req) + { + if(all.find(r) == all.end()) + { + throw Botan_Tests::Test_Error("Unknown test suite: " + r); + } + } + } + + output() << "Testing " << Botan::version_string() << "\n"; + output() << "Starting tests"; + + if(!pkcs11_lib.empty()) + { + output() << " pkcs11 library:" << pkcs11_lib; + } + + Botan_Tests::Provider_Filter pf; + if(!provider.empty()) + { + output() << " provider:" << provider; + pf.set(provider); + } + + std::unique_ptr<Botan::RandomNumberGenerator> rng = create_test_rng(drbg_seed, output()); + + Botan_Tests::Test::setup_tests(log_success, run_online_tests, run_long_tests, + data_dir, pkcs11_lib, pf, rng.get()); + + for(size_t i = 0; i != runs; ++i) + { + const size_t failed = run_tests(req); + + // Throw so main returns an error + if(failed) + { + return failed; + } + } + + return 0; + } + +namespace { + +std::string report_out(const std::vector<Botan_Tests::Test::Result>& results, + size_t& tests_failed, + size_t& tests_ran) + { + std::ostringstream out; + + std::map<std::string, Botan_Tests::Test::Result> combined; + for(auto const& result : results) + { + const std::string who = result.who(); + auto i = combined.find(who); + if(i == combined.end()) + { + combined.insert(std::make_pair(who, Botan_Tests::Test::Result(who))); + i = combined.find(who); + } + + i->second.merge(result); + } + + for(auto const& result : combined) + { + const bool verbose = false; + out << result.second.result_string(verbose); + tests_failed += result.second.tests_failed(); + tests_ran += result.second.tests_run(); + } + + return out.str(); + } + +} + +size_t Test_Runner::run_tests(const std::vector<std::string>& tests_to_run) + { + size_t tests_ran = 0, tests_failed = 0; + + const uint64_t start_time = Botan_Tests::Test::timestamp(); + + for(auto const& test_name : tests_to_run) + { + try + { + output() << test_name << ':' << std::endl; + const auto results = Botan_Tests::Test::run_test(test_name, false); + output() << report_out(results, tests_failed, tests_ran) << std::flush; + } + catch(std::exception& e) + { + output() << "Test " << test_name << " failed with exception " << e.what() << std::flush; + } + } + + const uint64_t total_ns = Botan_Tests::Test::timestamp() - start_time; + output() << "Tests complete ran " << tests_ran << " tests in " + << Botan_Tests::Test::format_time(total_ns) << " "; + + if(tests_failed > 0) + { + output() << tests_failed << " tests failed"; + } + else if(tests_ran > 0) + { + output() << "all tests ok"; + } + + output() << std::endl; + + return tests_failed; + } + +} + diff --git a/src/tests/test_runner.h b/src/tests/test_runner.h new file mode 100644 index 000000000..7d111920c --- /dev/null +++ b/src/tests/test_runner.h @@ -0,0 +1,43 @@ +/* +* (C) 2017 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_TEST_RUNNER_H_ +#define BOTAN_TEST_RUNNER_H_ + +#include <iosfwd> +#include <string> +#include <vector> + +namespace Botan_Tests { + +class Test_Runner final + { + public: + Test_Runner(std::ostream& out); + + std::string help_text() const; + + int run(const std::vector<std::string>& requested_tests, + const std::string& data_dir, + const std::string& pkcs11_lib, + const std::string& provider, + bool log_success, + bool run_online_tests, + bool run_long_tests, + const std::string& drbg_seed, + size_t runs); + + private: + std::ostream& output() const { return m_output; } + + size_t run_tests(const std::vector<std::string>& tests_to_run); + + std::ostream& m_output; + }; + +} + +#endif |