diff --git a/include/loot/exception/error_categories.h b/include/loot/exception/error_categories.h index 3b7d6c41..42773fcc 100644 --- a/include/loot/exception/error_categories.h +++ b/include/loot/exception/error_categories.h @@ -30,6 +30,14 @@ #include "loot/api_decorator.h" namespace loot { +/** + * @brief Get the error category that can be used to identify system_error + * exceptions that are due to esplugin errors. + * @returns A reference to the static object of unspecified runtime type, + * derived from std::error_category. + */ +LOOT_API const std::error_category& esplugin_category(); + /** * @brief Get the error category that can be used to identify system_error * exceptions that are due to libloadorder errors. diff --git a/src/api/error_categories.cpp b/src/api/error_categories.cpp index c29544e8..b4bd9521 100644 --- a/src/api/error_categories.cpp +++ b/src/api/error_categories.cpp @@ -28,6 +28,16 @@ namespace loot { namespace detail { +class esplugin_category : public std::error_category { + const char* name() const noexcept override { return "esplugin"; } + + std::string message(int) const override { return "esplugin error"; } + + bool equivalent(const std::error_code& code, int) const noexcept override { + return code.category().name() == name(); + } +}; + class libloadorder_category : public std::error_category { const char* name() const noexcept override { return "libloadorder"; } @@ -39,6 +49,11 @@ class libloadorder_category : public std::error_category { }; } +LOOT_API const std::error_category& esplugin_category() { + static detail::esplugin_category instance; + return instance; +} + LOOT_API const std::error_category& libloadorder_category() { static detail::libloadorder_category instance; return instance; diff --git a/src/api/plugin.cpp b/src/api/plugin.cpp index 028884ef..b4398e65 100644 --- a/src/api/plugin.cpp +++ b/src/api/plugin.cpp @@ -33,6 +33,7 @@ #include "api/helpers/crc.h" #include "api/helpers/logging.h" #include "api/helpers/text.h" +#include "loot/exception/error_categories.h" #include "loot/exception/file_access_error.h" namespace loot { @@ -201,7 +202,7 @@ void HandleEspluginError(const std::string& operation, logger->error(err); } - throw FileAccessError(err); + throw std::system_error(returnCode, esplugin_category(), err); } Plugin::Plugin(const GameType gameType, @@ -239,6 +240,18 @@ Plugin::Plugin(const GameType gameType, } tags_ = ExtractBashTags(GetDescription()); + } catch (const std::system_error& e) { + if (e.code().category() == esplugin_category()) { + throw; + } + + if (logger) { + logger->error("Cannot read plugin file \"{}\". Details: {}", + pluginPath.u8string(), + e.what()); + } + throw FileAccessError("Cannot read \"" + pluginPath.u8string() + + "\". Details: " + e.what()); } catch (const std::exception& e) { if (logger) { logger->error("Cannot read plugin file \"{}\". Details: {}", diff --git a/src/tests/api/internals/game/game_test.h b/src/tests/api/internals/game/game_test.h index b6019010..5b5a0350 100644 --- a/src/tests/api/internals/game/game_test.h +++ b/src/tests/api/internals/game/game_test.h @@ -26,6 +26,7 @@ along with LOOT. If not, see #define LOOT_TESTS_API_INTERNALS_GAME_GAME_TEST #include "api/game/game.h" +#include "loot/exception/error_categories.h" #include "tests/common_game_test_fixture.h" namespace loot { @@ -363,6 +364,31 @@ TEST_P(GameTest, loadPluginsShouldUseAbsolutePathsAsGiven) { EXPECT_NE(nullptr, game.GetPlugin(blankEsm)); } +TEST_P( + GameTest, + loadPluginsShouldThrowIfFullyLoadingAPluginWithAMissingMasterIfGameIsMorrowindOrStarfield) { + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + const auto pluginName = + GetParam() == GameType::starfield ? blankFullEsm : blankEsm; + + std::filesystem::remove(dataPath / pluginName); + + if (GetParam() == GameType::tes3 || GetParam() == GameType::starfield) { + try { + game.LoadPlugins({blankMasterDependentEsm}, false); + FAIL(); + } catch (const std::system_error& e) { + EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value()); + EXPECT_EQ(esplugin_category(), e.code().category()); + } + } else { + game.LoadPlugins({blankMasterDependentEsm}, false); + + EXPECT_NE(nullptr, game.GetPlugin(blankMasterDependentEsm)); + } +} + TEST_P(GameTest, sortPluginsShouldHandlePluginPathsThatAreNotJustFilenames) { Game game = Game(GetParam(), dataPath.parent_path(), localPath); diff --git a/src/tests/api/internals/plugin_test.h b/src/tests/api/internals/plugin_test.h index 3fcd441f..dbf3e649 100644 --- a/src/tests/api/internals/plugin_test.h +++ b/src/tests/api/internals/plugin_test.h @@ -27,6 +27,7 @@ along with LOOT. If not, see #include "api/game/game.h" #include "api/plugin.h" +#include "loot/exception/error_categories.h" #include "tests/common_game_test_fixture.h" namespace loot { @@ -337,8 +338,7 @@ TEST_P( plugin3.IsLightPlugin()); } -TEST_P( - PluginTest, +TEST_P(PluginTest, isMediumPluginShouldBeTrueForAMediumFlaggedPluginForStarfield) { if (GetParam() != GameType::starfield) { auto bytes = ReadFile(dataPath / blankEsm); @@ -348,14 +348,13 @@ TEST_P( const auto pluginName = GetParam() == GameType::starfield ? blankMediumEsm : blankEsm; - Plugin plugin(game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, - true); + Plugin plugin( + game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, true); EXPECT_EQ(GetParam() == GameType::starfield, plugin.IsMediumPlugin()); } -TEST_P(PluginTest, - isUpdatePluginShouldOnlyBeTrueForAStarfieldUpdatePlugin) { +TEST_P(PluginTest, isUpdatePluginShouldOnlyBeTrueForAStarfieldUpdatePlugin) { auto bytes = ReadFile(dataPath / blankMasterDependentEsp); bytes[9] = 0x2; WriteFile(dataPath / blankMasterDependentEsp, bytes); @@ -385,11 +384,16 @@ TEST_P(PluginTest, loadingAPluginWithMastersShouldReadThemCorrectly) { } TEST_P(PluginTest, loadingAPluginThatDoesNotExistShouldThrow) { - EXPECT_THROW(Plugin(game_.GetType(), - game_.GetCache(), - game_.DataPath() / "Blank\\.esp", - true), - FileAccessError); + try { + Plugin(game_.GetType(), + game_.GetCache(), + game_.DataPath() / "Blank\\.esp", + true); + FAIL(); + } catch (const std::system_error& e) { + EXPECT_EQ(ESP_ERROR_FILE_NOT_FOUND, e.code().value()); + EXPECT_EQ(esplugin_category(), e.code().category()); + } } TEST_P( @@ -545,8 +549,7 @@ TEST_P( PluginTest, isValidAsMediumPluginShouldReturnTrueOnlyForAStarfieldPluginWithNewFormIdsBetween0And0xFFFFInclusive) { bool valid = - Plugin( - game_.GetType(), game_.GetCache(), dataPath / blankEsm, true) + Plugin(game_.GetType(), game_.GetCache(), dataPath / blankEsm, true) .IsValidAsMediumPlugin(); if (GetParam() == GameType::starfield) { EXPECT_TRUE(valid); @@ -558,12 +561,11 @@ TEST_P( TEST_P( PluginTest, IsValidAsUpdatePluginShouldOnlyReturnTrueForAStarfieldPluginWithNoNewRecords) { - const auto sourcePluginName = GetParam() == GameType::starfield - ? blankFullEsm - : blankEsp; + const auto sourcePluginName = + GetParam() == GameType::starfield ? blankFullEsm : blankEsp; const auto updatePluginName = GetParam() == GameType::starfield - ? blankMasterDependentEsp - : blankDifferentPluginDependentEsp; + ? blankMasterDependentEsp + : blankDifferentPluginDependentEsp; Plugin plugin1(game_.GetType(), game_.GetCache(), @@ -583,8 +585,7 @@ TEST_P( } EXPECT_FALSE(plugin1.IsValidAsUpdatePlugin()); - EXPECT_EQ(GetParam() == GameType::starfield, - plugin2.IsValidAsUpdatePlugin()); + EXPECT_EQ(GetParam() == GameType::starfield, plugin2.IsValidAsUpdatePlugin()); } TEST_P(PluginTest,