From 5948f63fc87cb1524139938cfa8f93a6ac08d012 Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Fri, 2 Feb 2018 00:08:01 +0000 Subject: [PATCH] When CryFS fails to load a file system, the process stops with a helpful error code, which can be used by GUI tools to show detailed messages. --- ChangeLog.txt | 1 + src/cpp-utils/testutils/CaptureStderrRAII.h | 42 +++++++++++ src/cryfs-cli/Cli.cpp | 65 ++++++++-------- src/cryfs-cli/Cli.h | 7 +- src/cryfs-cli/main.cpp | 8 +- src/cryfs-cli/program_options/Parser.cpp | 64 +++++++++------- src/cryfs-cli/program_options/Parser.h | 4 +- src/cryfs/CMakeLists.txt | 1 + src/cryfs/CryfsException.cpp | 1 + src/cryfs/CryfsException.h | 25 +++++++ src/cryfs/ErrorCodes.h | 50 +++++++++++++ src/cryfs/config/CryConfigLoader.cpp | 7 +- test/cryfs-cli/CliTest_ShowingHelp.cpp | 14 ++-- test/cryfs-cli/CliTest_WrongEnvironment.cpp | 42 ++++++----- test/cryfs-cli/program_options/ParserTest.cpp | 75 ++++++++++++------- test/cryfs-cli/testutils/CliTest.h | 21 +++--- 16 files changed, 299 insertions(+), 128 deletions(-) create mode 100644 src/cpp-utils/testutils/CaptureStderrRAII.h create mode 100644 src/cryfs/CryfsException.cpp create mode 100644 src/cryfs/CryfsException.h create mode 100644 src/cryfs/ErrorCodes.h diff --git a/ChangeLog.txt b/ChangeLog.txt index 88ee4a56..500082af 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -3,6 +3,7 @@ Version 0.9.9 (unreleased) Improvements: * Add --allow-filesystem-upgrade option which will upgrade old file systems without asking the user. This will be especially helpful for GUI tools. * Add --version option that shows the CryFS version and exits. +* When CryFS fails to load a file system, the process stops with a helpful error code, which can be used by GUI tools to show detailed messages. Version 0.9.8 -------------- diff --git a/src/cpp-utils/testutils/CaptureStderrRAII.h b/src/cpp-utils/testutils/CaptureStderrRAII.h new file mode 100644 index 00000000..17da8ce9 --- /dev/null +++ b/src/cpp-utils/testutils/CaptureStderrRAII.h @@ -0,0 +1,42 @@ +#pragma once +#ifndef MESSMER_CPPUTILS_CAPTURESTDERRRAII_H +#define MESSMER_CPPUTILS_CAPTURESTDERRRAII_H + +#include +#include +#include + +namespace cpputils { + +class CaptureStderrRAII final { +public: + CaptureStderrRAII() { + _oldBuffer = std::cerr.rdbuf(); + + // Capture stderr to _buffer + std::cerr.rdbuf(_buffer.rdbuf()); + } + + ~CaptureStderrRAII() { + // reset + std::cerr.rdbuf(_oldBuffer); + } + + std::string stderr() const { + return _buffer.str(); + } + + void EXPECT_MATCHES(const std::string ®ex) { + EXPECT_THAT(stderr(), testing::MatchesRegex(".*" + regex + ".*")); + } + +private: + std::stringstream _buffer; + std::streambuf *_oldBuffer; + + DISALLOW_COPY_AND_ASSIGN(CaptureStderrRAII); +}; + +} + +#endif diff --git a/src/cryfs-cli/Cli.cpp b/src/cryfs-cli/Cli.cpp index a0e5fea3..b56ccfa2 100644 --- a/src/cryfs-cli/Cli.cpp +++ b/src/cryfs-cli/Cli.cpp @@ -24,6 +24,7 @@ #include #include #include "Environment.h" +#include //TODO Many functions accessing the ProgramOptions object. Factor out into class that stores it as a member. //TODO Factor out class handling askPassword @@ -169,7 +170,7 @@ namespace cryfs { //TODO Test string password = _askPasswordFromStdin("Password: "); if (!_checkPassword(password)) { - throw std::runtime_error("Invalid password. Password cannot be empty."); + throw CryfsException("Invalid password. Password cannot be empty.", ErrorCode::EmptyPassword); } return password; } @@ -199,18 +200,13 @@ namespace cryfs { } CryConfigFile Cli::_loadOrCreateConfig(const ProgramOptions &options) { - try { - auto configFile = _determineConfigFile(options); - auto config = _loadOrCreateConfigFile(configFile, options.cipher(), options.blocksizeBytes(), options.allowFilesystemUpgrade()); - if (config == none) { - std::cerr << "Could not load config file. Did you enter the correct password?" << std::endl; - exit(1); - } - return std::move(*config); - } catch (const std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; - exit(1); + auto configFile = _determineConfigFile(options); + auto config = _loadOrCreateConfigFile(configFile, options.cipher(), options.blocksizeBytes(), + options.allowFilesystemUpgrade()); + if (config == none) { + throw CryfsException("Could not load config file. Did you enter the correct password?", ErrorCode::WrongPassword); } + return std::move(*config); } optional Cli::_loadOrCreateConfigFile(const bf::path &configFilePath, const optional &cipher, const optional &blocksizeBytes, bool allowFilesystemUpgrade) { @@ -262,11 +258,11 @@ namespace cryfs { //Try to list contents of base directory auto _rootDir = device->Load("/"); // this might throw an exception if the root blob doesn't exist if (_rootDir == none) { - throw std::runtime_error("Couldn't find root blob"); + throw CryfsException("Couldn't find root blob", ErrorCode::InvalidFilesystem); } auto rootDir = dynamic_pointer_move(*_rootDir); if (rootDir == none) { - throw std::runtime_error("Base directory blob doesn't contain a directory"); + throw CryfsException("Base directory blob doesn't contain a directory", ErrorCode::InvalidFilesystem); } (*rootDir)->children(); // Load children } @@ -293,39 +289,40 @@ namespace cryfs { } void Cli::_sanityChecks(const ProgramOptions &options) { - _checkDirAccessible(options.baseDir(), "base directory"); - _checkDirAccessible(options.mountDir(), "mount directory"); + _checkDirAccessible(options.baseDir(), "base directory", ErrorCode::InaccessibleBaseDir); + _checkDirAccessible(options.mountDir(), "mount directory", ErrorCode::InaccessibleMountDir); _checkMountdirDoesntContainBasedir(options); } - void Cli::_checkDirAccessible(const bf::path &dir, const std::string &name) { + void Cli::_checkDirAccessible(const bf::path &dir, const std::string &name, ErrorCode errorCode) { if (!bf::exists(dir)) { bool create = _console->askYesNo("Could not find " + name + ". Do you want to create it?", false); if (create) { if (!bf::create_directory(dir)) { - throw std::runtime_error("Error creating "+name); + throw CryfsException("Error creating "+name, errorCode); } } else { - throw std::runtime_error(name + " not found."); + //std::cerr << "Exit code: " << exitCode(errorCode) << std::endl; + throw CryfsException(name + " not found.", errorCode); } } if (!bf::is_directory(dir)) { - throw std::runtime_error(name+" is not a directory."); + throw CryfsException(name+" is not a directory.", errorCode); } - auto file = _checkDirWriteable(dir, name); - _checkDirReadable(dir, file, name); + auto file = _checkDirWriteable(dir, name, errorCode); + _checkDirReadable(dir, file, name, errorCode); } - shared_ptr Cli::_checkDirWriteable(const bf::path &dir, const std::string &name) { + shared_ptr Cli::_checkDirWriteable(const bf::path &dir, const std::string &name, ErrorCode errorCode) { auto path = dir / "tempfile"; try { return make_shared(path); } catch (const std::runtime_error &e) { - throw std::runtime_error("Could not write to "+name+"."); + throw CryfsException("Could not write to "+name+".", errorCode); } } - void Cli::_checkDirReadable(const bf::path &dir, shared_ptr tempfile, const std::string &name) { + void Cli::_checkDirReadable(const bf::path &dir, shared_ptr tempfile, const std::string &name, ErrorCode errorCode) { ASSERT(bf::equivalent(dir, tempfile->path().parent_path()), "This function should be called with a file inside the directory"); try { bool found = false; @@ -340,13 +337,13 @@ namespace cryfs { throw std::runtime_error("Error accessing "+name+"."); } } catch (const boost::filesystem::filesystem_error &e) { - throw std::runtime_error("Could not read from "+name+"."); + throw CryfsException("Could not read from "+name+".", errorCode); } } void Cli::_checkMountdirDoesntContainBasedir(const ProgramOptions &options) { if (_pathContains(options.mountDir(), options.baseDir())) { - throw std::runtime_error("base directory can't be inside the mount directory."); + throw CryfsException("base directory can't be inside the mount directory.", ErrorCode::BaseDirInsideMountDir); } } @@ -367,17 +364,21 @@ namespace cryfs { int Cli::main(int argc, const char *argv[]) { cpputils::showBacktraceOnSigSegv(); - _showVersion(); - - ProgramOptions options = program_options::Parser(argc, argv).parse(CryCiphers::supportedCipherNames()); try { + _showVersion(); + ProgramOptions options = program_options::Parser(argc, argv).parse(CryCiphers::supportedCipherNames()); _sanityChecks(options); _runFilesystem(options); + } catch (const CryfsException &e) { + if (e.errorCode() != ErrorCode::Success) { + std::cerr << "Error: " << e.what() << std::endl; + } + return exitCode(e.errorCode()); } catch (const std::runtime_error &e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); + return exitCode(ErrorCode::UnspecifiedError); } - return 0; + return exitCode(ErrorCode::Success); } } diff --git a/src/cryfs-cli/Cli.h b/src/cryfs-cli/Cli.h index 2a3d9f4b..395f5743 100644 --- a/src/cryfs-cli/Cli.h +++ b/src/cryfs-cli/Cli.h @@ -11,6 +11,7 @@ #include #include #include "CallAfterTimeout.h" +#include namespace cryfs { class Cli final { @@ -35,9 +36,9 @@ namespace cryfs { void _sanityChecks(const program_options::ProgramOptions &options); void _checkMountdirDoesntContainBasedir(const program_options::ProgramOptions &options); bool _pathContains(const boost::filesystem::path &parent, const boost::filesystem::path &child); - void _checkDirAccessible(const boost::filesystem::path &dir, const std::string &name); - std::shared_ptr _checkDirWriteable(const boost::filesystem::path &dir, const std::string &name); - void _checkDirReadable(const boost::filesystem::path &dir, std::shared_ptr tempfile, const std::string &name); + void _checkDirAccessible(const boost::filesystem::path &dir, const std::string &name, ErrorCode errorCode); + std::shared_ptr _checkDirWriteable(const boost::filesystem::path &dir, const std::string &name, ErrorCode errorCode); + void _checkDirReadable(const boost::filesystem::path &dir, std::shared_ptr tempfile, const std::string &name, ErrorCode errorCode); boost::optional> _createIdleCallback(boost::optional minutes, std::function callback); void _sanityCheckFilesystem(CryDevice *device); diff --git a/src/cryfs-cli/main.cpp b/src/cryfs-cli/main.cpp index 162b8c82..3bfffc6c 100644 --- a/src/cryfs-cli/main.cpp +++ b/src/cryfs-cli/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace cryfs; using cpputils::Random; @@ -17,8 +18,13 @@ int main(int argc, const char *argv[]) { auto &keyGenerator = Random::OSRandom(); return Cli(keyGenerator, SCrypt::DefaultSettings, make_shared(), make_shared()).main(argc, argv); + } catch (const CryfsException &e) { + if (e.errorCode() != ErrorCode::Success) { + std::cerr << "Error: " << e.what() << std::endl; + } + return exitCode(e.errorCode()); } catch (const std::exception &e) { cerr << "Error: " << e.what(); - return EXIT_FAILURE; + return exitCode(ErrorCode::UnspecifiedError); } } diff --git a/src/cryfs-cli/program_options/Parser.cpp b/src/cryfs-cli/program_options/Parser.cpp index b0802deb..0bc29eb8 100644 --- a/src/cryfs-cli/program_options/Parser.cpp +++ b/src/cryfs-cli/program_options/Parser.cpp @@ -3,12 +3,14 @@ #include #include #include +#include #include namespace po = boost::program_options; namespace bf = boost::filesystem; using namespace cryfs::program_options; using cryfs::CryConfigConsole; +using cryfs::CryfsException; using std::pair; using std::vector; using std::cerr; @@ -35,12 +37,10 @@ ProgramOptions Parser::parse(const vector &supportedCiphers) const { po::variables_map vm = _parseOptionsOrShowHelp(options.first, supportedCiphers); if (!vm.count("base-dir")) { - std::cerr << "Please specify a base directory.\n"; - _showHelpAndExit(); + _showHelpAndExit("Please specify a base directory.", ErrorCode::InvalidArguments); } if (!vm.count("mount-dir")) { - std::cerr << "Please specify a mount directory.\n"; - _showHelpAndExit(); + _showHelpAndExit("Please specify a mount directory.", ErrorCode::InvalidArguments); } bf::path baseDir = bf::absolute(vm["base-dir"].as()); bf::path mountDir = bf::absolute(vm["mount-dir"].as()); @@ -76,17 +76,23 @@ ProgramOptions Parser::parse(const vector &supportedCiphers) const { void Parser::_checkValidCipher(const string &cipher, const vector &supportedCiphers) { if (std::find(supportedCiphers.begin(), supportedCiphers.end(), cipher) == supportedCiphers.end()) { - std::cerr << "Invalid cipher: " << cipher << std::endl; - exit(1); + throw CryfsException("Invalid cipher: " + cipher, ErrorCode::InvalidArguments); } } po::variables_map Parser::_parseOptionsOrShowHelp(const vector &options, const vector &supportedCiphers) { try { - return _parseOptions(options, supportedCiphers); + return _parseOptions(options, supportedCiphers); + } catch (const CryfsException& e) { + // If CryfsException is thrown, we already know what's wrong. + // Show usage information and pass through the exception, don't catch it. + if (e.errorCode() != ErrorCode::Success) { + _showHelp(); + } + throw; } catch(const std::exception &e) { std::cerr << e.what() << std::endl; - _showHelpAndExit(); + _showHelpAndExit("Invalid arguments", ErrorCode::InvalidArguments); } } @@ -101,7 +107,7 @@ po::variables_map Parser::_parseOptions(const vector &options, const vec po::store(po::command_line_parser(_options.size(), _options.data()) .options(desc).positional(positional_desc).run(), vm); if (vm.count("help")) { - _showHelpAndExit(); + _showHelpAndExit("", ErrorCode::Success); } if (vm.count("show-ciphers")) { _showCiphersAndExit(supportedCiphers); @@ -159,28 +165,32 @@ void Parser::_addPositionalOptionForBaseDir(po::options_description *desc, po::p for (const auto &cipher : supportedCiphers) { std::cerr << cipher << "\n"; } - exit(0); + throw CryfsException("", ErrorCode::Success); } -[[noreturn]] void Parser::_showHelpAndExit() { - cerr << "Usage: cryfs [options] baseDir mountPoint [-- [FUSE Mount Options]]\n"; - po::options_description desc; - _addAllowedOptions(&desc); - cerr << desc << endl; - cerr << "Environment variables:\n" - << " " << Environment::FRONTEND_KEY << "=" << Environment::FRONTEND_NONINTERACTIVE << "\n" - << "\tWork better together with tools.\n" - << "\tWith this option set, CryFS won't ask anything, but use default values\n" - << "\tfor options you didn't specify on command line. Furthermore, it won't\n" - << "\task you to enter a new password a second time (password confirmation).\n" - << " " << Environment::NOUPDATECHECK_KEY << "=true\n" - << "\tBy default, CryFS connects to the internet to check for known\n" - << "\tsecurity vulnerabilities and new versions. This option disables this.\n" - << endl; - exit(1); +void Parser::_showHelp() { + cerr << "Usage: cryfs [options] baseDir mountPoint [-- [FUSE Mount Options]]\n"; + po::options_description desc; + _addAllowedOptions(&desc); + cerr << desc << endl; + cerr << "Environment variables:\n" + << " " << Environment::FRONTEND_KEY << "=" << Environment::FRONTEND_NONINTERACTIVE << "\n" + << "\tWork better together with tools.\n" + << "\tWith this option set, CryFS won't ask anything, but use default values\n" + << "\tfor options you didn't specify on command line. Furthermore, it won't\n" + << "\task you to enter a new password a second time (password confirmation).\n" + << " " << Environment::NOUPDATECHECK_KEY << "=true\n" + << "\tBy default, CryFS connects to the internet to check for known\n" + << "\tsecurity vulnerabilities and new versions. This option disables this.\n" + << endl; +} + +[[noreturn]] void Parser::_showHelpAndExit(const std::string& message, ErrorCode errorCode) { + _showHelp(); + throw CryfsException(message, errorCode); } [[noreturn]] void Parser::_showVersionAndExit() { // no need to show version because it was already shown in the CryFS header before parsing program options - exit(0); + throw CryfsException("", ErrorCode::Success); } diff --git a/src/cryfs-cli/program_options/Parser.h b/src/cryfs-cli/program_options/Parser.h index 97b1982c..4ecec05c 100644 --- a/src/cryfs-cli/program_options/Parser.h +++ b/src/cryfs-cli/program_options/Parser.h @@ -4,6 +4,7 @@ #include "ProgramOptions.h" #include +#include namespace cryfs { namespace program_options { @@ -19,7 +20,8 @@ namespace cryfs { static void _addAllowedOptions(boost::program_options::options_description *desc); static void _addPositionalOptionForBaseDir(boost::program_options::options_description *desc, boost::program_options::positional_options_description *positional); - [[noreturn]] static void _showHelpAndExit(); + static void _showHelp(); + [[noreturn]] static void _showHelpAndExit(const std::string& message, ErrorCode errorCode); [[noreturn]] static void _showCiphersAndExit(const std::vector &supportedCiphers); [[noreturn]] static void _showVersionAndExit(); static boost::program_options::variables_map _parseOptionsOrShowHelp(const std::vector &options, const std::vector &supportedCiphers); diff --git a/src/cryfs/CMakeLists.txt b/src/cryfs/CMakeLists.txt index 85001df9..1f4c322d 100644 --- a/src/cryfs/CMakeLists.txt +++ b/src/cryfs/CMakeLists.txt @@ -2,6 +2,7 @@ project (cryfs) set(LIB_SOURCES # cryfs.cpp + CryfsException.cpp config/crypto/outer/OuterConfig.cpp config/crypto/outer/OuterEncryptor.cpp config/crypto/CryConfigEncryptorFactory.cpp diff --git a/src/cryfs/CryfsException.cpp b/src/cryfs/CryfsException.cpp new file mode 100644 index 00000000..1b72efd4 --- /dev/null +++ b/src/cryfs/CryfsException.cpp @@ -0,0 +1 @@ +#include "CryfsException.h" diff --git a/src/cryfs/CryfsException.h b/src/cryfs/CryfsException.h new file mode 100644 index 00000000..24926dd6 --- /dev/null +++ b/src/cryfs/CryfsException.h @@ -0,0 +1,25 @@ +#pragma once +#ifndef MESSMER_CRYFS_CRYFSEXCEPTION_H +#define MESSMER_CRYFS_CRYFSEXCEPTION_H + +#include "ErrorCodes.h" +#include + +namespace cryfs { + +class CryfsException final : public std::runtime_error { +public: + CryfsException(std::string message, ErrorCode errorCode) + : std::runtime_error(std::move(message)), _errorCode(errorCode) {} + + ErrorCode errorCode() const { + return _errorCode; + } + +private: + ErrorCode _errorCode; +}; + +} + +#endif diff --git a/src/cryfs/ErrorCodes.h b/src/cryfs/ErrorCodes.h new file mode 100644 index 00000000..de170ab9 --- /dev/null +++ b/src/cryfs/ErrorCodes.h @@ -0,0 +1,50 @@ +#pragma once +#ifndef MESSMER_CRYFSCLI_EXITCODES_H +#define MESSMER_CRYFSCLI_EXITCODES_H + +namespace cryfs { + +enum class ErrorCode : int { + Success = 0, + + // An error happened that doesn't have an error code associated with it + UnspecifiedError = 1, + + // The command line arguments are invalid. + InvalidArguments = 10, + + // Couldn't load config file. Probably the password is wrong + WrongPassword = 11, + + // Password cannot be empty + EmptyPassword = 12, + + // The file system format is too new for this CryFS version. Please update your CryFS version. + TooNewFilesystemFormat = 13, + + // The file system format is too old for this CryFS version. Run with --allow-filesystem-upgrade to upgrade it. + TooOldFilesystemFormat = 14, + + // The file system uses a different cipher than the one specified on the command line using the --cipher argument. + WrongCipher = 15, + + // Base directory doesn't exist or is inaccessible (i.e. not read or writable or not a directory) + InaccessibleBaseDir = 16, + + // Mount directory doesn't exist or is inaccessible (i.e. not read or writable or not a directory) + InaccessibleMountDir = 17, + + // Base directory can't be a subdirectory of the mount directory + BaseDirInsideMountDir = 18, + + // Something's wrong with the file system. + InvalidFilesystem = 19, +}; + +inline int exitCode(ErrorCode code) { + return static_cast(code); +} + +} + +#endif diff --git a/src/cryfs/config/CryConfigLoader.cpp b/src/cryfs/config/CryConfigLoader.cpp index 64bf034c..2fdd658e 100644 --- a/src/cryfs/config/CryConfigLoader.cpp +++ b/src/cryfs/config/CryConfigLoader.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "../CryfsException.h" namespace bf = boost::filesystem; using cpputils::unique_ref; @@ -58,19 +59,19 @@ optional CryConfigLoader::_loadConfig(const bf::path &filename, b void CryConfigLoader::_checkVersion(const CryConfig &config, bool allowFilesystemUpgrade) { if (gitversion::VersionCompare::isOlderThan(gitversion::VersionString(), config.Version())) { if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + " and should not be opened with older versions. It is strongly recommended to update your CryFS version. However, if you have backed up your base directory and know what you're doing, you can continue trying to load it. Do you want to continue?", false)) { - throw std::runtime_error("This filesystem is for CryFS " + config.Version() + ". Please update your CryFS version."); + throw CryfsException("This filesystem is for CryFS " + config.Version() + ". Please update your CryFS version.", ErrorCode::TooNewFilesystemFormat); } } if (!allowFilesystemUpgrade && gitversion::VersionCompare::isOlderThan(config.Version(), gitversion::VersionString())) { if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + ". It can be migrated to CryFS " + gitversion::VersionString() + ", but afterwards couldn't be opened anymore with older versions. Do you want to migrate it?", false)) { - throw std::runtime_error("This filesystem is for CryFS " + config.Version() + ". It has to be migrated."); + throw CryfsException("This filesystem is for CryFS " + config.Version() + ". It has to be migrated.", ErrorCode::TooOldFilesystemFormat); } } } void CryConfigLoader::_checkCipher(const CryConfig &config) const { if (_cipherFromCommandLine != none && config.Cipher() != *_cipherFromCommandLine) { - throw std::runtime_error(string() + "Filesystem uses " + config.Cipher() + " cipher and not " + *_cipherFromCommandLine + " as specified."); + throw CryfsException(string() + "Filesystem uses " + config.Cipher() + " cipher and not " + *_cipherFromCommandLine + " as specified.", ErrorCode::WrongCipher); } } diff --git a/test/cryfs-cli/CliTest_ShowingHelp.cpp b/test/cryfs-cli/CliTest_ShowingHelp.cpp index e701d278..a729bce1 100644 --- a/test/cryfs-cli/CliTest_ShowingHelp.cpp +++ b/test/cryfs-cli/CliTest_ShowingHelp.cpp @@ -2,26 +2,28 @@ using CliTest_ShowingHelp = CliTest; +using cryfs::ErrorCode; + TEST_F(CliTest_ShowingHelp, HelpLongOption) { - EXPECT_EXIT_WITH_HELP_MESSAGE({"--help"}); + EXPECT_EXIT_WITH_HELP_MESSAGE({"--help"}, "", ErrorCode::Success); } TEST_F(CliTest_ShowingHelp, HelpLongOptionTogetherWithOtherOptions) { - EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str(), mountdir.c_str(), "--help"}); + EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str(), mountdir.c_str(), "--help"}, "", ErrorCode::Success); } TEST_F(CliTest_ShowingHelp, HelpShortOption) { - EXPECT_EXIT_WITH_HELP_MESSAGE({"-h"}); + EXPECT_EXIT_WITH_HELP_MESSAGE({"-h"}, "", ErrorCode::Success); } TEST_F(CliTest_ShowingHelp, HelpShortOptionTogetherWithOtherOptions) { - EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str(), mountdir.c_str(), "-h"}); + EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str(), mountdir.c_str(), "-h"}, "", ErrorCode::Success); } TEST_F(CliTest_ShowingHelp, MissingAllOptions) { - EXPECT_EXIT_WITH_HELP_MESSAGE({}, "Please specify a base directory"); + EXPECT_EXIT_WITH_HELP_MESSAGE({}, "Please specify a base directory", ErrorCode::InvalidArguments); } TEST_F(CliTest_ShowingHelp, MissingDir) { - EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str()}, "Please specify a mount directory"); + EXPECT_EXIT_WITH_HELP_MESSAGE({basedir.c_str()}, "Please specify a mount directory", ErrorCode::InvalidArguments); } diff --git a/test/cryfs-cli/CliTest_WrongEnvironment.cpp b/test/cryfs-cli/CliTest_WrongEnvironment.cpp index 5b8ea84b..4a0290cf 100644 --- a/test/cryfs-cli/CliTest_WrongEnvironment.cpp +++ b/test/cryfs-cli/CliTest_WrongEnvironment.cpp @@ -7,6 +7,7 @@ using ::testing::Return; using ::testing::_; using std::vector; using cpputils::TempFile; +using cryfs::ErrorCode; struct TestConfig { bool externalConfigfile; @@ -41,10 +42,11 @@ public: EXPECT_RUN_SUCCESS(args(), mountdir); } - void Test_Run_Error(const char *expectedError) { + void Test_Run_Error(const char *expectedError, cryfs::ErrorCode errorCode) { EXPECT_RUN_ERROR( args(), - expectedError + expectedError, + errorCode ); } @@ -85,7 +87,7 @@ TEST_P(CliTest_WrongEnvironment, NoErrorCondition) { TEST_P(CliTest_WrongEnvironment, MountDirIsBaseDir) { mountdir = basedir; - Test_Run_Error("Error: base directory can't be inside the mount directory"); + Test_Run_Error("Error: base directory can't be inside the mount directory", ErrorCode::BaseDirInsideMountDir); } bf::path make_relative(const bf::path &path) { @@ -100,26 +102,26 @@ bf::path make_relative(const bf::path &path) { TEST_P(CliTest_WrongEnvironment, MountDirIsBaseDir_MountDirRelative) { mountdir = make_relative(basedir); - Test_Run_Error("Error: base directory can't be inside the mount directory"); + Test_Run_Error("Error: base directory can't be inside the mount directory", ErrorCode::BaseDirInsideMountDir); } TEST_P(CliTest_WrongEnvironment, MountDirIsBaseDir_BaseDirRelative) { mountdir = basedir; basedir = make_relative(basedir); - Test_Run_Error("Error: base directory can't be inside the mount directory"); + Test_Run_Error("Error: base directory can't be inside the mount directory", ErrorCode::BaseDirInsideMountDir); } TEST_P(CliTest_WrongEnvironment, MountDirIsBaseDir_BothRelative) { basedir = make_relative(basedir); mountdir = basedir; - Test_Run_Error("Error: base directory can't be inside the mount directory"); + Test_Run_Error("Error: base directory can't be inside the mount directory", ErrorCode::BaseDirInsideMountDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist) { _basedir.remove(); // ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents. ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?", _)).WillByDefault(Return(false)); - Test_Run_Error("Error: base directory not found"); + Test_Run_Error("Error: base directory not found", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist_Noninteractive) { @@ -128,7 +130,7 @@ TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist_Noninteractive) { // So we set a default answer that shouldn't crash and check it's not called by checking that it crashes. ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?", _)).WillByDefault(Return(true)); ::setenv("CRYFS_FRONTEND", "noninteractive", 1); - Test_Run_Error("Error: base directory not found"); + Test_Run_Error("Error: base directory not found", ErrorCode::InaccessibleBaseDir); ::unsetenv("CRYFS_FRONTEND"); } @@ -143,7 +145,7 @@ TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist_Create) { TEST_P(CliTest_WrongEnvironment, BaseDir_IsNotDirectory) { TempFile basedirfile; basedir = basedirfile.path(); - Test_Run_Error("Error: base directory is not a directory"); + Test_Run_Error("Error: base directory is not a directory", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_AllPermissions) { @@ -155,29 +157,29 @@ TEST_P(CliTest_WrongEnvironment, BaseDir_AllPermissions) { TEST_P(CliTest_WrongEnvironment, BaseDir_NoReadPermission) { SetNoReadPermission(basedir); - Test_Run_Error("Error: Could not read from base directory"); + Test_Run_Error("Error: Could not read from base directory", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_NoWritePermission) { SetNoWritePermission(basedir); - Test_Run_Error("Error: Could not write to base directory"); + Test_Run_Error("Error: Could not write to base directory", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_NoExePermission) { SetNoExePermission(basedir); - Test_Run_Error("Error: Could not write to base directory"); + Test_Run_Error("Error: Could not write to base directory", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, BaseDir_NoPermission) { SetNoPermission(basedir); - Test_Run_Error("Error: Could not write to base directory"); + Test_Run_Error("Error: Could not write to base directory", ErrorCode::InaccessibleBaseDir); } TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist) { _mountdir.remove(); // ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents. ON_CALL(*console, askYesNo("Could not find mount directory. Do you want to create it?", _)).WillByDefault(Return(false)); - Test_Run_Error("Error: mount directory not found"); + Test_Run_Error("mount directory not found", ErrorCode::InaccessibleMountDir); } TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist_Noninteractive) { @@ -186,7 +188,7 @@ TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist_Noninteractive) { // So we set a default answer that shouldn't crash and check it's not called by checking that it crashes. ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?", _)).WillByDefault(Return(true)); ::setenv("CRYFS_FRONTEND", "noninteractive", 1); - Test_Run_Error("Error: mount directory not found"); + Test_Run_Error("mount directory not found", ErrorCode::InaccessibleMountDir); ::unsetenv("CRYFS_FRONTEND"); } @@ -201,7 +203,7 @@ TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist_Create) { TEST_P(CliTest_WrongEnvironment, MountDir_IsNotDirectory) { TempFile mountdirfile; mountdir = mountdirfile.path(); - Test_Run_Error("Error: mount directory is not a directory"); + Test_Run_Error("Error: mount directory is not a directory", ErrorCode::InaccessibleMountDir); } TEST_P(CliTest_WrongEnvironment, MountDir_AllPermissions) { @@ -213,20 +215,20 @@ TEST_P(CliTest_WrongEnvironment, MountDir_AllPermissions) { TEST_P(CliTest_WrongEnvironment, MountDir_NoReadPermission) { SetNoReadPermission(mountdir); - Test_Run_Error("Error: Could not read from mount directory"); + Test_Run_Error("Error: Could not read from mount directory", ErrorCode::InaccessibleMountDir); } TEST_P(CliTest_WrongEnvironment, MountDir_NoWritePermission) { SetNoWritePermission(mountdir); - Test_Run_Error("Error: Could not write to mount directory"); + Test_Run_Error("Error: Could not write to mount directory", ErrorCode::InaccessibleMountDir); } TEST_P(CliTest_WrongEnvironment, MountDir_NoExePermission) { SetNoExePermission(mountdir); - Test_Run_Error("Error: Could not write to mount directory"); + Test_Run_Error("Error: Could not write to mount directory", ErrorCode::InaccessibleMountDir); } TEST_P(CliTest_WrongEnvironment, MountDir_NoPermission) { SetNoPermission(mountdir); - Test_Run_Error("Error: Could not write to mount directory"); + Test_Run_Error("Error: Could not write to mount directory", ErrorCode::InaccessibleMountDir); } diff --git a/test/cryfs-cli/program_options/ParserTest.cpp b/test/cryfs-cli/program_options/ParserTest.cpp index a02d4cf4..c0707a5c 100644 --- a/test/cryfs-cli/program_options/ParserTest.cpp +++ b/test/cryfs-cli/program_options/ParserTest.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include using namespace cryfs; using namespace cryfs::program_options; @@ -10,6 +12,7 @@ using std::vector; using std::string; using boost::none; namespace bf = boost::filesystem; +using cpputils::CaptureStderrRAII; class ProgramOptionsParserTest: public ProgramOptionsTestBase { public: @@ -20,39 +23,58 @@ public: }; TEST_F(ProgramOptionsParserTest, MissingAllOptions) { - EXPECT_DEATH( - parse({"./myExecutable"}), - "Usage:" - ); + CaptureStderrRAII captureStderr; + try { + parse({"./myExecutable"}); + EXPECT_TRUE(false); // expect throws + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::InvalidArguments, e.errorCode()); + captureStderr.EXPECT_MATCHES("Usage:"); // expect show usage information + } } TEST_F(ProgramOptionsParserTest, MissingDir) { - EXPECT_DEATH( - parse({"./myExecutable", "/home/user/baseDir"}), - "Usage:" - ); + CaptureStderrRAII captureStderr; + try { + parse({"./myExecutable", "/home/user/baseDir"}); + EXPECT_TRUE(false); // expect throw + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::InvalidArguments, e.errorCode()); + captureStderr.EXPECT_MATCHES("Usage:"); // expect show usage information + } } TEST_F(ProgramOptionsParserTest, HelpLongOption) { - EXPECT_DEATH( - parse({"./myExecutable", "--help"}), - "Usage:" - ); + CaptureStderrRAII captureStderr; + try { + parse({"./myExecutable", "--help"}); + EXPECT_TRUE(false); // expect throw + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::Success, e.errorCode()); + captureStderr.EXPECT_MATCHES("Usage:"); // expect show usage information + } } TEST_F(ProgramOptionsParserTest, HelpShortOption) { - EXPECT_DEATH( - parse({"./myExecutable", "-h"}), - "Usage:" - ); + CaptureStderrRAII captureStderr; + try { + parse({"./myExecutable", "-h"}); + EXPECT_TRUE(false); // expect throw + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::Success, e.errorCode()); + captureStderr.EXPECT_MATCHES("Usage:"); // expect show usage information + } } TEST_F(ProgramOptionsParserTest, ShowCiphers) { - EXPECT_EXIT( - parse({"./myExecutable", "--show-ciphers"}), - ::testing::ExitedWithCode(0), - "aes-256-gcm" - ); + CaptureStderrRAII captureStderr; + try { + parse({"./myExecutable", "--show-ciphers"}); + EXPECT_TRUE(false); // expect throw + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::Success, e.errorCode()); + captureStderr.EXPECT_MATCHES("aes-256-gcm"); // expect show ciphers + } } TEST_F(ProgramOptionsParserTest, BaseDir_Absolute) { @@ -136,10 +158,13 @@ TEST_F(ProgramOptionsParserTest, CipherNotGiven) { } TEST_F(ProgramOptionsParserTest, InvalidCipher) { - EXPECT_DEATH( - parse({"./myExecutable", "/home/user/baseDir", "--cipher", "invalid-cipher", "/home/user/mountDir"}), - "Invalid cipher: invalid-cipher" - ); + try { + parse({"./myExecutable", "/home/user/baseDir", "--cipher", "invalid-cipher", "/home/user/mountDir"}); + EXPECT_TRUE(false); // expect throw + } catch (const CryfsException& e) { + EXPECT_EQ(ErrorCode::InvalidArguments, e.errorCode()); + EXPECT_THAT(e.what(), testing::MatchesRegex(".*Invalid cipher: invalid-cipher.*")); + } } TEST_F(ProgramOptionsParserTest, UnmountAfterIdleMinutesGiven) { diff --git a/test/cryfs-cli/testutils/CliTest.h b/test/cryfs-cli/testutils/CliTest.h index 9323ac83..c2517350 100644 --- a/test/cryfs-cli/testutils/CliTest.h +++ b/test/cryfs-cli/testutils/CliTest.h @@ -12,6 +12,8 @@ #include #include #include "../../cryfs/testutils/MockConsole.h" +#include +#include class CliTest : public ::testing::Test { public: @@ -31,7 +33,7 @@ public: return httpClient; } - void run(std::vector args) { + int run(std::vector args) { std::vector _args; _args.reserve(args.size()+1); _args.push_back("cryfs"); @@ -43,19 +45,18 @@ public: std::cin.putback('\n'); std::cin.putback('s'); std::cin.putback('s'); std::cin.putback('a'); std::cin.putback('p'); std::cin.putback('\n'); std::cin.putback('s'); std::cin.putback('s'); std::cin.putback('a'); std::cin.putback('p'); // Run Cryfs - cryfs::Cli(keyGenerator, cpputils::SCrypt::TestSettings, console, _httpClient()).main(_args.size(), _args.data()); + return cryfs::Cli(keyGenerator, cpputils::SCrypt::TestSettings, console, _httpClient()).main(_args.size(), _args.data()); } - void EXPECT_EXIT_WITH_HELP_MESSAGE(std::vector args, const std::string &message = "") { - EXPECT_RUN_ERROR(args, (message+".*Usage").c_str()); + void EXPECT_EXIT_WITH_HELP_MESSAGE(std::vector args, const std::string &message, cryfs::ErrorCode errorCode) { + EXPECT_RUN_ERROR(args, (".*Usage:.*"+message).c_str(), errorCode); } - void EXPECT_RUN_ERROR(std::vector args, const char *message) { - EXPECT_EXIT( - run(args), - ::testing::ExitedWithCode(1), - message - ); + void EXPECT_RUN_ERROR(std::vector args, const char* message, cryfs::ErrorCode errorCode) { + cpputils::CaptureStderrRAII capturedStderr; + int exit_code = run(args); + capturedStderr.EXPECT_MATCHES(string(".*") + message + ".*"); + EXPECT_EQ(exitCode(errorCode), exit_code); } void EXPECT_RUN_SUCCESS(std::vector args, const boost::filesystem::path &mountDir) {