Improve Python wrapper error handling

The new libloot-ffi-errors crate is for sharing code between the CXX and PyO3 wrappers.
This commit is contained in:
Oliver Hamlet
2025-03-25 22:02:33 +00:00
parent 3c02bfd1da
commit de97b90618
18 changed files with 605 additions and 324 deletions
Generated
+10 -1
View File
@@ -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",
]
+9 -4
View File
@@ -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" }
+1 -4
View File
@@ -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"
-236
View File
@@ -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,
}
}
}
+9 -25
View File
@@ -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<libloot::Vertex>),
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<LoadOrderStateError> for VerboseError {
impl From<LoadOrderError> for VerboseError {
fn from(value: LoadOrderError) -> Self {
let error = value
.source()
.expect("LoadOrderError has source")
.downcast_ref::<loadorder::Error>()
.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<WriteMetadataError> for VerboseError {
impl From<ConditionEvaluationError> for VerboseError {
fn from(value: ConditionEvaluationError) -> Self {
let error = value
.source()
.expect("LoadOrderError has source")
.downcast_ref::<loot_condition_interpreter::Error>()
.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<MetadataRetrievalError> for VerboseError {
impl From<PluginDataError> for VerboseError {
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 = 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())
}
}
+11
View File
@@ -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
+65
View File
@@ -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,
}
}
+45
View File
@@ -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,
}
}
+99
View File
@@ -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<PluginDataError> 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<LoadOrderError> for SystemError {
fn from(value: LoadOrderError) -> Self {
let error = value
.source()
.expect("LoadOrderError has source")
.downcast_ref::<loadorder::Error>()
.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<ConditionEvaluationError> for SystemError {
fn from(value: ConditionEvaluationError) -> Self {
let error = value
.source()
.expect("ConditionEvaluationError has source")
.downcast_ref::<loot_condition_interpreter::Error>()
.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(),
}
}
}
+118
View File
@@ -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,
}
}
+1
View File
@@ -10,4 +10,5 @@ crate-type = ["cdylib"]
[dependencies]
libloot = { path = ".." }
libloot-ffi-errors = { path = "../ffi-errors" }
pyo3 = "0.24.0"
+8 -2
View File
@@ -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.
+1 -1
View File
@@ -11,7 +11,7 @@ use pyo3::{
};
use crate::{
UnsupportedEnumValueError, VerboseError,
error::{UnsupportedEnumValueError, VerboseError},
metadata::{Group, Message, NONE_REPR, PluginMetadata},
};
+208
View File
@@ -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<UnsupportedEnumValueError> for PyErr {
fn from(value: UnsupportedEnumValueError) -> Self {
PyValueError::new_err(value.to_string())
}
}
#[derive(Debug)]
pub enum VerboseError {
CyclicInteractionError(Vec<libloot::Vertex>),
UndefinedGroupError(String),
EspluginError(i32, String),
Other(Box<dyn std::error::Error>),
}
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<GameHandleCreationError> for VerboseError {
fn from(value: GameHandleCreationError) -> Self {
match value {
GameHandleCreationError::LoadOrderError(e) => e.into(),
_ => Self::Other(Box::new(value)),
}
}
}
impl From<UnsupportedEnumValueError> for VerboseError {
fn from(value: UnsupportedEnumValueError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<DatabaseLockPoisonError> for VerboseError {
fn from(value: DatabaseLockPoisonError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<LoadPluginsError> for VerboseError {
fn from(value: LoadPluginsError) -> Self {
match value {
LoadPluginsError::PluginDataError(e) => e.into(),
_ => Self::Other(Box::new(value)),
}
}
}
impl From<SortPluginsError> 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<LoadOrderStateError> for VerboseError {
fn from(value: LoadOrderStateError) -> Self {
match value {
LoadOrderStateError::LoadOrderError(e) => e.into(),
_ => Self::Other(Box::new(value)),
}
}
}
impl From<LoadOrderError> for VerboseError {
fn from(value: LoadOrderError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<LoadMetadataError> for VerboseError {
fn from(value: LoadMetadataError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<WriteMetadataError> for VerboseError {
fn from(value: WriteMetadataError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<ConditionEvaluationError> for VerboseError {
fn from(value: ConditionEvaluationError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<GroupsPathError> 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<MetadataRetrievalError> for VerboseError {
fn from(value: MetadataRetrievalError) -> Self {
match value {
MetadataRetrievalError::ConditionEvaluationError(e) => e.into(),
_ => Self::Other(Box::new(value)),
}
}
}
impl From<PluginDataError> for VerboseError {
fn from(value: PluginDataError) -> Self {
let error = SystemError::from(value);
Self::EspluginError(error.code(), error.message().to_string())
}
}
impl From<MultilingualMessageContentsError> for VerboseError {
fn from(value: MultilingualMessageContentsError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<RegexError> for VerboseError {
fn from(value: RegexError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<VerboseError> for PyErr {
fn from(value: VerboseError) -> Self {
let message = value.to_string();
match value {
VerboseError::CyclicInteractionError(c) => PyErr::new::<CyclicInteractionError, _>((
c.into_iter().map(Vertex::from).collect::<Vec<_>>(),
message,
)),
VerboseError::UndefinedGroupError(g) => {
PyErr::new::<UndefinedGroupError, _>((g, message))
}
VerboseError::EspluginError(i, s) => PyErr::new::<EspluginError, _>((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()
}
+5 -1
View File
@@ -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)]
+13 -48
View File
@@ -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<UnsupportedEnumValueError> for PyErr {
fn from(value: UnsupportedEnumValueError) -> Self {
PyValueError::new_err(value.to_string())
}
}
#[derive(Debug)]
pub struct VerboseError(Box<dyn std::error::Error>);
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<T: std::error::Error + 'static> From<T> for VerboseError {
fn from(value: T) -> Self {
VerboseError(Box::new(value))
}
}
impl From<VerboseError> 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::<Location>()?;
m.add_class::<PluginMetadata>()?;
m.add(
"CyclicInteractionError",
py.get_type::<CyclicInteractionError>(),
)?;
m.add("UndefinedGroupError", py.get_type::<UndefinedGroupError>())?;
m.add("EspluginError", py.get_type::<EspluginError>())?;
Ok(())
}
+1 -1
View File
@@ -5,7 +5,7 @@ use pyo3::{
types::{PyAnyMethods, PyTypeMethods},
};
use crate::{UnsupportedEnumValueError, VerboseError};
use crate::error::{UnsupportedEnumValueError, VerboseError};
pub const NONE_REPR: &str = "None";
+1 -1
View File
@@ -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)]