diff --git a/Cargo.lock b/Cargo.lock index 3c7e598d..04a5fa64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "arraydeque" version = "0.5.1" @@ -77,12 +83,57 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "const-random" version = "0.1.18" @@ -162,6 +213,65 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050906babad73f9b32a91cecc3063ff1e2235226dd2367dd839fd6fbc941c68a" +dependencies = [ + "cc", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875d58f2ac56025a775b91a424515b5adf1e68205765f2c90e6dd81e269ae004" +dependencies = [ + "cc", + "codespan-reporting", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c3062da294104183e1c34ea9887941d4d8c74f6195ce9fbb430ac4b5290ede" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4b358173a166833ddef75fe468579f71727c789b8082d4cc77c38d08f656c59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9531217f3b5f7728244d2b7312bc6660f6b3e4cdbc118f4f1fbce48cb401a0f" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "dataview" version = "1.0.1" @@ -171,6 +281,17 @@ dependencies = [ "derive_pod", ] +[[package]] +name = "delegate" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297806318ef30ad066b15792a8372858020ae3ca2e414ee6c2133b1eb9e9e945" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_pod" version = "0.1.2" @@ -283,6 +404,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "futures-core" version = "0.3.31" @@ -454,6 +581,16 @@ dependencies = [ "windows", ] +[[package]] +name = "libloot-cxx" +version = "0.25.3" +dependencies = [ + "cxx", + "cxx-build", + "delegate", + "libloot", +] + [[package]] name = "libredox" version = "0.1.3" @@ -464,6 +601,15 @@ dependencies = [ "libc", ] +[[package]] +name = "link-cplusplus" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -841,6 +987,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "saphyr" version = "0.0.3" @@ -863,6 +1015,12 @@ dependencies = [ "hashlink", ] +[[package]] +name = "scratch" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" + [[package]] name = "semver" version = "1.0.26" @@ -880,6 +1038,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.9" @@ -889,6 +1053,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.98" @@ -914,6 +1084,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1010,6 +1189,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "version_check" version = "0.9.5" @@ -1047,6 +1232,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 24ab2fcf..cb2eb6ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,6 @@ windows = { version = "0.60.0", features = ["Win32_Storage_FileSystem"] } rstest = "0.25.0" rstest_reuse = "0.7.0" tempfile = "3.17.1" + +[workspace] +members = ["cxx"] diff --git a/cxx/.gitignore b/cxx/.gitignore new file mode 100644 index 00000000..e9881e13 --- /dev/null +++ b/cxx/.gitignore @@ -0,0 +1,2 @@ +/build +/include diff --git a/cxx/CMakeLists.txt b/cxx/CMakeLists.txt new file mode 100644 index 00000000..1eab6eb3 --- /dev/null +++ b/cxx/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 2.8) +project(ffi_tests CXX) + +include_directories("${CMAKE_SOURCE_DIR}/../target/cxxbridge") + +set(CMAKE_CXX_STANDARD 17) + +include(FetchContent) +include(GoogleTest) + +set(BUILD_GMOCK OFF) +set(gtest_force_shared_crt ON) +set(INSTALL_GTEST OFF) +# Google Test doesn't have a scoped equivalent to BUILD_SHARED_LIBS, so set the +# global value and then set it back to the original value once Google Test has +# been configured. +set(BUILD_SHARED_LIBS_INITIAL ${BUILD_SHARED_LIBS}) +set(BUILD_SHARED_LIBS OFF) +FetchContent_Declare( + GTest + URL "https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz" + URL_HASH "SHA256=7b42b4d6ed48810c5362c265a17faebe90dc2373c885e5216439d37927f02926" + FIND_PACKAGE_ARGS) + +FetchContent_MakeAvailable(GTest) + +if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set (SYSTEM_LIBS pthread dl) +endif () + +if (MSVC) + set (SYSTEM_LIBS ntdll ws2_32 Userenv bcrypt) +endif () + +set (FFI_LIBRARY "${CMAKE_SOURCE_DIR}/../target/debug/libloot_cxx.lib") + +add_executable(ffi_cpp_tests "${CMAKE_SOURCE_DIR}/tests/ffi.cpp") +target_link_libraries(ffi_cpp_tests ${FFI_LIBRARY} ${SYSTEM_LIBS} GTest::gtest_main) + +enable_testing() +gtest_discover_tests(ffi_cpp_tests DISCOVERY_TIMEOUT 10) diff --git a/cxx/Cargo.toml b/cxx/Cargo.toml new file mode 100644 index 00000000..b6c80298 --- /dev/null +++ b/cxx/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libloot-cxx" +version = "0.25.3" +edition = "2024" +license = "GPL-3.0" + +[dependencies] +cxx = { version = "1.0", features = ["c++17"] } +delegate = "0.13.2" +libloot = { path = ".." } + +[build-dependencies] +cxx-build = "1.0" + +[lib] +crate-type = ["staticlib"] diff --git a/cxx/README.md b/cxx/README.md new file mode 100644 index 00000000..5f1bf278 --- /dev/null +++ b/cxx/README.md @@ -0,0 +1,57 @@ +# libloot-rs C++ wrapper + +This is an **imcomplete** and **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface. + +## Current status + +- [x] Expose all API types, aside from `LogLevel` and error types +- [x] Expose all getters and setters on metadata types +- [x] Expose all other methods on `PluginMetadata` +- [x] Expose creator functions (`new_*`) for all metadata types +- [x] Expose `Group::DEFAULT_NAME` (using an accessor function) +- [x] Expose `MessageContent::DEFAULT_LANGUAGE` (using an accessor function) +- [x] Expose `select_message_content()` +- [x] Expose `is_compatible()` +- [x] Expose `libloot_revision()` +- [x] Expose `libloot_version()` +- [x] Expose `LIBLOOT_VERSION_*` constants (as unmangled extern C statics) +- [ ] Expose `LogLevel` +- [ ] Expose `set_logging_callback()` +- [ ] Write tests to cover API usage patterns +- [ ] Write a C++ layer that provides the same API as the C++ implementation of libloot + +## Building + +The wrapper is currently built as a static library using Cargo and cbindgen. + +``` +cargo build --release +cbindgen -o include/libloot.h +``` + +cbindgen is needed to create a header that contains the C FFI exports. It needs to be v0.28.0 or higher to support the Rust 2024 edition's unsafe attributes. The header needs to be included into C++ code separately from the CXX-generated header. While CXX does support including headers, it produces the wrong include path in the C++ it generates due to this crate being a member of a workspace, so the C++ side expects the header in a subdirectory that doesn't exist. + +There are also a small number of C++ tests that can be built and run using CMake: + +``` +cmake -B build . +cmake --build build --config RelWithDebInfo +ctest --test-dir build --output-on-failure -V +``` + +To package the build: + +``` +cpack --config build/CPackConfig.cmake +``` + +This repository (and so the created package) doesn't currently include any of libloot's documentation, besides the API documentation included in the Rust source code. + +## Usage notes + +- `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. diff --git a/cxx/build.rs b/cxx/build.rs new file mode 100644 index 00000000..92388cc9 --- /dev/null +++ b/cxx/build.rs @@ -0,0 +1,7 @@ +fn main() { + cxx_build::bridge("src/lib.rs") + .std("c++17") + .compile("libloot-cxx"); + + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/cxx/cbindgen.toml b/cxx/cbindgen.toml new file mode 100644 index 00000000..87df20f0 --- /dev/null +++ b/cxx/cbindgen.toml @@ -0,0 +1 @@ +namespace = "loot" diff --git a/cxx/src/database.rs b/cxx/src/database.rs new file mode 100644 index 00000000..1f684894 --- /dev/null +++ b/cxx/src/database.rs @@ -0,0 +1,281 @@ +use std::{ + path::Path, + sync::{Arc, RwLock}, +}; + +use delegate::delegate; +use libloot::error::DatabaseLockPoisonError; + +use crate::{ + UnsupportedEnumValueError, VerboseError, + ffi::EdgeType, + metadata::{Group, Message, OptionalPluginMetadata, PluginMetadata, to_vec_of_unwrapped}, +}; + +#[derive(Debug)] +#[repr(transparent)] +pub struct Database(Arc>); + +impl Database { + pub fn new(db: Arc>) -> Self { + Self(db) + } + + pub fn load_masterlist(&self, path: &str) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .load_masterlist(Path::new(path)) + .map_err(Into::into) + } + + pub fn load_masterlist_with_prelude( + &self, + masterlist_path: &str, + prelude_path: &str, + ) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .load_masterlist_with_prelude(Path::new(masterlist_path), Path::new(prelude_path)) + .map_err(Into::into) + } + + pub fn load_userlist(&self, path: &str) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .load_userlist(Path::new(path)) + .map_err(Into::into) + } + + pub fn write_user_metadata( + &self, + output_path: &str, + overwrite: bool, + ) -> Result<(), VerboseError> { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .write_user_metadata(Path::new(output_path), overwrite) + .map_err(Into::into) + } + + pub fn write_minimal_list( + &self, + output_path: &str, + overwrite: bool, + ) -> Result<(), VerboseError> { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .write_minimal_list(Path::new(output_path), overwrite) + .map_err(Into::into) + } + + pub fn known_bash_tags(&self) -> Result, VerboseError> { + Ok(self + .0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .known_bash_tags()) + } + + pub fn general_messages( + &self, + evaluate_conditions: bool, + ) -> Result, VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .general_messages(evaluate_conditions) + .map(|v| v.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + + pub fn groups(&self, include_user_metadata: bool) -> Result, VerboseError> { + Ok(self + .0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .groups(include_user_metadata) + .into_iter() + .map(Into::into) + .collect()) + } + + // I tried returning a GroupRef<'_> here, but it borrows from the DB read guard, which the borrow checker sees as an owned value within this scope, so won't allow me to return a reference to it. This is the only place that uses a group reference, so it's probably not worth figuring out a workaround, and cloning the groups is fine. + pub fn user_groups(&self) -> Result, VerboseError> { + Ok(self + .0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .user_groups() + .iter() + .cloned() + .map(Into::into) + .collect()) + } + + // This is ugly, but Group can't be held as a value in C++ and CXX doesn't support Vec> as a parameter, so this can't take ownership of the input groups. + pub fn set_user_groups(&self, groups: &[Box]) -> Result<(), VerboseError> { + let groups = to_vec_of_unwrapped(groups); + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .set_user_groups(groups); + Ok(()) + } + + pub fn groups_path( + &self, + from_group_name: &str, + to_group_name: &str, + ) -> Result, VerboseError> { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .groups_path(from_group_name, to_group_name) + .map(|v| v.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + + pub fn plugin_metadata( + &self, + plugin_name: &str, + include_user_metadata: bool, + evaluate_conditions: bool, + ) -> Result, VerboseError> { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .plugin_metadata(plugin_name, include_user_metadata, evaluate_conditions) + .map(|p| Box::new(p.map(Into::into).into())) + .map_err(Into::into) + } + + pub fn plugin_user_metadata( + &self, + plugin_name: &str, + evaluate_conditions: bool, + ) -> Result, VerboseError> { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .plugin_user_metadata(plugin_name, evaluate_conditions) + .map(|p| Box::new(p.map(Into::into).into())) + .map_err(Into::into) + } + + pub fn set_plugin_user_metadata( + &mut self, + plugin_metadata: Box, + ) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .set_plugin_user_metadata(plugin_metadata.into()); + Ok(()) + } + + pub fn discard_plugin_user_metadata(&self, plugin: &str) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .discard_plugin_user_metadata(plugin); + Ok(()) + } + + pub fn discard_all_user_metadata(&self) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(|_| DatabaseLockPoisonError)? + .discard_all_user_metadata(); + Ok(()) + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Vertex(libloot::Vertex); + +pub fn new_vertex(name: String) -> Box { + Box::new(Vertex(libloot::Vertex::new(name))) +} + +impl Vertex { + // A value of 255 is used to indicate that there is no out edge. + pub fn out_edge_type(&self) -> Result { + match self.0.out_edge_type() { + Some(e) => Ok(EdgeType::try_from(e)?.repr), + None => Ok(u8::MAX), + } + } + + pub fn set_out_edge_type(&mut self, out_edge_type: EdgeType) -> Result<(), VerboseError> { + self.0.set_out_edge_type(out_edge_type.try_into()?); + Ok(()) + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn name(&self) -> &str; + } + } +} + +impl From for Vertex { + fn from(value: libloot::Vertex) -> Self { + Self(value) + } +} + +impl TryFrom for EdgeType { + type Error = UnsupportedEnumValueError; + + fn try_from(value: libloot::EdgeType) -> Result { + match value { + libloot::EdgeType::Hardcoded => Ok(EdgeType::hardcoded), + libloot::EdgeType::MasterFlag => Ok(EdgeType::masterFlag), + libloot::EdgeType::Master => Ok(EdgeType::master), + libloot::EdgeType::MasterlistRequirement => Ok(EdgeType::masterlistRequirement), + libloot::EdgeType::UserRequirement => Ok(EdgeType::userRequirement), + libloot::EdgeType::MasterlistLoadAfter => Ok(EdgeType::masterlistLoadAfter), + libloot::EdgeType::UserLoadAfter => Ok(EdgeType::userLoadAfter), + libloot::EdgeType::MasterlistGroup => Ok(EdgeType::masterlistGroup), + libloot::EdgeType::UserGroup => Ok(EdgeType::userGroup), + libloot::EdgeType::RecordOverlap => Ok(EdgeType::recordOverlap), + libloot::EdgeType::AssetOverlap => Ok(EdgeType::assetOverlap), + libloot::EdgeType::TieBreak => Ok(EdgeType::tieBreak), + libloot::EdgeType::BlueprintMaster => Ok(EdgeType::blueprintMaster), + _ => Err(UnsupportedEnumValueError), + } + } +} + +impl TryFrom for libloot::EdgeType { + type Error = UnsupportedEnumValueError; + + fn try_from(value: EdgeType) -> Result { + match value { + EdgeType::hardcoded => Ok(libloot::EdgeType::Hardcoded), + EdgeType::masterFlag => Ok(libloot::EdgeType::MasterFlag), + EdgeType::master => Ok(libloot::EdgeType::Master), + EdgeType::masterlistRequirement => Ok(libloot::EdgeType::MasterlistRequirement), + EdgeType::userRequirement => Ok(libloot::EdgeType::UserRequirement), + EdgeType::masterlistLoadAfter => Ok(libloot::EdgeType::MasterlistLoadAfter), + EdgeType::userLoadAfter => Ok(libloot::EdgeType::UserLoadAfter), + EdgeType::masterlistGroup => Ok(libloot::EdgeType::MasterlistGroup), + EdgeType::userGroup => Ok(libloot::EdgeType::UserGroup), + EdgeType::recordOverlap => Ok(libloot::EdgeType::RecordOverlap), + EdgeType::assetOverlap => Ok(libloot::EdgeType::AssetOverlap), + EdgeType::tieBreak => Ok(libloot::EdgeType::TieBreak), + EdgeType::blueprintMaster => Ok(libloot::EdgeType::BlueprintMaster), + _ => Err(UnsupportedEnumValueError), + } + } +} diff --git a/cxx/src/game.rs b/cxx/src/game.rs new file mode 100644 index 00000000..147ebfa7 --- /dev/null +++ b/cxx/src/game.rs @@ -0,0 +1,196 @@ +use std::path::Path; + +use delegate::delegate; + +use crate::{ + PluginRef, UnsupportedEnumValueError, VerboseError, database::Database, ffi::GameType, + plugin::OptionalPluginRef, +}; + +impl TryFrom for GameType { + type Error = UnsupportedEnumValueError; + + fn try_from(value: libloot::GameType) -> Result { + match value { + libloot::GameType::TES4 => Ok(GameType::tes4), + libloot::GameType::TES5 => Ok(GameType::tes5), + libloot::GameType::FO3 => Ok(GameType::fo3), + libloot::GameType::FONV => Ok(GameType::fonv), + libloot::GameType::FO4 => Ok(GameType::fo4), + libloot::GameType::TES5SE => Ok(GameType::tes5se), + libloot::GameType::FO4VR => Ok(GameType::fo4vr), + libloot::GameType::TES5VR => Ok(GameType::tes5vr), + libloot::GameType::TES3 => Ok(GameType::tes3), + libloot::GameType::Starfield => Ok(GameType::starfield), + libloot::GameType::OpenMW => Ok(GameType::openmw), + _ => Err(UnsupportedEnumValueError), + } + } +} + +impl TryFrom for libloot::GameType { + type Error = UnsupportedEnumValueError; + + fn try_from(value: GameType) -> Result { + match value { + GameType::tes4 => Ok(libloot::GameType::TES4), + GameType::tes5 => Ok(libloot::GameType::TES5), + GameType::fo3 => Ok(libloot::GameType::FO3), + GameType::fonv => Ok(libloot::GameType::FONV), + GameType::fo4 => Ok(libloot::GameType::FO4), + GameType::tes5se => Ok(libloot::GameType::TES5SE), + GameType::fo4vr => Ok(libloot::GameType::FO4VR), + GameType::tes5vr => Ok(libloot::GameType::TES5VR), + GameType::tes3 => Ok(libloot::GameType::TES3), + GameType::starfield => Ok(libloot::GameType::Starfield), + GameType::openmw => Ok(libloot::GameType::OpenMW), + _ => Err(UnsupportedEnumValueError), + } + } +} + +impl From for libloot::Game { + fn from(value: Game) -> Self { + value.0 + } +} + +impl From for Game { + fn from(value: libloot::Game) -> Self { + Game(value) + } +} + +#[derive(Debug)] +pub struct NotValidUtf8; + +impl std::fmt::Display for NotValidUtf8 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Value was not valid UTF-8") + } +} + +impl std::error::Error for NotValidUtf8 {} + +#[derive(Debug)] +#[repr(transparent)] +pub struct Game(libloot::Game); + +// CXX doesn't support &Path so use &str instead. +pub fn new_game(game_type: GameType, game_path: &str) -> Result, VerboseError> { + libloot::Game::new(game_type.try_into()?, Path::new(game_path)) + .map(|g| Box::new(g.into())) + .map_err(Into::into) +} + +pub fn new_game_with_local_path( + game_type: GameType, + game_path: &str, + game_local_path: &str, +) -> Result, VerboseError> { + libloot::Game::with_local_path( + game_type.try_into()?, + Path::new(game_path), + Path::new(game_local_path), + ) + .map(|g| Box::new(g.into())) + .map_err(Into::into) +} + +fn path_to_string(path: &Path) -> Result { + path.to_str() + .map(|s| s.to_owned()) + .ok_or(NotValidUtf8) + .map_err(Into::into) +} + +fn strings_to_paths<'a>(paths: &'a [&'a str]) -> Vec<&'a Path> { + paths.iter().map(Path::new).collect() +} + +impl Game { + pub fn game_type(&self) -> Result { + self.0.game_type().try_into().map_err(Into::into) + } + + pub fn additional_data_paths(&self) -> Result, VerboseError> { + self.0 + .additional_data_paths() + .iter() + .map(|p| path_to_string(p)) + .collect() + } + + pub fn set_additional_data_paths( + &mut self, + additional_data_paths: &[&str], + ) -> Result<(), VerboseError> { + self.0 + .set_additional_data_paths(&strings_to_paths(additional_data_paths)) + .map_err(Into::into) + } + + pub fn database(&self) -> Box { + Box::new(Database::new(self.0.database())) + } + + pub fn is_valid_plugin(&self, plugin_path: &str) -> bool { + self.0.is_valid_plugin(Path::new(plugin_path)) + } + + pub fn load_plugins(&mut self, plugin_paths: &[&str]) -> Result<(), VerboseError> { + self.0 + .load_plugins(&strings_to_paths(plugin_paths)) + .map_err(Into::into) + } + + pub fn load_plugin_headers(&mut self, plugin_paths: &[&str]) -> Result<(), VerboseError> { + self.0 + .load_plugin_headers(&strings_to_paths(plugin_paths)) + .map_err(Into::into) + } + + pub fn plugin(&self, plugin_name: &str) -> Box { + Box::new(self.0.plugin(plugin_name).into()) + } + + pub fn loaded_plugins(&self) -> Vec> { + self.0 + .loaded_plugins() + .into_iter() + .map(Into::into) + .collect() + } + + pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result, VerboseError> { + self.0.sort_plugins(plugin_names).map_err(Into::into) + } + + pub fn load_current_load_order_state(&mut self) -> Result<(), VerboseError> { + self.0.load_current_load_order_state().map_err(Into::into) + } + + pub fn is_load_order_ambiguous(&self) -> Result { + self.0.is_load_order_ambiguous().map_err(Into::into) + } + + pub fn active_plugins_file_path(&self) -> Result { + path_to_string(self.0.active_plugins_file_path()) + } + + pub fn load_order(&self) -> Vec { + self.0.load_order().iter().map(|s| s.to_string()).collect() + } + + pub fn set_load_order(&mut self, load_order: &[&str]) -> Result<(), VerboseError> { + self.0.set_load_order(load_order).map_err(Into::into) + } + + delegate! { + to self.0 { + pub fn clear_loaded_plugins(&mut self); + + pub fn is_plugin_active(&self, plugin_name: &str) -> bool; + } + } +} diff --git a/cxx/src/lib.rs b/cxx/src/lib.rs new file mode 100644 index 00000000..9e3091eb --- /dev/null +++ b/cxx/src/lib.rs @@ -0,0 +1,569 @@ +mod database; +mod game; +mod metadata; +mod plugin; + +use database::{Database, Vertex, new_vertex}; +use game::{Game, new_game, new_game_with_local_path}; +use metadata::{ + File, Filename, Group, Location, Message, MessageContent, OptionalMessageContentRef, + OptionalPluginMetadata, PluginCleaningData, PluginMetadata, Tag, group_default_name, + message_content_default_language, multilingual_message, new_file, new_filename, new_group, + new_location, new_message, new_message_content, new_plugin_cleaning_data, new_plugin_metadata, + new_tag, select_message_content, +}; +use plugin::{OptionalPluginRef, PluginRef}; +use std::ffi::{CString, c_char, c_uint}; + +use libloot::set_logging_callback; +pub use libloot::{is_compatible, libloot_revision, libloot_version}; + +#[derive(Debug)] +pub struct VerboseError(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; + } + Ok(()) + } +} + +impl From for VerboseError { + fn from(value: E) -> Self { + VerboseError(Box::new(value)) + } +} + +#[derive(Debug)] +pub struct OptionalRef(*const T); + +impl OptionalRef { + pub fn is_some(&self) -> bool { + !self.0.is_null() + } +} + +impl From> for OptionalRef { + fn from(value: Option<&T>) -> Self { + match value { + Some(p) => OptionalRef(p), + None => OptionalRef(std::ptr::null()), + } + } +} + +#[derive(Debug)] +pub struct Optional(Option); + +impl Optional { + pub fn is_some(&self) -> bool { + self.0.is_some() + } + + pub fn as_ref(&self) -> Result<&T, EmptyOptionalError> { + match &self.0 { + Some(t) => Ok(t), + None => Err(EmptyOptionalError), + } + } +} + +#[derive(Debug)] +pub struct EmptyOptionalError; + +impl std::fmt::Display for EmptyOptionalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "optional is empty") + } +} + +impl std::error::Error for EmptyOptionalError {} + +#[derive(Debug)] +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 {} + +#[allow(clippy::needless_lifetimes)] +#[cxx::bridge(namespace = "loot")] +mod ffi { + + pub enum GameType { + tes4, + tes5, + fo3, + fonv, + fo4, + tes5se, + fo4vr, + tes5vr, + tes3, + starfield, + openmw, + } + + pub enum MessageType { + say, + warn, + error, + } + + pub enum TagSuggestion { + Addition, + Removal, + } + + pub enum EdgeType { + hardcoded, + masterFlag, + master, + masterlistRequirement, + userRequirement, + masterlistLoadAfter, + userLoadAfter, + masterlistGroup, + userGroup, + recordOverlap, + assetOverlap, + tieBreak, + blueprintMaster, + } + + extern "Rust" { + fn is_compatible(major: u32, minor: u32, patch: u32) -> bool; + fn libloot_version() -> String; + fn libloot_revision() -> String; + + fn select_message_content( + contents: &[MessageContent], + language: &str, + ) -> Box; + } + + extern "Rust" { + type Game; + + fn new_game(game_type: GameType, game_path: &str) -> Result>; + + fn new_game_with_local_path( + game_type: GameType, + game_path: &str, + game_local_path: &str, + ) -> Result>; + + pub fn game_type(&self) -> Result; + + pub fn additional_data_paths(&self) -> Result>; + + pub fn set_additional_data_paths(&mut self, additional_data_paths: &[&str]) -> Result<()>; + + pub fn database(&self) -> Box; + + pub fn is_valid_plugin(&self, plugin_path: &str) -> bool; + + pub fn load_plugins(&mut self, plugin_paths: &[&str]) -> Result<()>; + + pub fn load_plugin_headers(&mut self, plugin_paths: &[&str]) -> Result<()>; + + pub fn clear_loaded_plugins(&mut self); + + // The plugin's lifetime is actually less than &Game's, it's only valid so long as the loaded plugin is not overwritten or cleared. + pub fn plugin(&self, plugin_name: &str) -> Box; + + // The plugin's lifetime is actually less than &Game's, it's only valid so long as the loaded plugin is not overwritten or cleared. + pub unsafe fn loaded_plugins<'a>(&'a self) -> Vec>; + + pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result>; + + pub fn load_current_load_order_state(&mut self) -> Result<()>; + + pub fn is_load_order_ambiguous(&self) -> Result; + + pub fn active_plugins_file_path(&self) -> Result; + + pub fn is_plugin_active(&self, plugin_name: &str) -> bool; + + pub fn load_order(&self) -> Vec; + + pub fn set_load_order(&mut self, load_order: &[&str]) -> Result<()>; + } + + extern "Rust" { + type Database; + + pub fn load_masterlist(&self, path: &str) -> Result<()>; + + pub fn load_masterlist_with_prelude( + &self, + masterlist_path: &str, + prelude_path: &str, + ) -> Result<()>; + + pub fn load_userlist(&self, path: &str) -> Result<()>; + + pub fn write_user_metadata(&self, output_path: &str, overwrite: bool) -> Result<()>; + + pub fn write_minimal_list(&self, output_path: &str, overwrite: bool) -> Result<()>; + + pub fn known_bash_tags(&self) -> Result>; + + pub fn general_messages(&self, evaluate_conditions: bool) -> Result>; + + pub fn groups(&self, include_user_metadata: bool) -> Result>; + + pub fn user_groups(&self) -> Result>; + + pub fn set_user_groups(&self, groups: &[Box]) -> Result<()>; + + pub fn groups_path( + &self, + from_group_name: &str, + to_group_name: &str, + ) -> Result>; + + pub fn plugin_metadata( + &self, + plugin_name: &str, + include_user_metadata: bool, + evaluate_conditions: bool, + ) -> Result>; + + pub fn plugin_user_metadata( + &self, + plugin_name: &str, + evaluate_conditions: bool, + ) -> Result>; + + pub fn set_plugin_user_metadata( + &mut self, + plugin_metadata: Box, + ) -> Result<()>; + + pub fn discard_plugin_user_metadata(&self, plugin: &str) -> Result<()>; + + pub fn discard_all_user_metadata(&self) -> Result<()>; + } + + extern "Rust" { + type Message; + + pub fn new_message(message_type: MessageType, content: String) -> Result>; + + pub fn multilingual_message( + message_type: MessageType, + contents: &[Box], + ) -> Result>; + + pub fn message_type(&self) -> MessageType; + + pub fn content(&self) -> &[MessageContent]; + + pub fn condition(&self) -> &str; + + pub fn set_condition(&mut self, condition: String); + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type MessageContent; + + pub fn message_content_default_language() -> &'static str; + + pub fn new_message_content(text: String) -> Box; + + pub fn text(&self) -> &str; + + pub fn language(&self) -> &str; + + pub fn set_language(&mut self, language: String); + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type Group; + + pub fn new_group(name: String) -> Box; + + pub fn group_default_name() -> &'static str; + + pub fn boxed_clone(&self) -> Box; + + pub fn name(&self) -> &str; + + pub fn description(&self) -> &str; + + pub fn set_description(&mut self, description: String); + + pub fn after_groups(&self) -> &[String]; + + pub fn set_after_groups(&mut self, groups: Vec); + } + + extern "Rust" { + type PluginRef<'a>; + + pub unsafe fn name<'a>(&'a self) -> &'a str; + + // The None case is signalled by NaN. + pub fn header_version(&self) -> f32; + + // The None case is signalled by an empty string (which is not a valid version). + pub unsafe fn version<'a>(&'a self) -> &'a str; + + pub fn masters(&self) -> Result>; + + pub unsafe fn bash_tags<'a>(&'a self) -> &'a [String]; + + // The None case is signalled by -1, all other values fit in u32. + pub fn crc(&self) -> i64; + + pub fn is_master(&self) -> bool; + + pub fn is_light_plugin(&self) -> bool; + + pub fn is_medium_plugin(&self) -> bool; + + pub fn is_update_plugin(&self) -> bool; + + pub fn is_blueprint_plugin(&self) -> bool; + + pub fn is_valid_as_light_plugin(&self) -> Result; + + pub fn is_valid_as_medium_plugin(&self) -> Result; + + pub fn is_valid_as_update_plugin(&self) -> Result; + + pub fn is_empty(&self) -> bool; + + pub fn loads_archive(&self) -> bool; + + pub unsafe fn do_records_overlap<'a>(&self, plugin: &PluginRef<'a>) -> Result; + } + + extern "Rust" { + type OptionalPluginRef; + + pub fn is_some(&self) -> bool; + + // Again, these lifetimes are wrong. + pub unsafe fn as_ref<'a>(&'a self) -> Result>>; + } + + extern "Rust" { + type OptionalMessageContentRef; + + pub fn is_some(&self) -> bool; + + // Again, these lifetimes are wrong. + pub unsafe fn as_ref<'a>(&'a self) -> Result<&'a MessageContent>; + } + + extern "Rust" { + type Vertex; + + pub fn new_vertex(name: String) -> Box; + + pub fn name(&self) -> &str; + + pub fn out_edge_type(&self) -> Result; + + pub fn set_out_edge_type(&mut self, #[into] out_edge_type: EdgeType) -> Result<()>; + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type OptionalPluginMetadata; + + pub fn is_some(&self) -> bool; + + // Again, these lifetimes are wrong. + pub unsafe fn as_ref<'a>(&'a self) -> Result<&'a PluginMetadata>; + } + + extern "Rust" { + type PluginMetadata; + + pub fn new_plugin_metadata(name: &str) -> Result>; + + pub fn name(&self) -> &str; + + pub fn group(&self) -> &str; + + pub fn load_after_files(&self) -> &[File]; + + pub fn requirements(&self) -> &[File]; + + pub fn incompatibilities(&self) -> &[File]; + + pub fn messages(&self) -> &[Message]; + + pub fn tags(&self) -> &[Tag]; + + pub fn dirty_info(&self) -> &[PluginCleaningData]; + + pub fn clean_info(&self) -> &[PluginCleaningData]; + + pub fn locations(&self) -> &[Location]; + + pub fn set_group(&mut self, group: String); + + pub fn unset_group(&mut self); + + pub fn set_load_after_files(&mut self, files: &[Box]); + + pub fn set_requirements(&mut self, files: &[Box]); + + pub fn set_incompatibilities(&mut self, files: &[Box]); + + pub fn set_messages(&mut self, messages: &[Box]); + + pub fn set_tags(&mut self, tags: &[Box]); + + pub fn set_dirty_info(&mut self, info: &[Box]); + + pub fn set_clean_info(&mut self, info: &[Box]); + + pub fn set_locations(&mut self, locations: &[Box]); + + pub fn merge_metadata(&mut self, plugin: &PluginMetadata); + + pub fn has_name_only(&self) -> bool; + + pub fn is_regex_plugin(&self) -> bool; + + pub fn name_matches(&self, other_name: &str) -> bool; + + pub fn as_yaml(&self) -> String; + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type File; + + pub fn new_file(name: String) -> Box; + + pub fn filename(&self) -> &Filename; + + pub fn display_name(&self) -> &str; + + pub fn set_display_name(&mut self, display_name: String); + + pub fn detail(&self) -> &[MessageContent]; + + pub fn set_detail(&mut self, detail: &[Box]) -> Result<()>; + + pub fn condition(&self) -> &str; + + pub fn set_condition(&mut self, condition: String); + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type Filename; + + pub fn new_filename(name: String) -> Box; + + pub fn as_str(&self) -> &str; + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type Tag; + + pub fn new_tag(name: String, suggestion: TagSuggestion) -> Result>; + + pub fn name(&self) -> &str; + + pub fn is_addition(&self) -> bool; + + pub fn condition(&self) -> &str; + + pub fn set_condition(&mut self, condition: String); + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type PluginCleaningData; + + pub fn new_plugin_cleaning_data( + crc: u32, + cleaning_utility: String, + ) -> Box; + + pub fn crc(&self) -> u32; + + pub fn itm_count(&self) -> u32; + + pub fn set_itm_count(&mut self, count: u32); + + pub fn deleted_reference_count(&self) -> u32; + + pub fn set_deleted_reference_count(&mut self, count: u32); + + pub fn deleted_navmesh_count(&self) -> u32; + + pub fn set_deleted_navmesh_count(&mut self, count: u32); + + pub fn cleaning_utility(&self) -> &str; + + pub fn detail(&self) -> &[MessageContent]; + + pub fn set_detail(&mut self, detail: &[Box]) -> Result<()>; + + pub fn boxed_clone(&self) -> Box; + } + + extern "Rust" { + type Location; + + pub fn new_location(url: String) -> Box; + + pub fn url(&self) -> &str; + + pub fn name(&self) -> &str; + + pub fn set_name(&mut self, name: String); + + pub fn boxed_clone(&self) -> Box; + } +} + +#[unsafe(no_mangle)] +pub static LIBLOOT_VERSION_MAJOR: c_uint = libloot::LIBLOOT_VERSION_MAJOR; + +#[unsafe(no_mangle)] +pub static LIBLOOT_VERSION_MINOR: c_uint = libloot::LIBLOOT_VERSION_MINOR; + +#[unsafe(no_mangle)] +pub static LIBLOOT_VERSION_PATCH: c_uint = libloot::LIBLOOT_VERSION_PATCH; + +// TODO: Implement this properly. +#[unsafe(no_mangle)] +unsafe extern "C" fn libloot_set_logging_callback( + callback: unsafe extern "C" fn(u8, *const c_char), +) { + set_logging_callback(move |level, message| { + let c_string = CString::new(message).unwrap(); + unsafe { + callback(level as u8, c_string.as_ptr()); + } + }); +} diff --git a/cxx/src/metadata.rs b/cxx/src/metadata.rs new file mode 100644 index 00000000..fe228775 --- /dev/null +++ b/cxx/src/metadata.rs @@ -0,0 +1,592 @@ +use delegate::delegate; + +use crate::{ + EmptyOptionalError, Optional, OptionalRef, UnsupportedEnumValueError, VerboseError, + ffi::{MessageType, TagSuggestion}, +}; + +impl From for MessageType { + fn from(value: libloot::metadata::MessageType) -> Self { + match value { + libloot::metadata::MessageType::Say => MessageType::say, + libloot::metadata::MessageType::Warn => MessageType::warn, + libloot::metadata::MessageType::Error => MessageType::error, + } + } +} + +impl TryFrom for libloot::metadata::MessageType { + type Error = UnsupportedEnumValueError; + + fn try_from(value: MessageType) -> Result { + match value { + MessageType::say => Ok(libloot::metadata::MessageType::Say), + MessageType::warn => Ok(libloot::metadata::MessageType::Warn), + MessageType::error => Ok(libloot::metadata::MessageType::Error), + _ => Err(UnsupportedEnumValueError), + } + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct MessageContent(libloot::metadata::MessageContent); + +pub fn new_message_content(text: String) -> Box { + Box::new(MessageContent(libloot::metadata::MessageContent::new(text))) +} + +pub fn message_content_default_language() -> &'static str { + libloot::metadata::MessageContent::DEFAULT_LANGUAGE +} + +impl MessageContent { + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn text(&self) -> &str; + + pub fn language(&self) -> &str; + + pub fn set_language(&mut self, language: String); + } + } +} + +impl From for libloot::metadata::MessageContent { + fn from(value: MessageContent) -> Self { + value.0 + } +} + +impl From> for libloot::metadata::MessageContent { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_message_content_ref(unwrapped: &libloot::metadata::MessageContent) -> &MessageContent { + let v = unwrapped as *const libloot::metadata::MessageContent; + // SAFETY: MessageContent is a transparent wrapper, so reinterpreting the pointer is safe. + unsafe { + let v = v as *const MessageContent; + &*v + } +} + +fn wrap_message_contents(slice: &[libloot::metadata::MessageContent]) -> &[MessageContent] { + // SAFETY: This is safe because MessageContent is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +fn unwrap_message_contents(slice: &[MessageContent]) -> &[libloot::metadata::MessageContent] { + // SAFETY: This is safe because MessageContent is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +pub type OptionalMessageContentRef = OptionalRef; + +impl OptionalRef { + /// # Safety + /// + /// This is safe as long as the pointer in the OptionalRef is still valid. + pub unsafe fn as_ref(&self) -> Result<&MessageContent, EmptyOptionalError> { + if self.0.is_null() { + Err(EmptyOptionalError) + } else { + unsafe { Ok(&*self.0) } + } + } +} + +pub fn select_message_content( + contents: &[MessageContent], + language: &str, +) -> Box { + let option = + libloot::metadata::select_message_content(unwrap_message_contents(contents), language); + + Box::new(option.map(wrap_message_content_ref).into()) +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Message(libloot::metadata::Message); + +pub fn new_message( + message_type: MessageType, + content: String, +) -> Result, VerboseError> { + Ok(Box::new(Message(libloot::metadata::Message::new( + message_type.try_into()?, + content, + )))) +} + +pub fn multilingual_message( + message_type: MessageType, + contents: &[Box], +) -> Result, VerboseError> { + let contents = to_vec_of_unwrapped(contents); + Ok(Box::new(Message(libloot::metadata::Message::multilingual( + message_type.try_into()?, + contents, + )?))) +} + +impl Message { + pub fn condition(&self) -> &str { + self.0.condition().unwrap_or("") + } + + pub fn content(&self) -> &[MessageContent] { + wrap_message_contents(self.0.content()) + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + #[into] + pub fn message_type(&self) -> MessageType; + + pub fn set_condition(&mut self, condition: String); + } + } +} + +impl From for Message { + fn from(value: libloot::metadata::Message) -> Self { + Self(value) + } +} + +impl From> for libloot::metadata::Message { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_messages(slice: &[libloot::metadata::Message]) -> &[Message] { + // SAFETY: This is safe because Message is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Group(libloot::metadata::Group); + +pub fn new_group(name: String) -> Box { + Box::new(Group(libloot::metadata::Group::new(name))) +} + +pub fn group_default_name() -> &'static str { + libloot::metadata::Group::DEFAULT_NAME +} + +impl Group { + pub fn description(&self) -> &str { + self.0.description().unwrap_or("") + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn name(&self) -> &str; + + pub fn set_description(&mut self, description: String); + + pub fn after_groups(&self) -> &[String]; + + pub fn set_after_groups(&mut self, groups: Vec); + } + } +} + +impl From for Group { + fn from(value: libloot::metadata::Group) -> Self { + Self(value) + } +} + +impl From for libloot::metadata::Group { + fn from(value: Group) -> Self { + value.0 + } +} + +impl From> for libloot::metadata::Group { + fn from(value: Box) -> Self { + value.0 + } +} + +impl From> for Group { + fn from(value: Box) -> Self { + Self((*value).into()) + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct PluginMetadata(libloot::metadata::PluginMetadata); + +pub fn new_plugin_metadata(name: &str) -> Result, VerboseError> { + Ok(Box::new(PluginMetadata( + libloot::metadata::PluginMetadata::new(name)?, + ))) +} + +impl PluginMetadata { + pub fn group(&self) -> &str { + self.0.group().unwrap_or("") + } + + pub fn load_after_files(&self) -> &[File] { + wrap_files(self.0.load_after_files()) + } + + pub fn requirements(&self) -> &[File] { + wrap_files(self.0.requirements()) + } + + pub fn incompatibilities(&self) -> &[File] { + wrap_files(self.0.incompatibilities()) + } + + pub fn messages(&self) -> &[Message] { + wrap_messages(self.0.messages()) + } + + pub fn tags(&self) -> &[Tag] { + wrap_tags(self.0.tags()) + } + + pub fn dirty_info(&self) -> &[PluginCleaningData] { + wrap_cleaning_data(self.0.dirty_info()) + } + + pub fn clean_info(&self) -> &[PluginCleaningData] { + wrap_cleaning_data(self.0.clean_info()) + } + + pub fn locations(&self) -> &[Location] { + wrap_locations(self.0.locations()) + } + + pub fn set_load_after_files(&mut self, files: &[Box]) { + self.0.set_load_after_files(to_vec_of_unwrapped(files)); + } + + pub fn set_requirements(&mut self, files: &[Box]) { + self.0.set_requirements(to_vec_of_unwrapped(files)); + } + + pub fn set_incompatibilities(&mut self, files: &[Box]) { + self.0.set_incompatibilities(to_vec_of_unwrapped(files)); + } + + pub fn set_messages(&mut self, messages: &[Box]) { + self.0.set_messages(to_vec_of_unwrapped(messages)); + } + + pub fn set_tags(&mut self, tags: &[Box]) { + self.0.set_tags(to_vec_of_unwrapped(tags)); + } + + pub fn set_dirty_info(&mut self, info: &[Box]) { + self.0.set_dirty_info(to_vec_of_unwrapped(info)); + } + + pub fn set_clean_info(&mut self, info: &[Box]) { + self.0.set_clean_info(to_vec_of_unwrapped(info)); + } + + pub fn set_locations(&mut self, locations: &[Box]) { + self.0.set_locations(to_vec_of_unwrapped(locations)); + } + + pub fn merge_metadata(&mut self, plugin: &PluginMetadata) { + self.0.merge_metadata(&plugin.0); + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn name(&self) -> &str; + + pub fn set_group(&mut self, group: String); + + pub fn unset_group(&mut self); + + pub fn has_name_only(&self) -> bool; + + pub fn is_regex_plugin(&self) -> bool; + + pub fn name_matches(&self, other_name: &str) -> bool; + + pub fn as_yaml(&self) -> String; + } + } +} + +impl From for PluginMetadata { + fn from(value: libloot::metadata::PluginMetadata) -> Self { + Self(value) + } +} + +impl From> for libloot::metadata::PluginMetadata { + fn from(value: Box) -> Self { + value.0 + } +} + +pub type OptionalPluginMetadata = Optional; + +impl From> for Optional { + fn from(value: Option) -> Self { + Self(value) + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct File(libloot::metadata::File); + +pub fn new_file(name: String) -> Box { + Box::new(File(libloot::metadata::File::new(name))) +} + +impl File { + pub fn filename(&self) -> &Filename { + let f = self.0.name() as *const libloot::metadata::Filename; + // SAFETY: Filename is a transparent wrapper, so reinterpreting the pointer is safe. + unsafe { + let f = f as *const Filename; + &*f + } + } + + pub fn display_name(&self) -> &str { + self.0.display_name().unwrap_or("") + } + + pub fn set_display_name(&mut self, display_name: String) { + self.0.set_display_name(display_name); + } + + pub fn detail(&self) -> &[MessageContent] { + wrap_message_contents(self.0.detail()) + } + + pub fn set_detail(&mut self, detail: &[Box]) -> Result<(), VerboseError> { + self.0.set_detail(to_vec_of_unwrapped(detail))?; + Ok(()) + } + + pub fn condition(&self) -> &str { + self.0.condition().unwrap_or("") + } + + pub fn set_condition(&mut self, condition: String) { + self.0.set_condition(condition); + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } +} + +impl From> for libloot::metadata::File { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_files(slice: &[libloot::metadata::File]) -> &[File] { + // SAFETY: This is safe because File is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Filename(libloot::metadata::Filename); + +pub fn new_filename(name: String) -> Box { + Box::new(Filename(libloot::metadata::Filename::new(name))) +} + +impl Filename { + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn as_str(&self) -> &str; + } + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Tag(libloot::metadata::Tag); + +pub fn new_tag(name: String, suggestion: TagSuggestion) -> Result, VerboseError> { + Ok(Box::new(Tag(libloot::metadata::Tag::new( + name, + suggestion.try_into()?, + )))) +} + +impl Tag { + pub fn condition(&self) -> &str { + self.0.condition().unwrap_or("") + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn name(&self) -> &str; + + pub fn is_addition(&self) -> bool; + + pub fn set_condition(&mut self, condition: String); + } + } +} + +impl From> for libloot::metadata::Tag { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_tags(slice: &[libloot::metadata::Tag]) -> &[Tag] { + // SAFETY: This is safe because Tag is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +impl TryFrom for libloot::metadata::TagSuggestion { + type Error = UnsupportedEnumValueError; + + fn try_from(value: TagSuggestion) -> Result { + match value { + TagSuggestion::Addition => Ok(libloot::metadata::TagSuggestion::Addition), + TagSuggestion::Removal => Ok(libloot::metadata::TagSuggestion::Removal), + _ => Err(UnsupportedEnumValueError), + } + } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct PluginCleaningData(libloot::metadata::PluginCleaningData); + +pub fn new_plugin_cleaning_data(crc: u32, cleaning_utility: String) -> Box { + Box::new(PluginCleaningData( + libloot::metadata::PluginCleaningData::new(crc, cleaning_utility), + )) +} + +impl PluginCleaningData { + pub fn detail(&self) -> &[MessageContent] { + wrap_message_contents(self.0.detail()) + } + + pub fn set_detail(&mut self, detail: &[Box]) -> Result<(), VerboseError> { + self.0.set_detail(to_vec_of_unwrapped(detail))?; + Ok(()) + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn crc(&self) -> u32; + + pub fn itm_count(&self) -> u32; + + pub fn set_itm_count(&mut self, count: u32); + + pub fn deleted_reference_count(&self) -> u32; + + pub fn set_deleted_reference_count(&mut self, count: u32); + + pub fn deleted_navmesh_count(&self) -> u32; + + pub fn set_deleted_navmesh_count(&mut self, count: u32); + + pub fn cleaning_utility(&self) -> &str; + } + } +} + +impl From> for libloot::metadata::PluginCleaningData { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_cleaning_data(slice: &[libloot::metadata::PluginCleaningData]) -> &[PluginCleaningData] { + // SAFETY: This is safe because PluginCleaningData is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Location(libloot::metadata::Location); + +pub fn new_location(url: String) -> Box { + Box::new(Location(libloot::metadata::Location::new(url))) +} + +impl Location { + pub fn name(&self) -> &str { + self.0.name().unwrap_or("") + } + + pub fn boxed_clone(&self) -> Box { + Box::new(Self(self.0.clone())) + } + + delegate! { + to self.0 { + pub fn url(&self) -> &str; + + pub fn set_name(&mut self, name: String); + } + } +} + +impl From> for libloot::metadata::Location { + fn from(value: Box) -> Self { + value.0 + } +} + +fn wrap_locations(slice: &[libloot::metadata::Location]) -> &[Location] { + // SAFETY: This is safe because Location is a transparent wrapper around the libloot type. + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) } +} + +pub fn to_vec_of_unwrapped>>(slice: &[Box]) -> Vec { + slice.iter().cloned().map(Into::into).collect() +} diff --git a/cxx/src/plugin.rs b/cxx/src/plugin.rs new file mode 100644 index 00000000..4f769186 --- /dev/null +++ b/cxx/src/plugin.rs @@ -0,0 +1,88 @@ +use delegate::delegate; + +use crate::{EmptyOptionalError, OptionalRef, VerboseError}; + +#[derive(Debug)] +#[repr(transparent)] +pub struct PluginRef<'a>(&'a libloot::Plugin); + +impl<'a> PluginRef<'a> { + pub fn new(plugin: &'a libloot::Plugin) -> Self { + Self(plugin) + } + + pub fn header_version(&self) -> f32 { + self.0.header_version().unwrap_or(f32::NAN) + } + + pub fn version(&self) -> &str { + self.0.version().unwrap_or("") + } + + pub fn masters(&self) -> Result, VerboseError> { + self.0.masters().map_err(Into::into) + } + + pub fn crc(&self) -> i64 { + self.0.crc().map(Into::into).unwrap_or(-1) + } + + pub fn is_valid_as_light_plugin(&self) -> Result { + self.0.is_valid_as_light_plugin().map_err(Into::into) + } + + pub fn is_valid_as_medium_plugin(&self) -> Result { + self.0.is_valid_as_medium_plugin().map_err(Into::into) + } + + pub fn is_valid_as_update_plugin(&self) -> Result { + self.0.is_valid_as_update_plugin().map_err(Into::into) + } + + pub fn do_records_overlap(&self, plugin: &Self) -> Result { + self.0.do_records_overlap(plugin.0).map_err(Into::into) + } + + delegate! { + to self.0 { + pub fn name(&self) -> &str; + + pub fn bash_tags(&self) -> &[String]; + + pub fn is_master(&self) -> bool; + + pub fn is_light_plugin(&self) -> bool; + + pub fn is_medium_plugin(&self) -> bool; + + pub fn is_update_plugin(&self) -> bool; + + pub fn is_blueprint_plugin(&self) -> bool; + + pub fn is_empty(&self) -> bool; + + pub fn loads_archive(&self) -> bool; + } + } +} + +impl<'a> From<&'a libloot::Plugin> for PluginRef<'a> { + fn from(value: &'a libloot::Plugin) -> Self { + PluginRef(value) + } +} + +pub type OptionalPluginRef = OptionalRef; + +impl OptionalRef { + /// # Safety + /// + /// This is safe as long as the pointer in the OptionalRef is still valid. + pub unsafe fn as_ref(&self) -> Result>, EmptyOptionalError> { + if self.0.is_null() { + Err(EmptyOptionalError) + } else { + unsafe { Ok(Box::new(PluginRef::new(&*self.0))) } + } + } +}