From 327d9935c92b4af1011fedd263d269d8824d53ee Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Fri, 14 Mar 2025 17:05:34 +0000 Subject: [PATCH] Map C++ exception types --- Cargo.lock | 3 + README.md | 4 +- cxx/CMakeLists.txt | 2 + cxx/Cargo.toml | 4 + cxx/README.md | 54 ++------- cxx/src/api/convert.cpp | 112 +++++++++++------- cxx/src/api/database.cpp | 122 ++++++++++++++------ cxx/src/api/exception.cpp | 177 ++++++++++++++++++++++++++++ cxx/src/api/exception.h | 10 ++ cxx/src/api/game.cpp | 105 ++++++++++++----- cxx/src/api/plugin.cpp | 27 ++++- cxx/src/error_codes.rs | 236 ++++++++++++++++++++++++++++++++++++++ cxx/src/lib.rs | 214 +++++++++++++++++++++++++++++++--- 13 files changed, 905 insertions(+), 165 deletions(-) create mode 100644 cxx/src/api/exception.cpp create mode 100644 cxx/src/api/exception.h create mode 100644 cxx/src/error_codes.rs diff --git a/Cargo.lock b/Cargo.lock index 3545f348..5bea9336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,7 +588,10 @@ dependencies = [ "cxx", "cxx-build", "delegate", + "esplugin", + "libloadorder", "libloot", + "loot-condition-interpreter", "unicase", ] diff --git a/README.md b/README.md index f60c3622..a511ae5b 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Currently complete: - [x] Metadata-related functionality (excluding writing YAML) - [x] Sorting functionality - [x] Unit tests -- [ ] Integration tests -- [ ] C++ FFI +- [x] Integration tests +- [x] C++ FFI - [ ] Python FFI The complete bits should match libloot commit [55b341fc6cbdccee52e42923c13a91eddb5ca97d](https://github.com/loot/libloot/commit/55b341fc6cbdccee52e42923c13a91eddb5ca97d), which is libloot v0.25.3 plus a few changes prompted by this translation. diff --git a/cxx/CMakeLists.txt b/cxx/CMakeLists.txt index 78e4576c..e1c6de29 100644 --- a/cxx/CMakeLists.txt +++ b/cxx/CMakeLists.txt @@ -41,6 +41,7 @@ set(LIBLOOT_SRC_API_CPP_FILES "${CMAKE_SOURCE_DIR}/src/api/convert.cpp" "${CMAKE_SOURCE_DIR}/src/api/database.cpp" "${CMAKE_SOURCE_DIR}/src/api/error_categories.cpp" + "${CMAKE_SOURCE_DIR}/src/api/exception.cpp" "${CMAKE_SOURCE_DIR}/src/api/metadata/conditional_metadata.cpp" "${CMAKE_SOURCE_DIR}/src/api/metadata/file.cpp" "${CMAKE_SOURCE_DIR}/src/api/metadata/filename.cpp" @@ -89,6 +90,7 @@ set(LIBLOOT_INCLUDE_H_FILES set(LIBLOOT_SRC_API_H_FILES "${CMAKE_SOURCE_DIR}/src/api/convert.h" "${CMAKE_SOURCE_DIR}/src/api/database.h" + "${CMAKE_SOURCE_DIR}/src/api/exception.h" "${CMAKE_SOURCE_DIR}/src/api/game.h" "${CMAKE_SOURCE_DIR}/src/api/plugin.h" ) diff --git a/cxx/Cargo.toml b/cxx/Cargo.toml index 82e5480a..0a81b884 100644 --- a/cxx/Cargo.toml +++ b/cxx/Cargo.toml @@ -10,6 +10,10 @@ delegate = "0.13.2" libloot = { path = ".." } unicase = "2.8.1" +esplugin = "6.1.1" +libloadorder = { git = "https://github.com/Ortham/libloadorder.git", rev = "6245dbc6824c4751daef5ab01dfe7b9497f5446f" } +loot-condition-interpreter = { git = "https://github.com/loot/loot-condition-interpreter.git", rev = "f7d9947fa434037743ce81e2d409184ec0bfb693" } + [build-dependencies] cxx-build = "1.0" diff --git a/cxx/README.md b/cxx/README.md index 5dadc604..494e016c 100644 --- a/cxx/README.md +++ b/cxx/README.md @@ -1,49 +1,10 @@ # libloot-rs C++ wrapper -This is an **imcomplete** and **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface that's ABI-compatible with libloot v0.25.3. - -## Current status - -- [x] `EdgeType` -- [x] `GameType` -- [x] `LogLevel` -- [x] `MessageType` -- [ ] `ConditionSyntaxError` -- [ ] `CyclicInteractionError` -- [ ] `FileAccessError` -- [ ] `UndefinedGroupError` -- [ ] `std::system_error` with esplugin `std::error_category` -- [ ] `std::system_error` with libloadorder `std::error_category` -- [ ] `std::system_error` with loot-condition-interpreter `std::error_category` -- [x] `ConditionalMetadata` -- [x] `File` -- [x] `Filename` -- [x] `Group` -- [x] `Location` -- [x] `MessageContent` -- [x] `SelectMessageContent` -- [x] `Message` -- [x] `PluginCleaningData` -- [x] `PluginMetadata` -- [x] `Tag` -- [x] `SetLoggingCallback` -- [x] `IsCompatible` -- [x] `CreateGameHandle` -- [x] `DatabaseInterface` -- [x] `GameInterface` -- [x] `LIBLOOT_VERSION_MAJOR` -- [x] `LIBLOOT_VERSION_MINOR` -- [x] `LIBLOOT_VERSION_PATCH` -- [x] `GetLiblootVersion` -- [x] `GetLiblootRevision` -- [x] `PluginInterface` -- [x] `Vertex` - -The error types are defined but currently unused, causing behavioural differences when errors occur - see the implementation notes below for details. +This is an **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface that's ABI-compatible with libloot v0.25.3. ## Building -The wrapper is currently built in two layers: +The wrapper has two layers: - a static library built using Cargo, which provides a C++ interface - a shared library built using CMake, which wraps that C++ interface to provide another that is ABI-compatible with C++ libloot. @@ -62,8 +23,6 @@ This also builds a copy of the public API tests from C++ libloot v0.25.3, which ctest --test-dir build --output-on-failure -V ``` -Some of the tests currently fail: they are all expecting the exception classes that aren't yet used. - To package the build: ``` @@ -74,9 +33,16 @@ This repository (and so the created package) doesn't currently include any of li ## Usage notes +For the first layer of the wrapper, built using Cargo: + - `LIBLOOT_VERSION_MAJOR`, `LIBLOOT_VERSION_MINOR` and `LIBLOOT_VERSION_PATCH` are exposed as `extern "C"` `static unsigned int` globals. - CXX doesn't provide integration between Rust's `Option<_>` and C++'s `std::optional`, so sentinel values are used to communicate the absence of a value in the wrapper's API: - For `Option<&str>`, `None` is represented using an empty string, which is how it's already done for many metadata object fields in the C++ implementation of libloot. - For `Option`, `None` is represented using `NaN`, since the C++ implementation of libloot's public API already says that `NaN` values get converted to `None`. - Plugin CRCs (which are `Option` in Rust) are represented as `int64_t`, with `-1` representing `None`, since all `uint32_t` values are possibly valid CRCs. -- All Rust errors are converted to `::rust::Error` exceptions that have a `what()` string that is the concatenation of the error's display string and all its recursive source error display strings. +- All Rust errors are converted to `::rust::Error` exceptions that have a `what()` string that is the concatenation of the error's display string and all its recursive source error display strings. For some errors the `what()` string also includes some data that is parsed by the wrapper's second layer to differentiate certain error types. + +For the ABI-compatible second layer of the wrapper, built using CMake: + +- Some exceptions have changed type: they all still derive from `std::exception`, but for example some `std::logic_error` and `std::invalid_argument` exceptions have become `std::runtime_error` and `FileAccessError` exceptions, some `YAML::RepresentationException` exceptions have become `FileAccessError` exceptions, etc. +- Exception messages are generally not expected to be the same between the two implementations. diff --git a/cxx/src/api/convert.cpp b/cxx/src/api/convert.cpp index a2480ec3..08405f27 100644 --- a/cxx/src/api/convert.cpp +++ b/cxx/src/api/convert.cpp @@ -1,5 +1,7 @@ #include "api/convert.h" +#include "api/exception.h" + namespace loot { // To public types ///////////////////// @@ -118,11 +120,15 @@ std::optional convert(uint8_t edgeType) { } loot::Vertex convert(const loot::rust::Vertex& vertex) { - const auto outEdgeType = convert(vertex.out_edge_type()); - if (outEdgeType.has_value()) { - return loot::Vertex(std::string(vertex.name()), outEdgeType.value()); - } else { - return loot::Vertex(std::string(vertex.name())); + try { + const auto outEdgeType = convert(vertex.out_edge_type()); + if (outEdgeType.has_value()) { + return loot::Vertex(std::string(vertex.name()), outEdgeType.value()); + } else { + return loot::Vertex(std::string(vertex.name())); + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } } @@ -140,8 +146,14 @@ loot::Vertex convert(const loot::rust::Vertex& vertex) { ::rust::Box convert(const loot::File& file) { auto output = loot::rust::new_file(std::string(file.GetName())); output->set_display_name(file.GetDisplayName()); - output->set_detail( - ::rust::Slice(convert(file.GetDetail()))); + + try { + output->set_detail( + ::rust::Slice(convert(file.GetDetail()))); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } + output->set_condition(file.GetCondition()); return output; @@ -169,29 +181,44 @@ loot::rust::MessageType convert(loot::MessageType messageType) { } ::rust::Box convert(const loot::Message& message) { - auto output = loot::rust::multilingual_message( - convert(message.GetType()), - ::rust::Slice(convert(message.GetContent()))); - output->set_condition(message.GetCondition()); + try { + auto output = loot::rust::multilingual_message( + convert(message.GetType()), + ::rust::Slice( + convert(message.GetContent()))); + output->set_condition(message.GetCondition()); - return output; + return output; + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } ::rust::Box convert(const loot::Tag& tag) { - const auto suggestion = tag.IsAddition() ? loot::rust::TagSuggestion::Addition - : loot::rust::TagSuggestion::Removal; - auto output = loot::rust::new_tag(tag.GetName(), suggestion); - output->set_condition(tag.GetCondition()); + try { + const auto suggestion = tag.IsAddition() + ? loot::rust::TagSuggestion::Addition + : loot::rust::TagSuggestion::Removal; + auto output = loot::rust::new_tag(tag.GetName(), suggestion); + output->set_condition(tag.GetCondition()); - return output; + return output; + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } ::rust::Box convert( const loot::PluginCleaningData& data) { auto output = loot::rust::new_plugin_cleaning_data(data.GetCRC(), data.GetCleaningUtility()); - output->set_detail( - ::rust::Slice(convert(data.GetDetail()))); + try { + output->set_detail( + ::rust::Slice(convert(data.GetDetail()))); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } + output->set_itm_count(data.GetITMCount()); output->set_deleted_reference_count(data.GetDeletedReferenceCount()); output->set_deleted_navmesh_count(data.GetDeletedNavmeshCount()); @@ -208,29 +235,34 @@ loot::rust::MessageType convert(loot::MessageType messageType) { ::rust::Box convert( const loot::PluginMetadata& metadata) { - auto output = loot::rust::new_plugin_metadata(metadata.GetName()); + try { + auto output = loot::rust::new_plugin_metadata(metadata.GetName()); - if (metadata.GetGroup().has_value()) { - output->set_group(metadata.GetGroup().value()); + if (metadata.GetGroup().has_value()) { + output->set_group(metadata.GetGroup().value()); + } + + output->set_load_after_files( + ::rust::Slice(convert(metadata.GetLoadAfterFiles()))); + output->set_requirements( + ::rust::Slice(convert(metadata.GetRequirements()))); + output->set_incompatibilities(::rust::Slice( + convert(metadata.GetIncompatibilities()))); + output->set_messages( + ::rust::Slice(convert(metadata.GetMessages()))); + output->set_tags( + ::rust::Slice(convert(metadata.GetTags()))); + output->set_dirty_info(::rust::Slice( + convert(metadata.GetDirtyInfo()))); + output->set_clean_info(::rust::Slice( + convert(metadata.GetCleanInfo()))); + output->set_locations( + ::rust::Slice(convert(metadata.GetLocations()))); + + return output; + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } - - output->set_load_after_files( - ::rust::Slice(convert(metadata.GetLoadAfterFiles()))); - output->set_requirements( - ::rust::Slice(convert(metadata.GetRequirements()))); - output->set_incompatibilities(::rust::Slice( - convert(metadata.GetIncompatibilities()))); - output->set_messages( - ::rust::Slice(convert(metadata.GetMessages()))); - output->set_tags( - ::rust::Slice(convert(metadata.GetTags()))); - output->set_dirty_info( - ::rust::Slice(convert(metadata.GetDirtyInfo()))); - output->set_clean_info( - ::rust::Slice(convert(metadata.GetCleanInfo()))); - output->set_locations(::rust::Slice(convert(metadata.GetLocations()))); - - return output; } // Between containers diff --git a/cxx/src/api/database.cpp b/cxx/src/api/database.cpp index 2150596f..0bdb68e0 100644 --- a/cxx/src/api/database.cpp +++ b/cxx/src/api/database.cpp @@ -2,6 +2,7 @@ #include "api/database.h" #include "api/convert.h" +#include "api/exception.h" namespace loot { Database::Database(::rust::Box&& database) : @@ -10,91 +11,148 @@ Database::Database(::rust::Box&& database) : void Database::LoadLists(const std::filesystem::path& masterlistPath, const std::filesystem::path& userlistPath, const std::filesystem::path& masterlistPreludePath) { - if (!masterlistPath.empty()) { - if (!masterlistPreludePath.empty()) { - database_->load_masterlist_with_prelude(masterlistPath.u8string(), - masterlistPreludePath.u8string()); - } else { - database_->load_masterlist(masterlistPath.u8string()); + try { + if (!masterlistPath.empty()) { + if (!masterlistPreludePath.empty()) { + database_->load_masterlist_with_prelude( + masterlistPath.u8string(), masterlistPreludePath.u8string()); + } else { + database_->load_masterlist(masterlistPath.u8string()); + } } - } - if (!userlistPath.empty()) { - database_->load_userlist(userlistPath.u8string()); + if (!userlistPath.empty()) { + database_->load_userlist(userlistPath.u8string()); + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } } void Database::WriteUserMetadata(const std::filesystem::path& outputFile, const bool overwrite) const { - database_->write_user_metadata(outputFile.u8string(), overwrite); + try { + database_->write_user_metadata(outputFile.u8string(), overwrite); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Database::GetKnownBashTags() const { - return convert(database_->known_bash_tags()); + try { + return convert(database_->known_bash_tags()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Database::GetGeneralMessages( bool evaluateConditions) const { - return convert(database_->general_messages(evaluateConditions)); + try { + return convert(database_->general_messages(evaluateConditions)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Database::GetGroups(bool includeUserMetadata) const { - return convert(database_->groups(includeUserMetadata)); + try { + return convert(database_->groups(includeUserMetadata)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Database::GetUserGroups() const { - return convert(database_->user_groups()); + try { + return convert(database_->user_groups()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Database::SetUserGroups(const std::vector& groups) { - database_->set_user_groups(::rust::Slice(convert(groups))); + try { + database_->set_user_groups( + ::rust::Slice(convert(groups))); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Database::GetGroupsPath( const std::string& fromGroupName, const std::string& toGroupName) const { - return convert(database_->groups_path(fromGroupName, toGroupName)); + try { + return convert(database_->groups_path(fromGroupName, toGroupName)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::optional Database::GetPluginMetadata( const std::string& plugin, bool includeUserMetadata, bool evaluateConditions) const { - const auto metadata = database_->plugin_metadata( - plugin, includeUserMetadata, evaluateConditions); - if (metadata->is_some()) { - return convert(metadata->as_ref()); - } else { - return std::nullopt; + try { + const auto metadata = database_->plugin_metadata( + plugin, includeUserMetadata, evaluateConditions); + if (metadata->is_some()) { + return convert(metadata->as_ref()); + } else { + return std::nullopt; + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } } std::optional Database::GetPluginUserMetadata( const std::string& plugin, bool evaluateConditions) const { - const auto metadata = - database_->plugin_user_metadata(plugin, evaluateConditions); - if (metadata->is_some()) { - return convert(metadata->as_ref()); - } else { - return std::nullopt; + try { + const auto metadata = + database_->plugin_user_metadata(plugin, evaluateConditions); + if (metadata->is_some()) { + return convert(metadata->as_ref()); + } else { + return std::nullopt; + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } } void Database::SetPluginUserMetadata(const PluginMetadata& pluginMetadata) { - database_->set_plugin_user_metadata(convert(pluginMetadata)); + try { + database_->set_plugin_user_metadata(convert(pluginMetadata)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Database::DiscardPluginUserMetadata(const std::string& plugin) { - database_->discard_plugin_user_metadata(plugin); + try { + database_->discard_plugin_user_metadata(plugin); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Database::DiscardAllUserMetadata() { - database_->discard_all_user_metadata(); + try { + database_->discard_all_user_metadata(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Database::WriteMinimalList(const std::filesystem::path& outputFile, const bool overwrite) const { - database_->write_minimal_list(outputFile.u8string(), overwrite); + try { + database_->write_minimal_list(outputFile.u8string(), overwrite); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } } diff --git a/cxx/src/api/exception.cpp b/cxx/src/api/exception.cpp new file mode 100644 index 00000000..acec793c --- /dev/null +++ b/cxx/src/api/exception.cpp @@ -0,0 +1,177 @@ +#include "api/exception.h" + +#include + +#include "loot/exception/condition_syntax_error.h" +#include "loot/exception/cyclic_interaction_error.h" +#include "loot/exception/error_categories.h" +#include "loot/exception/file_access_error.h" +#include "loot/exception/undefined_group_error.h" +#include "loot/vertex.h" + +namespace { +using loot::EdgeType; +using loot::Vertex; + +constexpr std::string_view CYCLIC_ERROR_PREFIX = "CyclicInteractionError: "; +constexpr std::string_view UNDEFINED_GROUP_ERROR_PREFIX = + "UndefinedGroupError: "; +constexpr std::string_view ESPLUGIN_ERROR_PREFIX = "EspluginError: "; +constexpr std::string_view LIBLOADORDER_ERROR_PREFIX = "LibloadorderError: "; +constexpr std::string_view LCI_ERROR_PREFIX = "LciError: "; +constexpr std::string_view FILE_ACCESS_ERROR_PREFIX = "FileAccessError: "; +constexpr std::string_view INVALID_ARGUMENT_PREFIX = "InvalidArgument: "; + +bool startsWith(const std::string_view& str, const std::string_view& prefix) { + if (str.size() < prefix.size()) { + return false; + } + + return str.substr(0, prefix.size()) == prefix; +} + +std::string replace(const std::string_view& str, + const std::string_view& from, + const std::string_view& to) { + std::string out; + out.reserve(str.size()); + + size_t i = 0; + while (i < str.size()) { + if (i + from.size() <= str.size() && str.substr(i, from.size()) == from) { + out.append(to); + i += from.size(); + } else { + out.push_back(str[i]); + i += 1; + } + } + + return out; +} + +EdgeType toEdgeType(const std::string_view& edgeTypeDisplay) { + if (edgeTypeDisplay == "Hardcoded") { + return EdgeType::hardcoded; + } else if (edgeTypeDisplay == "Master Flag") { + return EdgeType::masterFlag; + } else if (edgeTypeDisplay == "Master") { + return EdgeType::master; + } else if (edgeTypeDisplay == "Masterlist Requirement") { + return EdgeType::masterlistRequirement; + } else if (edgeTypeDisplay == "User Requirement") { + return EdgeType::userRequirement; + } else if (edgeTypeDisplay == "Masterlist Load After") { + return EdgeType::masterlistLoadAfter; + } else if (edgeTypeDisplay == "User Load After") { + return EdgeType::userLoadAfter; + } else if (edgeTypeDisplay == "Masterlist Group") { + return EdgeType::masterlistGroup; + } else if (edgeTypeDisplay == "User Group") { + return EdgeType::userGroup; + } else if (edgeTypeDisplay == "Record Overlap") { + return EdgeType::recordOverlap; + } else if (edgeTypeDisplay == "Asset Overlap") { + return EdgeType::assetOverlap; + } else if (edgeTypeDisplay == "Tie Break") { + return EdgeType::tieBreak; + } else if (edgeTypeDisplay == "Blueprint Master") { + return EdgeType::blueprintMaster; + } else { + std::string what("Unrecognised edge type: "); + what += edgeTypeDisplay; + throw std::logic_error(what); + } +} + +std::vector parseCyclicError(const std::string_view& what) { + const auto suffix = what.substr(0, CYCLIC_ERROR_PREFIX.size()); + + std::vector vertices; + size_t pos = 0; + while (pos < suffix.size()) { + const auto sepPos = suffix.find("--", pos); + const auto escapedName = suffix.substr(pos, sepPos); + const auto name = replace(replace(escapedName, "\\-", "-"), "\\\\", "\\"); + + if (sepPos != std::string::npos) { + const auto secondSepPos = suffix.find("--", sepPos + 2); + const auto escapedEdgeName = + suffix.substr(sepPos + 2, secondSepPos - (sepPos + 2)); + + vertices.push_back(Vertex(name, toEdgeType(escapedEdgeName))); + + pos = secondSepPos + 2; + } else { + vertices.push_back(Vertex(name)); + pos = suffix.size(); + } + } + + return vertices; +} + +std::string getErrorSuffix(const std::string_view& what) { + const auto sepPos = what.find(": "); + + return std::string(what.substr(sepPos + 2)); +} + +std::pair parseSystemError( + const std::string_view& whatSuffix) { + const auto sepPos = whatSuffix.find(": "); + int code; + const auto result = + std::from_chars(whatSuffix.data(), whatSuffix.data() + sepPos, code); + if (result.ec != std::errc{}) { + std::string err = "Could not parse error code from string: "; + err += whatSuffix; + throw std::runtime_error(err); + } + + return std::make_pair(code, std::string(whatSuffix.substr(sepPos + 2))); +} +} + +namespace loot { +std::exception_ptr mapError(const ::rust::Error& error) { + if (startsWith(error.what(), CYCLIC_ERROR_PREFIX)) { + return std::make_exception_ptr( + CyclicInteractionError(parseCyclicError(error.what()))); + } else if (startsWith(error.what(), UNDEFINED_GROUP_ERROR_PREFIX)) { + return std::make_exception_ptr( + UndefinedGroupError(getErrorSuffix(error.what()))); + } else if (startsWith(error.what(), ESPLUGIN_ERROR_PREFIX)) { + const auto [code, details] = parseSystemError( + std::string_view(error.what()).substr(0, ESPLUGIN_ERROR_PREFIX.size())); + + return std::make_exception_ptr( + std::system_error(code, esplugin_category(), details)); + + } else if (startsWith(error.what(), LIBLOADORDER_ERROR_PREFIX)) { + const auto [code, details] = + parseSystemError(std::string_view(error.what()) + .substr(0, LIBLOADORDER_ERROR_PREFIX.size())); + + return std::make_exception_ptr( + std::system_error(code, libloadorder_category(), details)); + } else if (startsWith(error.what(), LCI_ERROR_PREFIX)) { + const auto [code, details] = parseSystemError( + std::string_view(error.what()).substr(0, LCI_ERROR_PREFIX.size())); + + return std::make_exception_ptr(ConditionSyntaxError( + code, loot_condition_interpreter_category(), details)); + } else if (startsWith(error.what(), FILE_ACCESS_ERROR_PREFIX)) { + return std::make_exception_ptr( + FileAccessError(getErrorSuffix(error.what()))); + } else if (startsWith(error.what(), FILE_ACCESS_ERROR_PREFIX)) { + return std::make_exception_ptr( + FileAccessError(getErrorSuffix(error.what()))); + } else if (startsWith(error.what(), INVALID_ARGUMENT_PREFIX)) { + return std::make_exception_ptr( + std::invalid_argument(getErrorSuffix(error.what()))); + } else { + return std::make_exception_ptr(std::runtime_error(error.what())); + } +} +} diff --git a/cxx/src/api/exception.h b/cxx/src/api/exception.h new file mode 100644 index 00000000..2f3fa90e --- /dev/null +++ b/cxx/src/api/exception.h @@ -0,0 +1,10 @@ +#ifndef LOOT_API_EXCEPTION +#define LOOT_API_EXCEPTION + +#include "rust/cxx.h" + +namespace loot { +std::exception_ptr mapError(const ::rust::Error& error); +} + +#endif diff --git a/cxx/src/api/game.cpp b/cxx/src/api/game.cpp index 52de97bc..f09079b4 100644 --- a/cxx/src/api/game.cpp +++ b/cxx/src/api/game.cpp @@ -2,6 +2,7 @@ #include "api/game.h" #include "api/convert.h" +#include "api/exception.h" namespace { loot::GameType convert(loot::rust::GameType gameType) { @@ -66,12 +67,16 @@ rust::Box constructGame( const loot::GameType gameType, const std::filesystem::path& gamePath, const std::filesystem::path& localDataPath) { - if (localDataPath.empty()) { - return std::move( - loot::rust::new_game(convert(gameType), gamePath.u8string())); - } else { - return std::move(loot::rust::new_game_with_local_path( - convert(gameType), gamePath.u8string(), localDataPath.u8string())); + try { + if (localDataPath.empty()) { + return std::move( + loot::rust::new_game(convert(gameType), gamePath.u8string())); + } else { + return std::move(loot::rust::new_game_with_local_path( + convert(gameType), gamePath.u8string(), localDataPath.u8string())); + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(loot::mapError(e)); } } @@ -92,19 +97,30 @@ Game::Game(const GameType gameType, game_(constructGame(gameType, gamePath, localDataPath)), database_(game_->database()) {} -GameType Game::GetType() const { return ::convert(game_->game_type()); } +GameType Game::GetType() const { + try { + return ::convert(game_->game_type()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } +} const DatabaseInterface& Game::GetDatabase() const { return database_; } DatabaseInterface& Game::GetDatabase() { return database_; } std::vector Game::GetAdditionalDataPaths() const { - std::vector paths; - for (const auto& path_str : game_->additional_data_paths()) { - paths.push_back(std::filesystem::u8path(path_str.begin(), path_str.end())); - } + try { + std::vector paths; + for (const auto& path_str : game_->additional_data_paths()) { + paths.push_back( + std::filesystem::u8path(path_str.begin(), path_str.end())); + } - return paths; + return paths; + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Game::SetAdditionalDataPaths( @@ -115,8 +131,12 @@ void Game::SetAdditionalDataPaths( path_strings.push_back(path.u8string()); path_strs.push_back(path_strings.back()); } - - game_->set_additional_data_paths(::rust::Slice(path_strs)); + try { + game_->set_additional_data_paths( + ::rust::Slice(path_strs)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Game::IsValidPlugin(const std::filesystem::path& pluginPath) const { @@ -132,10 +152,14 @@ void Game::LoadPlugins(const std::vector& pluginPaths, path_strs.push_back(path_strings.back()); } - if (loadHeadersOnly) { - game_->load_plugin_headers(::rust::Slice(path_strs)); - } else { - game_->load_plugins(::rust::Slice(path_strs)); + try { + if (loadHeadersOnly) { + game_->load_plugin_headers(::rust::Slice(path_strs)); + } else { + game_->load_plugins(::rust::Slice(path_strs)); + } + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } for (const auto& path : pluginPaths) { @@ -164,10 +188,14 @@ const PluginInterface* Game::GetPlugin(const std::string& pluginName) const { return it->second.get(); } - auto plugin = std::make_shared(std::move(pluginOpt->as_ref())); - const auto result = plugins_.emplace(key, plugin); + try { + auto plugin = std::make_shared(std::move(pluginOpt->as_ref())); + const auto result = plugins_.emplace(key, plugin); - return result.first->second.get(); + return result.first->second.get(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Game::GetLoadedPlugins() const { @@ -188,23 +216,38 @@ std::vector Game::SortPlugins( const std::vector& pluginFilenames) { const auto strs = as_str_refs(pluginFilenames); - const auto results = - game_->sort_plugins(::rust::Slice(strs)); + try { + const auto results = game_->sort_plugins(::rust::Slice(strs)); - return convert(results); + return convert(results); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } void Game::LoadCurrentLoadOrderState() { - game_->load_current_load_order_state(); + try { + game_->load_current_load_order_state(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Game::IsLoadOrderAmbiguous() const { - return game_->is_load_order_ambiguous(); + try { + return game_->is_load_order_ambiguous(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::filesystem::path Game::GetActivePluginsFilePath() const { - const auto path_string = game_->active_plugins_file_path(); - return std::filesystem::u8path(path_string.begin(), path_string.end()); + try { + const auto path_string = game_->active_plugins_file_path(); + return std::filesystem::u8path(path_string.begin(), path_string.end()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Game::IsPluginActive(const std::string& pluginName) const { @@ -218,6 +261,10 @@ std::vector Game::GetLoadOrder() const { void Game::SetLoadOrder(const std::vector& loadOrder) { const auto strs = as_str_refs(loadOrder); - game_->set_load_order(::rust::Slice(strs)); + try { + game_->set_load_order(::rust::Slice(strs)); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } } diff --git a/cxx/src/api/plugin.cpp b/cxx/src/api/plugin.cpp index 5e9e17d4..ddf6c208 100644 --- a/cxx/src/api/plugin.cpp +++ b/cxx/src/api/plugin.cpp @@ -4,6 +4,7 @@ #include #include "api/convert.h" +#include "api/exception.h" namespace loot { Plugin::Plugin(::rust::Box plugin) : @@ -30,7 +31,11 @@ std::optional Plugin::GetVersion() const { } std::vector Plugin::GetMasters() const { - return convert(plugin_->masters()); + try { + return convert(plugin_->masters()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } std::vector Plugin::GetBashTags() const { @@ -64,15 +69,27 @@ bool Plugin::IsBlueprintPlugin() const { } bool Plugin::IsValidAsLightPlugin() const { - return plugin_->is_valid_as_light_plugin(); + try { + return plugin_->is_valid_as_light_plugin(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Plugin::IsValidAsMediumPlugin() const { - return plugin_->is_valid_as_medium_plugin(); + try { + return plugin_->is_valid_as_medium_plugin(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Plugin::IsValidAsUpdatePlugin() const { - return plugin_->is_valid_as_update_plugin(); + try { + return plugin_->is_valid_as_update_plugin(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } } bool Plugin::IsEmpty() const { return plugin_->is_empty(); } @@ -86,6 +103,8 @@ bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const { return plugin_->do_records_overlap(*otherPlugin.plugin_); } catch (std::bad_cast&) { throw std::invalid_argument("Tried to check if records overlapped with a different concrete type implementing PluginInterface"); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); } } } diff --git a/cxx/src/error_codes.rs b/cxx/src/error_codes.rs new file mode 100644 index 00000000..170b0137 --- /dev/null +++ b/cxx/src/error_codes.rs @@ -0,0 +1,236 @@ +pub(crate) mod lci { + use loot_condition_interpreter::Error; + use std::ffi::c_int; + + /// Invalid arguments were given for the function. + #[unsafe(no_mangle)] + pub static LCI_ERROR_INVALID_ARGS: c_int = -1; + + /// Something went wrong while parsing the condition expression. + #[unsafe(no_mangle)] + pub static LCI_ERROR_PARSING_ERROR: c_int = -2; + + /// Something went wrong while getting the version of an executable. + #[unsafe(no_mangle)] + pub static LCI_ERROR_PE_PARSING_ERROR: c_int = -3; + + /// Some sort of I/O error occurred. + #[unsafe(no_mangle)] + pub static LCI_ERROR_IO_ERROR: c_int = -4; + + /// Something panicked. + #[unsafe(no_mangle)] + pub static LCI_ERROR_PANICKED: c_int = -5; + + /// A thread lock was poisoned. + #[unsafe(no_mangle)] + pub static LCI_ERROR_POISONED_THREAD_LOCK: c_int = -6; + + /// Failed to encode string as a C string, e.g. because there was a nul present. + #[unsafe(no_mangle)] + pub static LCI_ERROR_TEXT_ENCODE_FAIL: c_int = -7; + + /// The library encountered an error that should not have been possible to encounter. + #[unsafe(no_mangle)] + pub static LCI_ERROR_INTERNAL_LOGIC_ERROR: c_int = -8; + + pub(crate) fn map_error(err: &Error) -> c_int { + match err { + Error::ParsingIncomplete(_) => LCI_ERROR_PARSING_ERROR, + Error::UnconsumedInput(_) => LCI_ERROR_PARSING_ERROR, + Error::ParsingError(_, _) => LCI_ERROR_PARSING_ERROR, + Error::PeParsingError(_, _) => LCI_ERROR_PE_PARSING_ERROR, + Error::IoError(_, _) => LCI_ERROR_IO_ERROR, + _ => LCI_ERROR_INTERNAL_LOGIC_ERROR, + } + } +} + +pub(crate) mod libloadorder { + use loadorder::Error; + use std::ffi::c_uint; + + /// There is a mismatch between the files used to keep track of load order. + /// + /// This warning can only occur when using libloadorder with a game that uses the textfile-based + /// load order system. The load order in the active plugins list file (`plugins.txt`) does not + /// match the load order in the full load order file (`loadorder.txt`). Synchronisation between + /// the two is automatic when load order is managed through libloadorder. It is left to the client + /// to decide how best to restore synchronisation. + #[unsafe(no_mangle)] + pub static LIBLO_WARN_LO_MISMATCH: c_uint = 2; + + /// The specified file could not be found. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_FILE_NOT_FOUND: c_uint = 6; + + /// A file could not be renamed. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_FILE_RENAME_FAIL: c_uint = 7; + + /// There was an error parsing a plugin file. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_FILE_PARSE_FAIL: c_uint = 10; + + /// Invalid arguments were given for the function. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_INVALID_ARGS: c_uint = 12; + + /// A thread lock was poisoned. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_POISONED_THREAD_LOCK: c_uint = 14; + + /// An unknown I/O error occurred. This is used when the I/O error kind doesn't fit another error + /// code. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_IO_ERROR: c_uint = 15; + + /// Permission denied while trying to access a filesystem path. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_IO_PERMISSION_DENIED: c_uint = 16; + + /// A plugin filename contains characters that do not have Windows-1252 code points, or a character + /// string contains a null character. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_TEXT_ENCODE_FAIL: c_uint = 17; + + /// Text expected to be encoded in Windows-1252 could not be decoded to UTF-8. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_TEXT_DECODE_FAIL: c_uint = 18; + + /// The library encountered an error that should not have been possible to encounter. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_INTERNAL_LOGIC_ERROR: c_uint = 19; + + /// Something panicked. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_PANICKED: c_uint = 20; + + /// A path cannot be encoded in UTF-8. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_PATH_ENCODE_FAIL: c_uint = 21; + + /// An unknown operating system error occurred. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_SYSTEM_ERROR: c_uint = 22; + + /// A system path definition (e.g. for local app data on Windows, or $HOME on Linux) could not be + /// found. + #[unsafe(no_mangle)] + pub static LIBLO_ERROR_NO_PATH: c_uint = 23; + + /// Matches the value of the highest-numbered return code. + /// + /// Provided in case clients wish to incorporate additional return codes in their implementation + /// and desire some method of avoiding value conflicts. + #[unsafe(no_mangle)] + pub static LIBLO_RETURN_MAX: c_uint = 23; + + fn map_io_error(err: &std::io::Error) -> c_uint { + use std::io::ErrorKind::*; + match err.kind() { + NotFound => LIBLO_ERROR_FILE_NOT_FOUND, + AlreadyExists => LIBLO_ERROR_FILE_RENAME_FAIL, + PermissionDenied => LIBLO_ERROR_IO_PERMISSION_DENIED, + _ => LIBLO_ERROR_IO_ERROR, + } + } + + pub(crate) fn map_error(err: &Error) -> c_uint { + use Error::*; + match *err { + InvalidPath(_) => LIBLO_ERROR_FILE_NOT_FOUND, + IoError(_, ref x) => map_io_error(x), + NoFilename(_) => LIBLO_ERROR_FILE_PARSE_FAIL, + DecodeError(_) => LIBLO_ERROR_TEXT_DECODE_FAIL, + EncodeError(_) => LIBLO_ERROR_TEXT_ENCODE_FAIL, + PluginParsingError(_, _) => LIBLO_ERROR_FILE_PARSE_FAIL, + PluginNotFound(_) => LIBLO_ERROR_INVALID_ARGS, + TooManyActivePlugins { .. } => LIBLO_ERROR_INVALID_ARGS, + DuplicatePlugin(_) => LIBLO_ERROR_INVALID_ARGS, + NonMasterBeforeMaster { .. } => LIBLO_ERROR_INVALID_ARGS, + InvalidEarlyLoadingPluginPosition { .. } => LIBLO_ERROR_INVALID_ARGS, + ImplicitlyActivePlugin(_) => LIBLO_ERROR_INVALID_ARGS, + NoLocalAppData => LIBLO_ERROR_INVALID_ARGS, + NoDocumentsPath => LIBLO_ERROR_INVALID_ARGS, + NoUserConfigPath => LIBLO_ERROR_NO_PATH, + NoUserDataPath => LIBLO_ERROR_NO_PATH, + NoProgramFilesPath => LIBLO_ERROR_NO_PATH, + UnrepresentedHoist { .. } => LIBLO_ERROR_INVALID_ARGS, + InstalledPlugin(_) => LIBLO_ERROR_INVALID_ARGS, + IniParsingError { .. } => LIBLO_ERROR_FILE_PARSE_FAIL, + VdfParsingError(_, _) => LIBLO_ERROR_FILE_PARSE_FAIL, + SystemError(_, _) => LIBLO_ERROR_SYSTEM_ERROR, + InvalidBlueprintPluginPosition { .. } => LIBLO_ERROR_INVALID_ARGS, + _ => LIBLO_ERROR_INTERNAL_LOGIC_ERROR, + } + } +} + +pub(crate) mod esplugin { + use std::ffi::c_uint; + + use esplugin::Error; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_NULL_POINTER: u32 = 1; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_NOT_UTF8: u32 = 2; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_STRING_CONTAINS_NUL: u32 = 3; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_INVALID_GAME_ID: u32 = 4; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_PARSE_ERROR: u32 = 5; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_PANICKED: u32 = 6; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_NO_FILENAME: u32 = 7; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_TEXT_DECODE_ERROR: u32 = 8; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_TEXT_ENCODE_ERROR: u32 = 9; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_IO_ERROR: u32 = 10; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_FILE_NOT_FOUND: u32 = 11; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_IO_PERMISSION_DENIED: u32 = 12; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_UNRESOLVED_RECORD_IDS: u32 = 13; + + #[unsafe(no_mangle)] + pub static ESP_ERROR_PLUGIN_METADATA_NOT_FOUND: u32 = 14; + + fn map_io_error(err: &std::io::Error) -> c_uint { + match err.kind() { + std::io::ErrorKind::NotFound => ESP_ERROR_FILE_NOT_FOUND, + std::io::ErrorKind::PermissionDenied => ESP_ERROR_IO_PERMISSION_DENIED, + _ => ESP_ERROR_IO_ERROR, + } + } + + pub(crate) fn map_error(err: &Error) -> c_uint { + match *err { + Error::IoError(ref x) => map_io_error(x), + Error::NoFilename(_) => ESP_ERROR_NO_FILENAME, + Error::ParsingIncomplete(_) => ESP_ERROR_PARSE_ERROR, + Error::ParsingError(_, _) => ESP_ERROR_PARSE_ERROR, + Error::DecodeError(_) => ESP_ERROR_TEXT_DECODE_ERROR, + Error::UnresolvedRecordIds(_) => ESP_ERROR_UNRESOLVED_RECORD_IDS, + Error::PluginMetadataNotFound(_) => ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, + } + } +} diff --git a/cxx/src/lib.rs b/cxx/src/lib.rs index e52a8e9c..56d97ea3 100644 --- a/cxx/src/lib.rs +++ b/cxx/src/lib.rs @@ -1,10 +1,11 @@ mod database; +mod error_codes; mod game; mod metadata; mod plugin; use database::{Database, Vertex, new_vertex}; -use game::{Game, new_game, new_game_with_local_path}; +use game::{Game, NotValidUtf8, new_game, new_game_with_local_path}; use metadata::{ File, Filename, Group, Location, Message, MessageContent, OptionalMessageContentRef, OptionalPluginMetadata, PluginCleaningData, PluginMetadata, Tag, group_default_name, @@ -14,32 +15,217 @@ use metadata::{ }; use plugin::{OptionalPluginRef, PluginRef}; use std::{ - ffi::{c_char, c_uchar, c_uint, c_void, CString}, - sync::{atomic::AtomicPtr, Mutex}, + error::Error, + ffi::{CString, c_char, c_uchar, c_uint, c_void}, + sync::{Mutex, atomic::AtomicPtr}, }; use unicase::UniCase; -use libloot::set_logging_callback; +use libloot::{ + error::{ + ConditionEvaluationError, DatabaseLockPoisonError, GameHandleCreationError, + GroupsPathError, LoadOrderError, LoadOrderStateError, LoadPluginsError, + MetadataRetrievalError, PluginDataError, SortPluginsError, + }, + metadata::error::{ + LoadMetadataError, MultilingualMessageContentsError, RegexError, WriteMetadataError, + }, + set_logging_callback, +}; pub use libloot::{is_compatible, libloot_revision, libloot_version}; #[derive(Debug)] -pub struct VerboseError(Box); +pub enum VerboseError { + CyclicInteractionError(Vec), + UndefinedGroupError(String), + EspluginError(u32, String), + LibloadorderError(u32, String), + LciError(i32, String), + FileAccessError(String), + InvalidArgument(String), + Other(Box), +} impl std::fmt::Display for VerboseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0)?; - let mut error = self.0.as_ref(); - while let Some(source) = error.source() { - write!(f, ": {}", source)?; - error = source; + match self { + Self::CyclicInteractionError(cycle) => { + write!(f, "CyclicInteractionError: ")?; + for vertex in cycle { + let name = vertex.name().replace("\\", "\\\\").replace("-", "\\-"); + match vertex.out_edge_type() { + Some(e) => write!(f, "{}--{}--", name, e)?, + None => write!(f, "{}", name)?, + } + } + Ok(()) + } + Self::UndefinedGroupError(group) => { + write!(f, "UndefinedGroupError: {}", group) + } + Self::EspluginError(c, s) => { + write!(f, "EspluginError: {}: {}", c, s) + } + Self::LibloadorderError(c, s) => { + write!(f, "LibloadorderError: {}: {}", c, s) + } + Self::LciError(c, s) => { + write!(f, "LciError: {}: {}", c, s) + } + Self::FileAccessError(s) => write!(f, "FileAccessError: {}", s), + Self::InvalidArgument(s) => write!(f, "InvalidArgument: {}", s), + Self::Other(e) => { + write!(f, "{}", e)?; + let mut error = e.as_ref(); + while let Some(source) = error.source() { + write!(f, ": {}", source)?; + error = source; + } + Ok(()) + } } - Ok(()) } } -impl From for VerboseError { - fn from(value: E) -> Self { - VerboseError(Box::new(value)) +impl From for VerboseError { + fn from(value: GameHandleCreationError) -> Self { + match value { + GameHandleCreationError::LoadOrderError(e) => e.into(), + GameHandleCreationError::NotADirectory(_) => { + Self::InvalidArgument(value.to_string()) + } + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: UnsupportedEnumValueError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: NotValidUtf8) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: DatabaseLockPoisonError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: LoadPluginsError) -> Self { + match value { + LoadPluginsError::PluginDataError(e) => e.into(), + LoadPluginsError::PluginValidationError(_) => { + Self::InvalidArgument(value.to_string()) + } + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: SortPluginsError) -> Self { + match value { + SortPluginsError::MetadataRetrievalError(e) => e.into(), + SortPluginsError::UndefinedGroup(g) => Self::UndefinedGroupError(g), + SortPluginsError::CycleFound(cycle) => Self::CyclicInteractionError(cycle), + SortPluginsError::PluginDataError(e) => e.into(), + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: LoadOrderStateError) -> Self { + match value { + LoadOrderStateError::LoadOrderError(e) => e.into(), + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: LoadOrderError) -> Self { + let error = value + .source() + .expect("LoadOrderError has source") + .downcast_ref::() + .expect("LoadOrderError source is a loadorder::Error"); + let error_code = error_codes::libloadorder::map_error(error); + Self::LibloadorderError(error_code, error.to_string()) + } +} + +impl From for VerboseError { + fn from(value: LoadMetadataError) -> Self { + Self::FileAccessError(value.to_string()) + } +} + +impl From for VerboseError { + fn from(value: WriteMetadataError) -> Self { + Self::FileAccessError(value.to_string()) + } +} + +impl From for VerboseError { + fn from(value: ConditionEvaluationError) -> Self { + let error = value + .source() + .expect("LoadOrderError has source") + .downcast_ref::() + .expect("LoadOrderError source is a loot_condition_interpreter::Error"); + let error_code = error_codes::lci::map_error(error); + Self::LciError(error_code, error.to_string()) + } +} + +impl From for VerboseError { + fn from(value: GroupsPathError) -> Self { + match value { + GroupsPathError::UndefinedGroup(g) => Self::UndefinedGroupError(g), + GroupsPathError::CycleFound(cycle) => Self::CyclicInteractionError(cycle), + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: MetadataRetrievalError) -> Self { + match value { + MetadataRetrievalError::ConditionEvaluationError(e) => e.into(), + _ => Self::Other(Box::new(value)), + } + } +} + +impl From for VerboseError { + fn from(value: PluginDataError) -> Self { + let error = value + .source() + .expect("LoadOrderError has source") + .downcast_ref::() + .expect("LoadOrderError source is an esplugin::Error"); + let error_code = error_codes::esplugin::map_error(error); + Self::EspluginError(error_code, error.to_string()) + } +} + +impl From for VerboseError { + fn from(value: MultilingualMessageContentsError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: RegexError) -> Self { + Self::Other(Box::new(value)) } }