/*! C bindings for the YARA-X library.

This crate defines the C-compatible API that C/C++ programs can use for
interfacing with the YARA-X Rust library. A header file for this library
(`yara_x.h`) will be automatically generated by [`cbindgen`][1], during
compilation, together with dynamic-linking and static-linking versions of
the library.

# How to build and install

You will need [`cargo-c`][2] for building this library, if you didn't install
it before, this is the first step:
```text
cargo install cargo-c
```

You will also need the `openssl` library, depending on your platform you
can choose one of the following methods:

Ubuntu:

```text
sudo apt install libssl-dev
```

MacOS (using [`brew`][3]):

```text
brew install openssl@3
```

Windows (using [`vcpkg`][4]):

```text
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
bootstrap-vcpkg.bat
vcpkg install openssl:x64-windows-static
set OPENSSL_DIR=%cd%\installed\x64-windows-static
```

Once you have installed the pre-requisites, go to the root directory
of the YARA-X repository and type:

```text
cargo cinstall -p yara-x-capi --release
```

The command above will put the library and header files in the correct path
in your system (usually `/usr/local/lib` and `/usr/local/include` for Linux
and macOS users), and will generate a `.pc` file so that `pkg-config` knows
about the library.

In Linux and macOS you can check if everything went fine by compiling a simple
test program, like this:

```text
cat <<EOF > test.c
#include <yara_x.h>
int main() {
    YRX_RULES* rules;
    yrx_compile("rule dummy { condition: true }", &rules);
    yrx_rules_destroy(rules);
}
EOF
```

```text
gcc `pkg-config --cflags yara_x_capi` `pkg-config --libs yara_x_capi` test.c
```

The compilation should succeed without errors.

Windows users can find all the files you need for importing the YARA-X library
in your project in the `target/x86_64-pc-windows-msvc/release` directory. This
includes:

* A header file (`yara_x.h`)
* A [module definition file][4] (`yara_x_capi.def`)
* A DLL file (`yara_x_capi.dll`) with its corresponding import library (`yara_x_capi.dll.lib`)
* A static library (`yara_x_capi.lib`)


[1]: https://github.com/mozilla/cbindgen
[2]: https://github.com/lu-zero/cargo-c
[3]: https://brew.sh
[4]: https://vcpkg.io/
[4]: https://learn.microsoft.com/en-us/cpp/build/reference/module-definition-dot-def-files
 */

#![deny(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

use std::cell::RefCell;
use std::ffi::{c_char, CStr, CString};
use std::ptr::slice_from_raw_parts_mut;

use yara_x::errors::CompileError;

pub use metadata::*;
pub use pattern::*;
pub use rule::*;
pub use rules::*;
pub use scanner::*;

mod compiler;
mod metadata;
mod pattern;
mod rule;
mod rules;
mod scanner;

#[cfg(test)]
mod tests;

thread_local! {
    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}

fn _yrx_set_last_error<E>(err: Option<E>)
where
    E: ToString,
{
    LAST_ERROR.set(err.map(|err| CString::new(err.to_string()).unwrap()))
}

/// Error codes returned by functions in this API.
#[derive(PartialEq, Debug)]
#[repr(C)]
pub enum YRX_RESULT {
    /// Everything was OK.
    SUCCESS,
    /// A syntax error occurred while compiling YARA rules.
    SYNTAX_ERROR,
    /// An error occurred while defining or setting a global variable. This may
    /// happen when a variable is defined twice and when you try to set a value
    /// that doesn't correspond to the variable's type.
    VARIABLE_ERROR,
    /// An error occurred during a scan operation.
    SCAN_ERROR,
    /// A scan operation was aborted due to a timeout.
    SCAN_TIMEOUT,
    /// An error indicating that some of the arguments passed to a function is
    /// invalid. Usually indicates a nil pointer to a scanner or compiler.
    INVALID_ARGUMENT,
    /// An error indicating that some of the strings passed to a function is
    /// not valid UTF-8.
    INVALID_UTF8,
    /// An error occurred while serializing/deserializing YARA rules.
    SERIALIZATION_ERROR,
    /// An error returned when a rule doesn't have any metadata.
    NO_METADATA,
    /// An error returned in cases where some API is not supported because the
    /// library was not built with the required features.
    NOT_SUPPORTED,
}

/// Returns the error message for the most recent function in this API
/// invoked by the current thread.
///
/// The returned pointer is only valid until this thread calls some other
/// function, as it can modify the last error and render the pointer to
/// a previous error message invalid. Also, the pointer will be null if
/// the most recent function was successfully.
#[no_mangle]
pub unsafe extern "C" fn yrx_last_error() -> *const c_char {
    LAST_ERROR.with_borrow(|err| {
        if let Some(err) = err {
            err.as_ptr()
        } else {
            std::ptr::null()
        }
    })
}

/// Contains information about a pattern match.
#[repr(C)]
pub struct YRX_MATCH {
    /// Offset within the data where the match occurred.
    pub offset: usize,
    /// Length of the match.
    pub length: usize,
}

/// Represents a buffer with arbitrary data.
#[repr(C)]
pub struct YRX_BUFFER {
    /// Pointer to the data contained in the buffer.
    pub data: *mut u8,
    /// Length of data in bytes.
    pub length: usize,
}

impl Drop for YRX_BUFFER {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(slice_from_raw_parts_mut(
                self.data,
                self.length,
            )));
        }
    }
}

/// Destroys a [`YRX_BUFFER`] object.
#[no_mangle]
pub unsafe extern "C" fn yrx_buffer_destroy(buf: *mut YRX_BUFFER) {
    drop(Box::from_raw(buf));
}

/// Compiles YARA source code and creates a [`YRX_RULES`] object that contains
/// the compiled rules.
///
/// The rules must be destroyed with [`yrx_rules_destroy`].
#[no_mangle]
pub unsafe extern "C" fn yrx_compile(
    src: *const c_char,
    rules: &mut *mut YRX_RULES,
) -> YRX_RESULT {
    let c_str = CStr::from_ptr(src);
    match yara_x::compile(c_str.to_bytes()) {
        Ok(r) => {
            *rules = Box::into_raw(YRX_RULES::boxed(r));
            _yrx_set_last_error::<CompileError>(None);
            YRX_RESULT::SUCCESS
        }
        Err(err) => {
            _yrx_set_last_error(Some(err));
            YRX_RESULT::SYNTAX_ERROR
        }
    }
}
