From de97b906182fcffd3da7c19965cc5cb2d8d85a3e Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Mon, 17 Mar 2025 21:21:51 +0000 Subject: [PATCH] Improve Python wrapper error handling The new libloot-ffi-errors crate is for sharing code between the CXX and PyO3 wrappers. --- Cargo.lock | 11 +- Cargo.toml | 13 +- cxx/Cargo.toml | 5 +- cxx/src/error_codes.rs | 236 --------------------------------- cxx/src/lib.rs | 34 ++--- ffi-errors/Cargo.toml | 11 ++ ffi-errors/src/esplugin.rs | 65 +++++++++ ffi-errors/src/lci.rs | 45 +++++++ ffi-errors/src/lib.rs | 99 ++++++++++++++ ffi-errors/src/libloadorder.rs | 118 +++++++++++++++++ pyo3/Cargo.toml | 1 + pyo3/README.md | 10 +- pyo3/src/database.rs | 2 +- pyo3/src/error.rs | 208 +++++++++++++++++++++++++++++ pyo3/src/game.rs | 6 +- pyo3/src/lib.rs | 61 ++------- pyo3/src/metadata.rs | 2 +- pyo3/src/plugin.rs | 2 +- 18 files changed, 605 insertions(+), 324 deletions(-) delete mode 100644 cxx/src/error_codes.rs create mode 100644 ffi-errors/Cargo.toml create mode 100644 ffi-errors/src/esplugin.rs create mode 100644 ffi-errors/src/lci.rs create mode 100644 ffi-errors/src/lib.rs create mode 100644 ffi-errors/src/libloadorder.rs create mode 100644 pyo3/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 8b5f4c29..5ad283a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,11 +600,19 @@ dependencies = [ "cxx", "cxx-build", "delegate", + "libloot", + "libloot-ffi-errors", + "unicase", +] + +[[package]] +name = "libloot-ffi-errors" +version = "0.25.5" +dependencies = [ "esplugin", "libloadorder", "libloot", "loot-condition-interpreter", - "unicase", ] [[package]] @@ -612,6 +620,7 @@ name = "libloot_pyo3" version = "0.25.5" dependencies = [ "libloot", + "libloot-ffi-errors", "pyo3", ] diff --git a/Cargo.toml b/Cargo.toml index 30b8dfdd..66bc01c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,10 @@ license = "GPL-3.0" [dependencies] crc32fast = "1.4.2" -esplugin = "6.1.1" -libloadorder = { git = "https://github.com/Ortham/libloadorder.git", rev = "6245dbc6824c4751daef5ab01dfe7b9497f5446f" } +esplugin.workspace = true log = { version = "0.4.26", features = ["std"] } -loot-condition-interpreter = { git = "https://github.com/loot/loot-condition-interpreter.git", rev = "f7d9947fa434037743ce81e2d409184ec0bfb693" } +libloadorder.workspace = true +loot-condition-interpreter.workspace = true petgraph = "0.7.1" fancy-regex = "0.14.0" saphyr = "0.0.3" @@ -26,4 +26,9 @@ rstest_reuse = "0.7.0" tempfile = "3.17.1" [workspace] -members = ["cxx", "pyo3"] +members = ["cxx", "ffi-errors", "pyo3"] + +[workspace.dependencies] +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" } diff --git a/cxx/Cargo.toml b/cxx/Cargo.toml index 9e3bfc32..04d3b425 100644 --- a/cxx/Cargo.toml +++ b/cxx/Cargo.toml @@ -8,12 +8,9 @@ license = "GPL-3.0" cxx = { version = "1.0", features = ["c++17"] } delegate = "0.13.2" libloot = { path = ".." } +libloot-ffi-errors = { path = "../ffi-errors" } 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/src/error_codes.rs b/cxx/src/error_codes.rs deleted file mode 100644 index 170b0137..00000000 --- a/cxx/src/error_codes.rs +++ /dev/null @@ -1,236 +0,0 @@ -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 a56c2e5e..27012327 100644 --- a/cxx/src/lib.rs +++ b/cxx/src/lib.rs @@ -1,11 +1,11 @@ mod database; -mod error_codes; mod game; mod metadata; mod plugin; use database::{Database, Vertex, new_vertex}; use game::{Game, NotValidUtf8, new_game, new_game_with_local_path}; +use libloot_ffi_errors::SystemError; use metadata::{ File, Filename, Group, Location, Message, MessageContent, OptionalMessageContentRef, OptionalPluginMetadata, PluginCleaningData, PluginMetadata, Tag, group_default_name, @@ -15,7 +15,6 @@ use metadata::{ }; use plugin::{OptionalPlugin, Plugin}; use std::{ - error::Error, ffi::{CString, c_char, c_uchar, c_uint, c_void}, sync::{Mutex, atomic::AtomicPtr}, }; @@ -38,8 +37,8 @@ pub use libloot::{is_compatible, libloot_revision, libloot_version}; pub enum VerboseError { CyclicInteractionError(Vec), UndefinedGroupError(String), - EspluginError(u32, String), - LibloadorderError(u32, String), + EspluginError(i32, String), + LibloadorderError(i32, String), LciError(i32, String), FileAccessError(String), InvalidArgument(String), @@ -148,13 +147,8 @@ impl From for VerboseError { 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()) + let error = SystemError::from(value); + Self::LibloadorderError(error.code(), error.message().to_string()) } } @@ -172,13 +166,8 @@ impl From for VerboseError { 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()) + let error = SystemError::from(value); + Self::LciError(error.code(), error.message().to_string()) } } @@ -203,13 +192,8 @@ impl From for VerboseError { 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()) + let error = SystemError::from(value); + Self::EspluginError(error.code(), error.message().to_string()) } } diff --git a/ffi-errors/Cargo.toml b/ffi-errors/Cargo.toml new file mode 100644 index 00000000..1cc1b72f --- /dev/null +++ b/ffi-errors/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "libloot-ffi-errors" +version = "0.25.5" +edition = "2024" + +[dependencies] +libloot = { path = ".." } + +esplugin.workspace = true +libloadorder.workspace = true +loot-condition-interpreter.workspace = true diff --git a/ffi-errors/src/esplugin.rs b/ffi-errors/src/esplugin.rs new file mode 100644 index 00000000..71ccba5c --- /dev/null +++ b/ffi-errors/src/esplugin.rs @@ -0,0 +1,65 @@ +use std::ffi::c_int; + +use esplugin::Error; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_NULL_POINTER: c_int = 1; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_NOT_UTF8: c_int = 2; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_STRING_CONTAINS_NUL: c_int = 3; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_INVALID_GAME_ID: c_int = 4; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_PARSE_ERROR: c_int = 5; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_PANICKED: c_int = 6; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_NO_FILENAME: c_int = 7; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_TEXT_DECODE_ERROR: c_int = 8; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_TEXT_ENCODE_ERROR: c_int = 9; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_IO_ERROR: c_int = 10; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_FILE_NOT_FOUND: c_int = 11; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_IO_PERMISSION_DENIED: c_int = 12; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_UNRESOLVED_RECORD_IDS: c_int = 13; + +#[unsafe(no_mangle)] +pub static ESP_ERROR_PLUGIN_METADATA_NOT_FOUND: c_int = 14; + +fn map_io_error(err: &std::io::Error) -> c_int { + 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 fn map_error(err: &Error) -> c_int { + 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/ffi-errors/src/lci.rs b/ffi-errors/src/lci.rs new file mode 100644 index 00000000..d2475d50 --- /dev/null +++ b/ffi-errors/src/lci.rs @@ -0,0 +1,45 @@ +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 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, + } +} diff --git a/ffi-errors/src/lib.rs b/ffi-errors/src/lib.rs new file mode 100644 index 00000000..34b09daa --- /dev/null +++ b/ffi-errors/src/lib.rs @@ -0,0 +1,99 @@ +use std::{ + error::Error, + ffi::{c_int, c_uchar}, +}; + +use libloot::error::{ConditionEvaluationError, LoadOrderError, PluginDataError}; + +pub mod esplugin; +pub mod lci; +pub mod libloadorder; + +/// An error from esplugin. +#[unsafe(no_mangle)] +pub static LIBLOOT_SYSTEM_ERROR_CATEGORY_ESPLUGIN: c_uchar = 1; + +/// An error from libloadorder. +#[unsafe(no_mangle)] +pub static LIBLOOT_SYSTEM_ERROR_CATEGORY_LIBLOADORDER: c_uchar = 2; + +/// An error from loot-condition-interpreter. +#[unsafe(no_mangle)] +pub static LIBLOOT_SYSTEM_ERROR_CATEGORY_LCI: c_uchar = 3; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[repr(u8)] +pub enum SystemErrorCategory { + Esplugin = LIBLOOT_SYSTEM_ERROR_CATEGORY_ESPLUGIN, + Libloadorder = LIBLOOT_SYSTEM_ERROR_CATEGORY_LIBLOADORDER, + LootConditionInterpreter = LIBLOOT_SYSTEM_ERROR_CATEGORY_LCI, +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct SystemError { + code: c_int, + category: SystemErrorCategory, + message: String, +} + +impl SystemError { + pub fn code(&self) -> c_int { + self.code + } + + pub fn category(&self) -> SystemErrorCategory { + self.category + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl From for SystemError { + fn from(value: PluginDataError) -> Self { + let error = value + .source() + .expect("LoadOrderError has source") + .downcast_ref::<::esplugin::Error>() + .expect("LoadOrderError source is an esplugin::Error"); + let error_code = crate::esplugin::map_error(error); + SystemError { + code: error_code, + category: SystemErrorCategory::Esplugin, + message: error.to_string(), + } + } +} + +impl From for SystemError { + 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 = crate::libloadorder::map_error(error); + SystemError { + code: error_code, + category: SystemErrorCategory::Libloadorder, + message: error.to_string(), + } + } +} + +impl From for SystemError { + fn from(value: ConditionEvaluationError) -> Self { + let error = value + .source() + .expect("ConditionEvaluationError has source") + .downcast_ref::() + .expect("ConditionEvaluationError source is a loot_condition_interpreter::Error"); + let error_code = crate::lci::map_error(error); + SystemError { + code: error_code, + category: SystemErrorCategory::LootConditionInterpreter, + message: error.to_string(), + } + } +} diff --git a/ffi-errors/src/libloadorder.rs b/ffi-errors/src/libloadorder.rs new file mode 100644 index 00000000..dcd23a09 --- /dev/null +++ b/ffi-errors/src/libloadorder.rs @@ -0,0 +1,118 @@ +use loadorder::Error; +use std::ffi::c_int; + +/// 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_int = 2; + +/// The specified file could not be found. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_FILE_NOT_FOUND: c_int = 6; + +/// A file could not be renamed. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_FILE_RENAME_FAIL: c_int = 7; + +/// There was an error parsing a plugin file. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_FILE_PARSE_FAIL: c_int = 10; + +/// Invalid arguments were given for the function. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_INVALID_ARGS: c_int = 12; + +/// A thread lock was poisoned. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_POISONED_THREAD_LOCK: c_int = 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_int = 15; + +/// Permission denied while trying to access a filesystem path. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_IO_PERMISSION_DENIED: c_int = 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_int = 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_int = 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_int = 19; + +/// Something panicked. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_PANICKED: c_int = 20; + +/// A path cannot be encoded in UTF-8. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_PATH_ENCODE_FAIL: c_int = 21; + +/// An unknown operating system error occurred. +#[unsafe(no_mangle)] +pub static LIBLO_ERROR_SYSTEM_ERROR: c_int = 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_int = 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_int = 23; + +fn map_io_error(err: &std::io::Error) -> c_int { + 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 fn map_error(err: &Error) -> c_int { + 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, + } +} diff --git a/pyo3/Cargo.toml b/pyo3/Cargo.toml index d3bf2db9..5bb65565 100644 --- a/pyo3/Cargo.toml +++ b/pyo3/Cargo.toml @@ -10,4 +10,5 @@ crate-type = ["cdylib"] [dependencies] libloot = { path = ".." } +libloot-ffi-errors = { path = "../ffi-errors" } pyo3 = "0.24.0" diff --git a/pyo3/README.md b/pyo3/README.md index 806c886b..cca22f10 100644 --- a/pyo3/README.md +++ b/pyo3/README.md @@ -31,8 +31,7 @@ An **incomplete** and **experimental** Python wrapper around the libloot Rust im - [x] `MessageType` - [x] `TagSuggestion` - [x] `select_message_content()` -- [ ] Error types - - All errors are currently raised as Python `ValueError` values that contain the full detail of the error. +- [x] Error types ## Build @@ -58,3 +57,10 @@ Then import it in Python: py > import loot ``` + +## Usage notes + +- The Python exceptions that errors are mapped to are not the same as in the Rust or C++ interfaces: + - The API provides the custom `CyclicInteractionError`, `UndefinedGroupError`, `EspluginError` exception types. + - All other errors are raised as `ValueError` exceptions. + - There's no equivalent to the C++ interface's `FileAccessError` or `ConditionSyntaxError` classes or the libloadorder and loot-condition-interpreter system error categories. diff --git a/pyo3/src/database.rs b/pyo3/src/database.rs index 59ed3550..003f9eb3 100644 --- a/pyo3/src/database.rs +++ b/pyo3/src/database.rs @@ -11,7 +11,7 @@ use pyo3::{ }; use crate::{ - UnsupportedEnumValueError, VerboseError, + error::{UnsupportedEnumValueError, VerboseError}, metadata::{Group, Message, NONE_REPR, PluginMetadata}, }; diff --git a/pyo3/src/error.rs b/pyo3/src/error.rs new file mode 100644 index 00000000..119a4afa --- /dev/null +++ b/pyo3/src/error.rs @@ -0,0 +1,208 @@ +use libloot::{ + error::{ + ConditionEvaluationError, DatabaseLockPoisonError, GameHandleCreationError, + GroupsPathError, LoadOrderError, LoadOrderStateError, LoadPluginsError, + MetadataRetrievalError, PluginDataError, SortPluginsError, + }, + metadata::error::{ + LoadMetadataError, MultilingualMessageContentsError, RegexError, WriteMetadataError, + }, +}; +use libloot_ffi_errors::SystemError; +use pyo3::{PyErr, exceptions::PyValueError}; + +use crate::{CyclicInteractionError, EspluginError, UndefinedGroupError, database::Vertex}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct UnsupportedEnumValueError; + +impl std::fmt::Display for UnsupportedEnumValueError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Enum value is unsupported") + } +} + +impl std::error::Error for UnsupportedEnumValueError {} + +impl From for PyErr { + fn from(value: UnsupportedEnumValueError) -> Self { + PyValueError::new_err(value.to_string()) + } +} + +#[derive(Debug)] +pub enum VerboseError { + CyclicInteractionError(Vec), + UndefinedGroupError(String), + EspluginError(i32, String), + Other(Box), +} + +impl std::fmt::Display for VerboseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CyclicInteractionError(c) => { + let cycle = display_cycle(c); + write!(f, "cyclic interaction detected: {}", cycle) + } + Self::UndefinedGroupError(g) => { + write!(f, "the group \"{}\" does not exist", g) + } + Self::EspluginError(_, s) => s.fmt(f), + Self::Other(e) => { + write!(f, "{}", e)?; + let mut error = e.as_ref(); + while let Some(source) = error.source() { + write!(f, ": {}", source)?; + error = source; + } + Ok(()) + } + } + } +} + +impl From for VerboseError { + fn from(value: GameHandleCreationError) -> Self { + match value { + GameHandleCreationError::LoadOrderError(e) => e.into(), + _ => 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: DatabaseLockPoisonError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: LoadPluginsError) -> Self { + match value { + LoadPluginsError::PluginDataError(e) => e.into(), + _ => 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 { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: LoadMetadataError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: WriteMetadataError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for VerboseError { + fn from(value: ConditionEvaluationError) -> Self { + Self::Other(Box::new(value)) + } +} + +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 = SystemError::from(value); + Self::EspluginError(error.code(), error.message().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)) + } +} + +impl From for PyErr { + fn from(value: VerboseError) -> Self { + let message = value.to_string(); + + match value { + VerboseError::CyclicInteractionError(c) => PyErr::new::(( + c.into_iter().map(Vertex::from).collect::>(), + message, + )), + VerboseError::UndefinedGroupError(g) => { + PyErr::new::((g, message)) + } + VerboseError::EspluginError(i, s) => PyErr::new::((i, s)), + VerboseError::Other(_) => PyValueError::new_err(message), + } + } +} + +fn display_cycle(cycle: &[libloot::Vertex]) -> String { + cycle + .iter() + .map(|v| { + if let Some(edge_type) = v.out_edge_type() { + format!("{} --[{}]-> ", v.name(), edge_type) + } else { + v.name().to_string() + } + }) + .chain(cycle.first().iter().map(|v| v.name().to_string())) + .collect() +} diff --git a/pyo3/src/game.rs b/pyo3/src/game.rs index 66757372..0bca69c2 100644 --- a/pyo3/src/game.rs +++ b/pyo3/src/game.rs @@ -2,7 +2,11 @@ use std::path::{Path, PathBuf}; use pyo3::{pyclass, pymethods}; -use crate::{UnsupportedEnumValueError, VerboseError, database::Database, plugin::Plugin}; +use crate::{ + database::Database, + error::{UnsupportedEnumValueError, VerboseError}, + plugin::Plugin, +}; #[allow(non_camel_case_types)] #[pyclass(eq, frozen, hash, ord)] diff --git a/pyo3/src/lib.rs b/pyo3/src/lib.rs index 9eb43ed1..8a871ee5 100644 --- a/pyo3/src/lib.rs +++ b/pyo3/src/lib.rs @@ -1,4 +1,5 @@ mod database; +mod error; mod game; mod metadata; mod plugin; @@ -10,7 +11,7 @@ use metadata::{ TagSuggestion, select_message_content, }; use plugin::Plugin; -use pyo3::{exceptions::PyValueError, prelude::*}; +use pyo3::{create_exception, exceptions::PyException, prelude::*}; #[pyfunction] fn is_compatible(major: u32, minor: u32, patch: u32) -> bool { @@ -27,56 +28,13 @@ fn libloot_version() -> String { libloot::libloot_version() } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct UnsupportedEnumValueError; - -impl std::fmt::Display for UnsupportedEnumValueError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Enum value is unsupported") - } -} - -impl std::error::Error for UnsupportedEnumValueError {} - -impl From for PyErr { - fn from(value: UnsupportedEnumValueError) -> Self { - PyValueError::new_err(value.to_string()) - } -} - -#[derive(Debug)] -pub struct VerboseError(Box); - -impl std::fmt::Display for VerboseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let e = &self.0; - write!(f, "{}", e)?; - let mut error = e.as_ref(); - while let Some(source) = error.source() { - write!(f, ": {}", source)?; - error = source; - } - Ok(()) - } -} - -// impl std::error::Error for VerboseError {} - -impl From for VerboseError { - fn from(value: T) -> Self { - VerboseError(Box::new(value)) - } -} - -impl From for PyErr { - fn from(value: VerboseError) -> Self { - PyValueError::new_err(value.to_string()) - } -} +create_exception!(loot, CyclicInteractionError, PyException); +create_exception!(loot, UndefinedGroupError, PyException); +create_exception!(loot, EspluginError, PyException); /// A Python module implemented in Rust. #[pymodule(name = "loot")] -fn libloot_pyo3(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn libloot_pyo3(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("LIBLOOT_VERSION_MAJOR", libloot::LIBLOOT_VERSION_MAJOR)?; m.add("LIBLOOT_VERSION_MINOR", libloot::LIBLOOT_VERSION_MINOR)?; m.add("LIBLOOT_VERSION_PATCH", libloot::LIBLOOT_VERSION_PATCH)?; @@ -104,5 +62,12 @@ fn libloot_pyo3(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add( + "CyclicInteractionError", + py.get_type::(), + )?; + m.add("UndefinedGroupError", py.get_type::())?; + m.add("EspluginError", py.get_type::())?; + Ok(()) } diff --git a/pyo3/src/metadata.rs b/pyo3/src/metadata.rs index 3537210c..9c82e88b 100644 --- a/pyo3/src/metadata.rs +++ b/pyo3/src/metadata.rs @@ -5,7 +5,7 @@ use pyo3::{ types::{PyAnyMethods, PyTypeMethods}, }; -use crate::{UnsupportedEnumValueError, VerboseError}; +use crate::error::{UnsupportedEnumValueError, VerboseError}; pub const NONE_REPR: &str = "None"; diff --git a/pyo3/src/plugin.rs b/pyo3/src/plugin.rs index b897cedb..94eafcd9 100644 --- a/pyo3/src/plugin.rs +++ b/pyo3/src/plugin.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use pyo3::{pyclass, pymethods}; -use crate::VerboseError; +use crate::error::VerboseError; #[pyclass(eq, frozen)] #[derive(Clone, Debug, Eq, PartialEq)]