From c0bea6067360fddcd0994d85cdbb80ead0dfb110 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sun, 7 Oct 2018 22:13:48 +0100 Subject: [PATCH] Add FFI wrapper crate --- .gitignore | 3 + .travis.yml | 13 ++- Cargo.toml | 4 + appveyor.yml | 13 ++- ffi/CMakeLists.txt | 22 +++++ ffi/Cargo.toml | 21 +++++ ffi/build.rs | 30 +++++++ ffi/src/constants.rs | 88 +++++++++++++++++++ ffi/src/helpers.rs | 97 +++++++++++++++++++++ ffi/src/lib.rs | 94 ++++++++++++++++++++ ffi/src/state.rs | 199 +++++++++++++++++++++++++++++++++++++++++++ ffi/tests/ffi.cpp | 197 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 37 ++++++-- 13 files changed, 808 insertions(+), 10 deletions(-) create mode 100644 ffi/CMakeLists.txt create mode 100644 ffi/Cargo.toml create mode 100644 ffi/build.rs create mode 100644 ffi/src/constants.rs create mode 100644 ffi/src/helpers.rs create mode 100644 ffi/src/lib.rs create mode 100644 ffi/src/state.rs create mode 100644 ffi/tests/ffi.cpp diff --git a/.gitignore b/.gitignore index a68598c..1eb7c99 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ Cargo.lock /testing-plugins /loot_api* +/ffi/include/ +/ffi/build/ +.vscode diff --git a/.travis.yml b/.travis.yml index e963ef4..1e293cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,5 +23,14 @@ before_script: - 7z x loot_api-0.13.8-0-g47797cc_dev-win64.7z script: - - cargo fmt -- --check - - cargo test + - cargo fmt --all -- --check + - cargo test --all --all-features + + # Need to rebuild the FFI wrapper so that its binary is given a filename + # without a hash. + - cargo build --manifest-path ffi/Cargo.toml --features ffi-headers + - mkdir ffi/build + - cd ffi/build + - cmake .. + - make + - make test diff --git a/Cargo.toml b/Cargo.toml index 83c3fe6..b5a8004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "loot-condition-interpreter" version = "0.1.0" authors = ["Oliver Hamlet "] +license = "MIT" [dependencies] crc = "1.0.0" @@ -17,3 +18,6 @@ tempfile = "3.0.0" [[bench]] name = "eval" harness = false + +[workspace] +members = ["ffi"] diff --git a/appveyor.yml b/appveyor.yml index 3532e73..c9b949c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,5 +29,14 @@ install: build: false test_script: - - cargo fmt -- --check - - cargo test + - cargo fmt --all -- --check + - cargo test --all --all-features + + # Need to rebuild the FFI wrapper so that its binary is given a filename + # without a hash. + - cargo build --manifest-path ffi/Cargo.toml --features ffi-headers + - ps: mkdir ffi/build + - cd ffi/build + - cmake .. -G "Visual Studio 15 2017 Win64" + - cmake --build . + - ctest diff --git a/ffi/CMakeLists.txt b/ffi/CMakeLists.txt new file mode 100644 index 0000000..9a19547 --- /dev/null +++ b/ffi/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 2.8) +project(ffi_tests CXX) + +include_directories("${CMAKE_SOURCE_DIR}/include") + +set(CMAKE_CXX_STANDARD 11) + +if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set (SYSTEM_LIBS pthread dl) +endif () + +if (MSVC) + set (SYSTEM_LIBS ws2_32 Userenv) +endif () + +set (FFI_LIBRARY "${CMAKE_SOURCE_DIR}/../target/debug/${CMAKE_STATIC_LIBRARY_PREFIX}loot_condition_interpreter_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}") + +add_executable(ffi_cpp_tests "${CMAKE_SOURCE_DIR}/tests/ffi.cpp") +target_link_libraries(ffi_cpp_tests ${FFI_LIBRARY} ${SYSTEM_LIBS}) + +enable_testing() +add_test(ffi_cpp_tests ffi_cpp_tests) diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml new file mode 100644 index 0000000..261fab6 --- /dev/null +++ b/ffi/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "loot-condition-interpreter-ffi" +version = "0.1.0" +authors = ["Oliver Hamlet "] +license = "MIT" +build = "build.rs" + +[dependencies] +loot-condition-interpreter = { path = ".." } +libc = "0.2" + +[lib] +name = "loot_condition_interpreter_ffi" +crate-type = ["staticlib"] + +[features] +default = [] +ffi-headers = ["cbindgen"] + +[build-dependencies] +cbindgen = { version = "0.6", optional = true } diff --git a/ffi/build.rs b/ffi/build.rs new file mode 100644 index 0000000..0315e0b --- /dev/null +++ b/ffi/build.rs @@ -0,0 +1,30 @@ +#[cfg(feature = "ffi-headers")] +mod ffi_headers { + extern crate cbindgen; + + use std::env; + use std::fs; + + use self::cbindgen::Builder; + use self::cbindgen::Language; + + pub fn generate_headers() { + let crate_dir = env::var("CARGO_MANIFEST_DIR") + .expect("could not get value of CARGO_MANIFEST_DIR env var"); + + fs::create_dir_all("include").expect("could not create include directory"); + + Builder::new() + .with_crate(&crate_dir) + .with_language(Language::Cxx) + .with_std_types(false) + .generate() + .expect("could not generate C++ header file") + .write_to_file("include/loot_condition_interpreter.hpp"); + } +} + +fn main() { + #[cfg(feature = "ffi-headers")] + ffi_headers::generate_headers(); +} diff --git a/ffi/src/constants.rs b/ffi/src/constants.rs new file mode 100644 index 0000000..bbaca90 --- /dev/null +++ b/ffi/src/constants.rs @@ -0,0 +1,88 @@ +use libc::c_int; +use loot_condition_interpreter::GameType; + +#[no_mangle] +pub static LCI_OK: c_int = 0; + +#[no_mangle] +pub static LCI_RESULT_FALSE: c_int = 0; + +#[no_mangle] +pub static LCI_RESULT_TRUE: c_int = 1; + +/// Invalid arguments were given for the function. +#[no_mangle] +pub static LCI_ERROR_INVALID_ARGS: c_int = -1; + +/// Something went wrong while parsing the condition expression. +#[no_mangle] +pub static LCI_ERROR_PARSING_ERROR: c_int = -2; + +/// Something went wrong while getting the version of an executable. +#[no_mangle] +pub static LCI_ERROR_PE_PARSING_ERROR: c_int = -3; + +/// Some sort of I/O error occurred. +#[no_mangle] +pub static LCI_ERROR_IO_ERROR: c_int = -4; + +/// Something panicked. +#[no_mangle] +pub static LCI_ERROR_PANICKED: c_int = -5; + +/// A thread lock was poisoned. +#[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. +#[no_mangle] +pub static LCI_ERROR_TEXT_ENCODE_FAIL: c_int = -7; + +/// Game code for The Elder Scrolls IV: Oblivion. +#[no_mangle] +pub static LCI_GAME_TES4: c_int = GameType::Tes4 as c_int; + +/// Game code for The Elder Scrolls V: Skyrim. +#[no_mangle] +pub static LCI_GAME_TES5: c_int = GameType::Tes5 as c_int; + +/// Game code for Fallout 3. +#[no_mangle] +pub static LCI_GAME_FO3: c_int = GameType::Fo3 as c_int; + +/// Game code for Fallout: New Vegas. +#[no_mangle] +pub static LCI_GAME_FNV: c_int = GameType::Fonv as c_int; + +/// Game code for Fallout 4. +#[no_mangle] +pub static LCI_GAME_FO4: c_int = GameType::Fo4 as c_int; + +/// Game code for The Elder Scrolls V: Skyrim Special Edition. +#[no_mangle] +pub static LCI_GAME_TES5SE: c_int = GameType::Tes5se as c_int; + +/// Game code for The Elder Scrolls V: Skyrim VR. +#[no_mangle] +pub static LCI_GAME_TES5VR: c_int = GameType::Tes5vr as c_int; + +/// Game code for Fallout 4 VR. +#[no_mangle] +pub static LCI_GAME_FO4VR: c_int = GameType::Fo4vr as c_int; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn game_constants_should_have_expected_integer_values() { + assert_eq!(0, LCI_GAME_TES4); + assert_eq!(1, LCI_GAME_TES5); + assert_eq!(2, LCI_GAME_TES5SE); + assert_eq!(3, LCI_GAME_TES5VR); + assert_eq!(4, LCI_GAME_FO3); + assert_eq!(5, LCI_GAME_FNV); + assert_eq!(6, LCI_GAME_FO4); + assert_eq!(7, LCI_GAME_FO4VR); + } +} diff --git a/ffi/src/helpers.rs b/ffi/src/helpers.rs new file mode 100644 index 0000000..08f5759 --- /dev/null +++ b/ffi/src/helpers.rs @@ -0,0 +1,97 @@ +use std::ffi::{CStr, CString}; +use std::slice; + +use libc::{c_char, c_int, size_t}; +use loot_condition_interpreter::{Error, GameType}; + +use super::ERROR_MESSAGE; +use constants::*; +use state::{plugin_crc, plugin_version}; + +pub fn error(code: c_int, message: &str) -> c_int { + ERROR_MESSAGE.with(|f| { + *f.borrow_mut() = unsafe { CString::from_vec_unchecked(message.as_bytes().to_vec()) } + }); + code +} + +pub fn handle_error(err: Error) -> c_int { + let code = map_error(&err); + error(code, &format!("{}", err)) +} + +fn map_error(err: &Error) -> c_int { + match err { + Error::ParsingIncomplete => LCI_ERROR_PARSING_ERROR, + Error::GenericParsingError(_, _) => LCI_ERROR_PARSING_ERROR, + Error::CustomParsingError(_, _) => LCI_ERROR_PARSING_ERROR, + Error::PeParsingError(_, _) => LCI_ERROR_PE_PARSING_ERROR, + Error::IoError(_, _) => LCI_ERROR_IO_ERROR, + } +} + +pub fn map_game_type(game_type: c_int) -> Result { + match game_type { + x if x == LCI_GAME_TES4 => Ok(GameType::Tes4), + x if x == LCI_GAME_TES5 => Ok(GameType::Tes5), + x if x == LCI_GAME_TES5SE => Ok(GameType::Tes5se), + x if x == LCI_GAME_TES5VR => Ok(GameType::Tes5vr), + x if x == LCI_GAME_FO3 => Ok(GameType::Fo3), + x if x == LCI_GAME_FNV => Ok(GameType::Fonv), + x if x == LCI_GAME_FO4 => Ok(GameType::Fo4), + x if x == LCI_GAME_FO4VR => Ok(GameType::Fo4vr), + _ => Err(LCI_ERROR_INVALID_ARGS), + } +} + +pub unsafe fn to_str<'a>(c_string: *const c_char) -> Result<&'a str, c_int> { + if c_string.is_null() { + Err(error(LCI_ERROR_INVALID_ARGS, "Null pointer passed")) + } else { + CStr::from_ptr(c_string) + .to_str() + .map_err(|_| error(LCI_ERROR_INVALID_ARGS, "Non-UTF-8 string passed")) + } +} + +pub unsafe fn to_vec(array: *const U, array_size: size_t, mapper: F) -> Result, i32> +where + F: Fn(&U) -> Result, +{ + if array.is_null() || array_size == 0 { + Ok(Vec::new()) + } else { + slice::from_raw_parts(array, array_size) + .iter() + .map(|c| mapper(c)) + .collect() + } +} + +pub unsafe fn to_str_vec<'a>( + array: *const *const c_char, + array_size: size_t, +) -> Result, i32> { + to_vec(array, array_size, |c| to_str(*c)) +} + +unsafe fn map_plugin_version(c_object: &plugin_version) -> Result<(String, String), i32> { + to_str(c_object.plugin_name) + .and_then(|n| to_str(c_object.version).map(|v| (n.into(), v.into()))) +} + +pub unsafe fn map_plugin_versions( + plugin_versions: *const plugin_version, + num_plugins: size_t, +) -> Result, i32> { + to_vec(plugin_versions, num_plugins, |v| map_plugin_version(v)) +} + +pub unsafe fn map_plugin_crcs( + plugin_crcs: *const plugin_crc, + num_entries: size_t, +) -> Result, i32> { + to_vec(plugin_crcs, num_entries, |v| { + to_str(v.plugin_name).map(|s| (s.into(), v.crc)) + }) +} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs new file mode 100644 index 0000000..26eca71 --- /dev/null +++ b/ffi/src/lib.rs @@ -0,0 +1,94 @@ +extern crate libc; +extern crate loot_condition_interpreter; + +mod constants; +mod helpers; +mod state; + +use std::cell::RefCell; +use std::error::Error; +use std::ffi::CString; +use std::panic::catch_unwind; +use std::ptr; +use std::str::FromStr; + +use libc::{c_char, c_int}; +use loot_condition_interpreter::*; + +pub use constants::*; +use helpers::{error, handle_error, to_str}; +pub use state::*; + +thread_local!(static ERROR_MESSAGE: RefCell = RefCell::default()); + +#[no_mangle] +pub unsafe extern "C" fn lci_condition_parse(condition: *const c_char) -> c_int { + catch_unwind(|| { + if condition.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null pointer passed") + } else { + let expression = match to_str(condition) { + Ok(x) => x, + Err(e) => return e, + }; + + if let Err(e) = Expression::from_str(expression) { + handle_error(e) + } else { + LCI_OK + } + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_condition_eval( + condition: *const c_char, + state: *mut lci_state, +) -> c_int { + catch_unwind(|| { + if condition.is_null() || state.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null pointer passed") + } else { + let expression = match to_str(condition) { + Ok(x) => x, + Err(e) => return e, + }; + + let expression = match Expression::from_str(expression) { + Err(e) => return handle_error(e), + Ok(x) => x, + }; + + let state = match (*state).0.read() { + Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(s) => s, + }; + + match expression.eval(&state) { + Ok(true) => LCI_RESULT_TRUE, + Ok(false) => LCI_RESULT_FALSE, + Err(e) => handle_error(e), + } + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_get_error_message(message: *mut *const c_char) -> c_int { + catch_unwind(|| { + if message.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null pointer passed") + } else { + ERROR_MESSAGE.with(|f| { + if f.borrow().as_bytes().is_empty() { + *message = ptr::null(); + } else { + *message = f.borrow().as_ptr() as *const i8; + } + }); + + LCI_OK + } + }).unwrap_or(LCI_ERROR_PANICKED) +} diff --git a/ffi/src/state.rs b/ffi/src/state.rs new file mode 100644 index 0000000..20115b3 --- /dev/null +++ b/ffi/src/state.rs @@ -0,0 +1,199 @@ +use std::error::Error; +use std::panic::catch_unwind; +use std::path::PathBuf; +use std::sync::RwLock; + +use libc::{c_char, c_int, size_t, uint32_t}; +use loot_condition_interpreter::State; + +use constants::*; +use helpers::{error, map_game_type, map_plugin_crcs, map_plugin_versions, to_str, to_str_vec}; + +#[allow(non_camel_case_types)] +#[no_mangle] +pub struct lci_state(pub RwLock); + +#[allow(non_camel_case_types)] +#[no_mangle] +#[repr(C)] +pub struct plugin_version { + pub plugin_name: *const c_char, + pub version: *const c_char, +} + +#[allow(non_camel_case_types)] +#[no_mangle] +#[repr(C)] +pub struct plugin_crc { + pub plugin_name: *const c_char, + pub crc: uint32_t, +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_create( + state: *mut *mut lci_state, + game_type: c_int, + data_path: *const c_char, + loot_path: *const c_char, +) -> c_int { + catch_unwind(|| { + if state.is_null() || data_path.is_null() || loot_path.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null pointer passed") + } else { + let game_type = match map_game_type(game_type) { + Ok(x) => x, + Err(x) => return error(x, "Invalid game specified"), + }; + + let data_path = match to_str(data_path) { + Ok(x) => PathBuf::from(x), + Err(e) => return e, + }; + + let loot_path = match to_str(loot_path) { + Ok(x) => PathBuf::from(x), + Err(e) => return e, + }; + + *state = Box::into_raw(Box::new(lci_state(RwLock::new(State::new( + game_type, data_path, loot_path, + ))))); + + LCI_OK + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_destroy(state: *mut lci_state) { + if !state.is_null() { + Box::from_raw(state); + } +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_set_active_plugins( + state: *mut lci_state, + plugin_names: *const *const c_char, + num_plugins: size_t, +) -> c_int { + catch_unwind(|| { + if state.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed") + } else if plugin_names.is_null() && num_plugins != 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Null plugin_names pointer passed but num_plugins is non-zero", + ) + } else if !plugin_names.is_null() && num_plugins == 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Non-null plugin_names pointer passed but num_plugins is zero", + ) + } else { + let plugins: Vec<&str> = match to_str_vec(plugin_names, num_plugins) { + Ok(x) => x, + Err(e) => return e, + }; + + let mut state = match (*state).0.write() { + Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(h) => h, + }; + + state.set_active_plugins(&plugins); + + LCI_OK + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_set_plugin_versions( + state: *mut lci_state, + plugin_versions: *const plugin_version, + num_plugins: size_t, +) -> c_int { + catch_unwind(|| { + if state.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed") + } else if plugin_versions.is_null() && num_plugins != 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Null plugin_versions pointer passed but num_plugins is non-zero", + ) + } else if !plugin_versions.is_null() && num_plugins == 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Non-null plugin_versions pointer passed but num_plugins is zero", + ) + } else { + let plugin_versions = match map_plugin_versions(plugin_versions, num_plugins) { + Ok(x) => x, + Err(e) => return e, + }; + + let mut state = match (*state).0.write() { + Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(h) => h, + }; + + state.set_plugin_versions(&plugin_versions); + + LCI_OK + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_set_crc_cache( + state: *mut lci_state, + entries: *const plugin_crc, + num_entries: size_t, +) -> c_int { + catch_unwind(|| { + if state.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed") + } else if entries.is_null() && num_entries != 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Null entries pointer passed but num_entries is non-zero", + ) + } else if !entries.is_null() && num_entries == 0 { + error( + LCI_ERROR_INVALID_ARGS, + "Non-null entries pointer passed but num_entries is zero", + ) + } else { + let plugin_crcs = match map_plugin_crcs(entries, num_entries) { + Ok(x) => x, + Err(e) => return e, + }; + + match (*state).0.write() { + Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(mut s) => match s.set_cached_crcs(&plugin_crcs) { + Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(_) => LCI_OK, + }, + } + } + }).unwrap_or(LCI_ERROR_PANICKED) +} + +#[no_mangle] +pub unsafe extern "C" fn lci_state_clear_condition_cache(state: *mut lci_state) -> c_int { + catch_unwind(|| { + if state.is_null() { + error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed") + } else { + match (*state).0.write() { + Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(mut s) => match s.clear_condition_cache() { + Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()), + Ok(_) => LCI_OK, + }, + } + } + }).unwrap_or(LCI_ERROR_PANICKED) +} diff --git a/ffi/tests/ffi.cpp b/ffi/tests/ffi.cpp new file mode 100644 index 0000000..3280b87 --- /dev/null +++ b/ffi/tests/ffi.cpp @@ -0,0 +1,197 @@ + +#include +#include +#include +#include +#include + +#include +#include + +#include "loot_condition_interpreter.hpp" + +void test_game_id_values() { + printf("testing LCI_GAME_* values...\n"); + assert(LCI_GAME_TES4 == 0); + assert(LCI_GAME_TES5 == 1); + assert(LCI_GAME_TES5SE == 2); + assert(LCI_GAME_TES5VR == 3); + assert(LCI_GAME_FO3 == 4); + assert(LCI_GAME_FNV == 5); + assert(LCI_GAME_FO4 == 6); + assert(LCI_GAME_FO4VR == 7); +} + +void test_lci_condition_parse() { + printf("testing lci_condition_parse()...\n"); + + int return_code = lci_condition_parse("file(\"Blank.esm\")"); + + assert(return_code == LCI_OK); +} + +void test_lci_get_error_message() { + printf("testing lci_get_error_message()...\n"); + + const char * message = nullptr; + int return_code = lci_get_error_message(&message); + + assert(return_code == LCI_OK); + assert(message == nullptr); + + return_code = lci_condition_parse("file(\"Blank."); + + assert(return_code == LCI_ERROR_PARSING_ERROR); + + return_code = lci_get_error_message(&message); + assert(return_code == LCI_OK); + assert(message != nullptr); + assert(strcmp(message, "An error was encountered in the parser \"SeparatedList\" while parsing the expression \"file(\\\"Blank.\"") == 0); +} + +void test_lci_state_create() { + printf("testing lci_state_create()...\n"); + + lci_state * state = nullptr; + int return_code = lci_state_create(&state, LCI_GAME_TES4, ".", "."); + + assert(return_code == LCI_OK); + assert(state != nullptr); + + lci_state_destroy(state); +} + +void test_lci_condition_eval() { + printf("testing lci_condition_eval()...\n"); + + lci_state * state = nullptr; + int return_code = lci_state_create(&state, LCI_GAME_TES4, "../../testing-plugins/Oblivion/Data", "."); + + assert(return_code == LCI_OK); + assert(state != nullptr); + + return_code = lci_condition_eval("file(\"Blank.esm\")", state); + + assert(return_code == LCI_RESULT_TRUE); + + return_code = lci_condition_eval("file(\"missing.esm\")", state); + + assert(return_code == LCI_RESULT_FALSE); + + lci_state_destroy(state); +} + +void test_lci_state_set_active_plugins() { + printf("testing lci_state_set_active_plugins()...\n"); + + lci_state * state = nullptr; + int return_code = lci_state_create(&state, LCI_GAME_TES4, "../../testing-plugins/Oblivion/Data", "."); + + assert(return_code == LCI_OK); + assert(state != nullptr); + + char const * plugins[] = { "Blank.esm" }; + + return_code = lci_state_set_active_plugins(state, plugins, 0); + assert(return_code != LCI_OK); + + return_code = lci_state_set_active_plugins(state, nullptr, 1); + assert(return_code != LCI_OK); + + return_code = lci_state_set_active_plugins(state, plugins, 1); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("active(\"Blank.esm\")", state); + assert(return_code == LCI_RESULT_TRUE); + + return_code = lci_state_set_active_plugins(state, nullptr, 0); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("active(\"Blank.esm\")", state); + assert(return_code == LCI_RESULT_FALSE); + + lci_state_destroy(state); +} + +void test_lci_state_set_plugin_versions() { + printf("testing lci_state_set_plugin_versions()...\n"); + + lci_state * state = nullptr; + int return_code = lci_state_create(&state, LCI_GAME_TES4, "../../testing-plugins/Oblivion/Data", "."); + + assert(return_code == LCI_OK); + assert(state != nullptr); + + plugin_version plugins[] = { {"Blank.esm", "5"} }; + + return_code = lci_state_set_plugin_versions(state, plugins, 0); + assert(return_code != LCI_OK); + + return_code = lci_state_set_plugin_versions(state, nullptr, 1); + assert(return_code != LCI_OK); + + return_code = lci_state_set_plugin_versions(state, plugins, 1); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("version(\"Blank.esm\", \"5\", ==)", state); + assert(return_code == LCI_RESULT_TRUE); + + return_code = lci_state_set_plugin_versions(state, nullptr, 0); + assert(return_code == LCI_OK); + + return_code = lci_state_clear_condition_cache(state); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("version(\"Blank.esm\", \"5\", ==)", state); + assert(return_code == LCI_RESULT_FALSE); + + lci_state_destroy(state); +} + +void test_lci_state_set_crc_cache() { + printf("testing lci_state_set_crc_cache()...\n"); + + lci_state * state = nullptr; + int return_code = lci_state_create(&state, LCI_GAME_TES4, "../../testing-plugins/Oblivion/Data", "."); + + assert(return_code == LCI_OK); + assert(state != nullptr); + + plugin_crc plugin_crcs[] = { {"Blank.esm", 0xDEADBEEF} }; + + return_code = lci_state_set_crc_cache(state, plugin_crcs, 0); + assert(return_code != LCI_OK); + + return_code = lci_state_set_crc_cache(state, nullptr, 1); + assert(return_code != LCI_OK); + + return_code = lci_state_set_crc_cache(state, plugin_crcs, 1); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("checksum(\"Blank.esm\", DEADBEEF)", state); + assert(return_code == LCI_RESULT_TRUE); + + return_code = lci_state_set_crc_cache(state, nullptr, 0); + assert(return_code == LCI_OK); + + return_code = lci_condition_eval("checksum(\"Blank.esm\", DEADBEEF)", state); + assert(return_code == LCI_RESULT_FALSE); + + lci_state_destroy(state); +} + +int main(void) { + test_game_id_values(); + + test_lci_condition_parse(); + test_lci_get_error_message(); + + test_lci_state_create(); + test_lci_condition_eval(); + test_lci_state_set_active_plugins(); + test_lci_state_set_plugin_versions(); + test_lci_state_set_crc_cache(); + + printf("SUCCESS\n"); + return 0; +} diff --git a/src/lib.rs b/src/lib.rs index 4f7ea42..843aa2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,9 +15,10 @@ mod version; use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; use std::fmt; +use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::str; -use std::sync::RwLock; +use std::sync::{PoisonError, RwLock, RwLockWriteGuard}; use nom::types::CompleteStr; use nom::IResult; @@ -94,19 +95,43 @@ impl State { mut self, plugin_versions: &[(T, V)], ) -> Self { - self.plugin_versions = plugin_versions - .iter() - .map(|(p, v)| (p.as_ref().to_lowercase(), v.to_string())) - .collect(); + self.set_plugin_versions(plugin_versions); self } pub fn with_active_plugins>(mut self, active_plugins: &[T]) -> Self { + self.set_active_plugins(active_plugins); + self + } + + pub fn set_active_plugins>(&mut self, active_plugins: &[T]) { self.active_plugins = active_plugins .into_iter() .map(|s| s.as_ref().to_lowercase()) .collect(); - self + } + + pub fn set_plugin_versions, V: ToString>(&mut self, plugin_versions: &[(T, V)]) { + self.plugin_versions = plugin_versions + .iter() + .map(|(p, v)| (p.as_ref().to_lowercase(), v.to_string())) + .collect(); + } + + pub fn set_cached_crcs>( + &mut self, + plugin_crcs: &[(T, u32)], + ) -> Result<(), PoisonError>>> { + let mut writer = self.crc_cache.write()?; + + writer.deref_mut().clear(); + writer.deref_mut().extend( + plugin_crcs + .iter() + .map(|(p, v)| (p.as_ref().to_lowercase(), *v)), + ); + + Ok(()) } pub fn clear_condition_cache(