use crate::database::{Database, ReloadEvent};
use crate::schema_validation::SchemaValidator;
use crate::schemas::{get_schema_info, is_known_database_type};
use crate::DatabaseBuilder;
use chrono::TimeZone;
use matchy_data_format::DataValue;
use matchy_match_mode::MatchMode;
use std::collections::HashMap;
use std::ffi::{c_void, CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::slice;
struct CCallbackAdapter {
callback: unsafe extern "C" fn(event: *const matchy_reload_event_t, user_data: *mut c_void),
user_data: *mut c_void,
}
unsafe impl Send for CCallbackAdapter {}
unsafe impl Sync for CCallbackAdapter {}
impl CCallbackAdapter {
fn invoke(&self, event: &ReloadEvent) {
let path_cstring =
std::ffi::CString::new(event.path.to_string_lossy().as_ref()).unwrap_or_default();
let error_cstring = event
.error
.as_ref()
.and_then(|e| std::ffi::CString::new(e.as_str()).ok());
let c_event = matchy_reload_event_t {
path: path_cstring.as_ptr(),
success: event.success,
error: error_cstring
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null()),
generation: event.generation,
};
unsafe { (self.callback)(&c_event, self.user_data) };
}
}
pub const MATCHY_SUCCESS: i32 = 0;
pub const MATCHY_ERROR_FILE_NOT_FOUND: i32 = -1;
pub const MATCHY_ERROR_INVALID_FORMAT: i32 = -2;
pub const MATCHY_ERROR_CORRUPT_DATA: i32 = -3;
pub const MATCHY_ERROR_OUT_OF_MEMORY: i32 = -4;
pub const MATCHY_ERROR_INVALID_PARAM: i32 = -5;
pub const MATCHY_ERROR_IO: i32 = -6;
pub const MATCHY_ERROR_SCHEMA_VALIDATION: i32 = -7;
pub const MATCHY_ERROR_UNKNOWN_SCHEMA: i32 = -8;
#[repr(C)]
pub struct matchy_builder_t {
_private: [u8; 0],
}
#[repr(C)]
pub struct matchy_t {
_private: [u8; 0],
}
#[repr(C)]
pub struct matchy_result_t {
pub found: bool,
pub prefix_len: u8,
pub _result_type: u8,
pub _data_offset: u32,
pub _db_ref: *const matchy_t,
}
struct MatchyBuilderInternal {
builder: DatabaseBuilder,
validator: Option<SchemaValidator>,
}
struct MatchyInternal {
database: Database,
}
impl matchy_builder_t {
fn from_internal(internal: Box<MatchyBuilderInternal>) -> *mut Self {
Box::into_raw(internal).cast::<Self>()
}
unsafe fn into_internal(ptr: *mut Self) -> Box<MatchyBuilderInternal> {
Box::from_raw(ptr.cast::<MatchyBuilderInternal>())
}
unsafe fn as_internal_mut(ptr: *mut Self) -> &'static mut MatchyBuilderInternal {
&mut *ptr.cast::<MatchyBuilderInternal>()
}
}
impl matchy_t {
fn from_internal(internal: Box<MatchyInternal>) -> *mut Self {
Box::into_raw(internal).cast::<Self>()
}
unsafe fn into_internal(ptr: *mut Self) -> Box<MatchyInternal> {
Box::from_raw(ptr.cast::<MatchyInternal>())
}
unsafe fn as_internal(ptr: *const Self) -> &'static MatchyInternal {
&*ptr.cast::<MatchyInternal>()
}
}
#[no_mangle]
pub extern "C" fn matchy_builder_new() -> *mut matchy_builder_t {
let builder = DatabaseBuilder::new(MatchMode::CaseSensitive);
let internal = Box::new(MatchyBuilderInternal {
builder,
validator: None,
});
matchy_builder_t::from_internal(internal)
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_set_case_insensitive(
builder: *mut matchy_builder_t,
case_insensitive: bool,
) -> i32 {
if builder.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let internal = matchy_builder_t::as_internal_mut(builder);
let match_mode = if case_insensitive {
MatchMode::CaseInsensitive
} else {
MatchMode::CaseSensitive
};
internal.builder.set_match_mode(match_mode);
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_set_schema(
builder: *mut matchy_builder_t,
schema_name: *const c_char,
) -> i32 {
if builder.is_null() || schema_name.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let name = match CStr::from_ptr(schema_name).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
if !is_known_database_type(name) {
return MATCHY_ERROR_UNKNOWN_SCHEMA;
}
let validator = match SchemaValidator::new(name) {
Ok(v) => v,
Err(_) => return MATCHY_ERROR_INVALID_FORMAT,
};
let internal = matchy_builder_t::as_internal_mut(builder);
if let Some(info) = get_schema_info(name) {
let placeholder = DatabaseBuilder::new(MatchMode::CaseSensitive);
let old_builder = std::mem::replace(&mut internal.builder, placeholder);
internal.builder = old_builder.with_database_type(info.database_type);
}
internal.validator = Some(validator);
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_add(
builder: *mut matchy_builder_t,
key: *const c_char,
json_data: *const c_char,
) -> i32 {
if builder.is_null() || key.is_null() || json_data.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let key_str = match CStr::from_ptr(key).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let json_str = match CStr::from_ptr(json_data).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let data: DataValue = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return MATCHY_ERROR_INVALID_FORMAT,
};
let data_map = match data {
DataValue::Map(m) => m,
_ => {
let mut map = HashMap::new();
map.insert("value".to_string(), data);
map
}
};
let internal = matchy_builder_t::as_internal_mut(builder);
if let Some(ref validator) = internal.validator {
if validator.validate(&data_map).is_err() {
return MATCHY_ERROR_SCHEMA_VALIDATION;
}
}
match internal.builder.add_entry(key_str, data_map) {
Ok(_) => MATCHY_SUCCESS,
Err(_) => MATCHY_ERROR_INVALID_FORMAT,
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_set_description(
builder: *mut matchy_builder_t,
description: *const c_char,
) -> i32 {
if builder.is_null() || description.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let desc_str = match CStr::from_ptr(description).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let internal = matchy_builder_t::as_internal_mut(builder);
let old_builder = std::mem::replace(
&mut internal.builder,
DatabaseBuilder::new(MatchMode::CaseSensitive),
);
internal.builder = old_builder.with_description("en", desc_str);
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_set_update_url(
builder: *mut matchy_builder_t,
url: *const c_char,
) -> i32 {
if builder.is_null() || url.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let url_str = match CStr::from_ptr(url).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let internal = matchy_builder_t::as_internal_mut(builder);
let old_builder = std::mem::replace(
&mut internal.builder,
DatabaseBuilder::new(MatchMode::CaseSensitive),
);
internal.builder = old_builder.with_update_url(url_str);
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_save(
builder: *mut matchy_builder_t,
filename: *const c_char,
) -> i32 {
if builder.is_null() || filename.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let path = match CStr::from_ptr(filename).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let internal = matchy_builder_t::as_internal_mut(builder);
let builder_to_build = std::mem::replace(
&mut internal.builder,
DatabaseBuilder::new(MatchMode::CaseSensitive),
);
let bytes = match builder_to_build.build() {
Ok(b) => b,
Err(_) => return MATCHY_ERROR_INVALID_FORMAT,
};
match std::fs::write(path, bytes) {
Ok(_) => MATCHY_SUCCESS,
Err(_) => MATCHY_ERROR_IO,
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_build(
builder: *mut matchy_builder_t,
buffer: *mut *mut u8,
size: *mut usize,
) -> i32 {
if builder.is_null() || buffer.is_null() || size.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let internal = matchy_builder_t::as_internal_mut(builder);
let builder_to_build = std::mem::replace(
&mut internal.builder,
DatabaseBuilder::new(MatchMode::CaseSensitive),
);
let bytes = match builder_to_build.build() {
Ok(b) => b,
Err(_) => return MATCHY_ERROR_INVALID_FORMAT,
};
let buf_size = bytes.len();
let buf_ptr = libc::malloc(buf_size).cast::<u8>();
if buf_ptr.is_null() {
return MATCHY_ERROR_OUT_OF_MEMORY;
}
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf_ptr, buf_size);
*buffer = buf_ptr;
*size = buf_size;
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_builder_free(builder: *mut matchy_builder_t) {
if !builder.is_null() {
let _ = matchy_builder_t::into_internal(builder);
}
}
#[repr(C)]
pub struct matchy_reload_event_t {
pub path: *const c_char,
pub success: bool,
pub error: *const c_char,
pub generation: u64,
}
#[allow(non_camel_case_types)]
pub type matchy_reload_callback_t =
Option<unsafe extern "C" fn(event: *const matchy_reload_event_t, user_data: *mut c_void)>;
#[repr(C)]
pub struct matchy_open_options_t {
pub cache_capacity: u32,
pub auto_reload: bool,
pub auto_update: bool,
pub update_interval_secs: u32,
pub cache_dir: *const c_char,
pub reload_callback: matchy_reload_callback_t,
pub reload_callback_user_data: *mut c_void,
}
impl Default for matchy_open_options_t {
fn default() -> Self {
Self {
cache_capacity: 10000,
auto_reload: false,
auto_update: false,
update_interval_secs: 3600,
cache_dir: ptr::null(),
reload_callback: None,
reload_callback_user_data: ptr::null_mut(),
}
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_init_open_options(options: *mut matchy_open_options_t) {
if options.is_null() {
return;
}
*options = matchy_open_options_t::default();
}
#[no_mangle]
pub unsafe extern "C" fn matchy_open_with_options(
filename: *const c_char,
options: *const matchy_open_options_t,
) -> *mut matchy_t {
if filename.is_null() || options.is_null() {
return ptr::null_mut();
}
let path = match CStr::from_ptr(filename).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let opts = &*options;
let mut opener = Database::from(path);
if opts.cache_capacity == 0 {
opener = opener.no_cache();
} else {
opener = opener.cache_capacity(opts.cache_capacity as usize);
}
if opts.auto_reload {
opener = opener.watch();
}
#[cfg(feature = "auto-update")]
if opts.auto_update {
opener = opener
.auto_update()
.update_interval(std::time::Duration::from_secs(u64::from(
opts.update_interval_secs,
)));
if !opts.cache_dir.is_null() {
if let Ok(dir) = CStr::from_ptr(opts.cache_dir).to_str() {
opener = opener.cache_dir(dir);
}
}
}
if let Some(callback) = opts.reload_callback {
let adapter = CCallbackAdapter {
callback,
user_data: opts.reload_callback_user_data,
};
opener = opener.on_reload(move |event: ReloadEvent| {
adapter.invoke(&event);
});
}
match opener.open() {
Ok(db) => {
let internal = Box::new(MatchyInternal { database: db });
matchy_t::from_internal(internal)
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_open(filename: *const c_char) -> *mut matchy_t {
let opts = matchy_open_options_t::default();
matchy_open_with_options(filename, &opts)
}
#[no_mangle]
pub unsafe extern "C" fn matchy_open_buffer(buffer: *const u8, size: usize) -> *mut matchy_t {
if buffer.is_null() || size == 0 {
return ptr::null_mut();
}
let slice = slice::from_raw_parts(buffer, size);
match Database::from_bytes(slice.to_vec()) {
Ok(db) => {
let internal = Box::new(MatchyInternal { database: db });
matchy_t::from_internal(internal)
}
Err(_) => ptr::null_mut(),
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct matchy_stats_t {
pub total_queries: u64,
pub queries_with_match: u64,
pub queries_without_match: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub ip_queries: u64,
pub string_queries: u64,
}
#[no_mangle]
pub unsafe extern "C" fn matchy_get_stats(db: *const matchy_t, stats: *mut matchy_stats_t) {
if db.is_null() || stats.is_null() {
return;
}
let internal = matchy_t::as_internal(db);
let rust_stats = internal.database.stats();
*stats = matchy_stats_t {
total_queries: rust_stats.total_queries,
queries_with_match: rust_stats.queries_with_match,
queries_without_match: rust_stats.queries_without_match,
cache_hits: rust_stats.cache_hits,
cache_misses: rust_stats.cache_misses,
ip_queries: rust_stats.ip_queries,
string_queries: rust_stats.string_queries,
};
}
#[no_mangle]
pub unsafe extern "C" fn matchy_clear_cache(db: *const matchy_t) {
if db.is_null() {
return;
}
let internal = matchy_t::as_internal(db);
internal.database.clear_cache();
}
#[no_mangle]
pub unsafe extern "C" fn matchy_close(db: *mut matchy_t) {
if !db.is_null() {
let _ = matchy_t::into_internal(db);
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_query(
db: *const matchy_t,
query: *const c_char,
) -> matchy_result_t {
let empty_result = matchy_result_t {
found: false,
prefix_len: 0,
_result_type: 0,
_data_offset: 0,
_db_ref: ptr::null(),
};
if db.is_null() || query.is_null() {
return empty_result;
}
let query_str = match CStr::from_ptr(query).to_str() {
Ok(s) => s,
Err(_) => return empty_result,
};
let internal = matchy_t::as_internal(db);
match internal.database.lookup_ref(query_str) {
Ok(lookup_ref) if lookup_ref.found => matchy_result_t {
found: true,
prefix_len: lookup_ref.prefix_len,
_result_type: lookup_ref.result_type,
_data_offset: lookup_ref.data_offset,
_db_ref: db,
},
_ => empty_result,
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_query_into(
db: *const matchy_t,
query: *const c_char,
result: *mut matchy_result_t,
) {
if result.is_null() {
return;
}
*result = matchy_query(db, query);
}
#[no_mangle]
pub unsafe extern "C" fn matchy_free_result(_result: *mut matchy_result_t) {
}
#[no_mangle]
pub unsafe extern "C" fn matchy_free_string(string: *mut c_char) {
if !string.is_null() {
let _ = CString::from_raw(string);
}
}
#[no_mangle]
pub extern "C" fn matchy_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0")
.as_ptr()
.cast::<c_char>()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_format(db: *const matchy_t) -> *const c_char {
if db.is_null() {
return ptr::null();
}
let internal = matchy_t::as_internal(db);
let format_str = internal.database.format();
format_str.as_ptr().cast::<c_char>()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_has_ip_data(db: *const matchy_t) -> bool {
if db.is_null() {
return false;
}
let internal = matchy_t::as_internal(db);
internal.database.has_ip_data()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_has_string_data(db: *const matchy_t) -> bool {
if db.is_null() {
return false;
}
let internal = matchy_t::as_internal(db);
internal.database.has_string_data()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_has_literal_data(db: *const matchy_t) -> bool {
if db.is_null() {
return false;
}
let internal = matchy_t::as_internal(db);
internal.database.has_literal_data()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_has_glob_data(db: *const matchy_t) -> bool {
if db.is_null() {
return false;
}
let internal = matchy_t::as_internal(db);
internal.database.has_glob_data()
}
#[no_mangle]
#[deprecated(
since = "0.5.0",
note = "Use matchy_has_literal_data or matchy_has_glob_data instead"
)]
pub unsafe extern "C" fn matchy_has_pattern_data(db: *const matchy_t) -> bool {
if db.is_null() {
return false;
}
let internal = matchy_t::as_internal(db);
internal.database.has_string_data()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_metadata(db: *const matchy_t) -> *mut c_char {
if db.is_null() {
return ptr::null_mut();
}
let internal = matchy_t::as_internal(db);
match internal.database.metadata() {
Some(metadata) => {
match serde_json::to_string(&metadata) {
Ok(json_str) => match CString::new(json_str) {
Ok(c_str) => c_str.into_raw(),
Err(_) => ptr::null_mut(),
},
Err(_) => ptr::null_mut(),
}
}
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_get_pattern_string(
db: *const matchy_t,
pattern_id: u32,
) -> *mut c_char {
if db.is_null() {
return ptr::null_mut();
}
let internal = matchy_t::as_internal(db);
if let Some(pattern_str) = internal.database.get_pattern_string(pattern_id) {
match CString::new(pattern_str) {
Ok(c_str) => return c_str.into_raw(),
Err(_) => return ptr::null_mut(),
}
}
ptr::null_mut()
}
#[no_mangle]
pub unsafe extern "C" fn matchy_pattern_count(db: *const matchy_t) -> usize {
if db.is_null() {
return 0;
}
let internal = matchy_t::as_internal(db);
internal.database.pattern_count()
}
pub const MATCHY_DATA_TYPE_EXTENDED: u32 = 0;
pub const MATCHY_DATA_TYPE_POINTER: u32 = 1;
pub const MATCHY_DATA_TYPE_UTF8_STRING: u32 = 2;
pub const MATCHY_DATA_TYPE_DOUBLE: u32 = 3;
pub const MATCHY_DATA_TYPE_BYTES: u32 = 4;
pub const MATCHY_DATA_TYPE_UINT16: u32 = 5;
pub const MATCHY_DATA_TYPE_UINT32: u32 = 6;
pub const MATCHY_DATA_TYPE_MAP: u32 = 7;
pub const MATCHY_DATA_TYPE_INT32: u32 = 8;
pub const MATCHY_DATA_TYPE_UINT64: u32 = 9;
pub const MATCHY_DATA_TYPE_UINT128: u32 = 10;
pub const MATCHY_DATA_TYPE_ARRAY: u32 = 11;
pub const MATCHY_DATA_TYPE_BOOLEAN: u32 = 14;
pub const MATCHY_DATA_TYPE_FLOAT: u32 = 15;
pub const MATCHY_ERROR_LOOKUP_PATH_INVALID: i32 = -7;
pub const MATCHY_ERROR_NO_DATA: i32 = -8;
pub const MATCHY_ERROR_DATA_PARSE: i32 = -9;
#[repr(C)]
#[derive(Copy, Clone)]
pub union matchy_entry_data_value_u {
pub pointer: u32,
pub utf8_string: *const c_char,
pub double_value: f64,
pub bytes: *const u8,
pub uint16: u16,
pub uint32: u32,
pub int32: i32,
pub uint64: u64,
pub uint128: [u8; 16],
pub boolean: bool,
pub float_value: f32,
}
#[repr(C)]
pub struct matchy_entry_data_t {
pub has_data: bool,
pub type_: u32,
pub value: matchy_entry_data_value_u,
pub data_size: u32,
pub offset: u32,
}
#[repr(C)]
pub struct matchy_entry_s {
pub db: *const matchy_t,
pub _data_offset: u32,
}
#[repr(C)]
pub struct matchy_entry_data_list_t {
pub entry_data: matchy_entry_data_t,
pub next: *mut Self,
}
impl matchy_entry_data_t {
fn empty() -> Self {
Self {
has_data: false,
type_: 0,
value: matchy_entry_data_value_u { uint32: 0 },
data_size: 0,
offset: 0,
}
}
unsafe fn from_data_value(value: &DataValue, string_cache: &mut Vec<CString>) -> Option<Self> {
let (type_, data_value, data_size) = match value {
DataValue::Pointer(offset) => (
MATCHY_DATA_TYPE_POINTER,
matchy_entry_data_value_u { pointer: *offset },
0,
),
DataValue::String(s) => {
let c_str = CString::new(s.as_str()).ok()?;
let ptr = c_str.as_ptr();
string_cache.push(c_str);
(
MATCHY_DATA_TYPE_UTF8_STRING,
matchy_entry_data_value_u { utf8_string: ptr },
u32::try_from(s.len()).unwrap_or(u32::MAX),
)
}
DataValue::Double(d) => (
MATCHY_DATA_TYPE_DOUBLE,
matchy_entry_data_value_u { double_value: *d },
8,
),
DataValue::Bytes(b) => {
let ptr = b.as_ptr();
(
MATCHY_DATA_TYPE_BYTES,
matchy_entry_data_value_u { bytes: ptr },
u32::try_from(b.len()).unwrap_or(u32::MAX),
)
}
DataValue::Uint16(n) => (
MATCHY_DATA_TYPE_UINT16,
matchy_entry_data_value_u { uint16: *n },
2,
),
DataValue::Uint32(n) => (
MATCHY_DATA_TYPE_UINT32,
matchy_entry_data_value_u { uint32: *n },
4,
),
DataValue::Map(m) => (
MATCHY_DATA_TYPE_MAP,
matchy_entry_data_value_u { uint32: 0 },
u32::try_from(m.len()).unwrap_or(u32::MAX),
),
DataValue::Int32(n) => (
MATCHY_DATA_TYPE_INT32,
matchy_entry_data_value_u { int32: *n },
4,
),
DataValue::Uint64(n) => (
MATCHY_DATA_TYPE_UINT64,
matchy_entry_data_value_u { uint64: *n },
8,
),
DataValue::Uint128(n) => {
let bytes = n.to_be_bytes();
(
MATCHY_DATA_TYPE_UINT128,
matchy_entry_data_value_u { uint128: bytes },
16,
)
}
DataValue::Array(a) => (
MATCHY_DATA_TYPE_ARRAY,
matchy_entry_data_value_u { uint32: 0 },
u32::try_from(a.len()).unwrap_or(u32::MAX),
),
DataValue::Bool(b) => (
MATCHY_DATA_TYPE_BOOLEAN,
matchy_entry_data_value_u { boolean: *b },
1,
),
DataValue::Float(f) => (
MATCHY_DATA_TYPE_FLOAT,
matchy_entry_data_value_u { float_value: *f },
4,
),
DataValue::Timestamp(epoch) => {
let iso = chrono::Utc
.timestamp_opt(*epoch, 0)
.single()
.map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
.unwrap_or_else(|| format!("{epoch}"));
let c_str = CString::new(iso.as_str()).ok()?;
let ptr = c_str.as_ptr();
let len = iso.len();
string_cache.push(c_str);
(
MATCHY_DATA_TYPE_UTF8_STRING,
matchy_entry_data_value_u { utf8_string: ptr },
u32::try_from(len).unwrap_or(u32::MAX),
)
}
};
Some(Self {
has_data: true,
type_,
value: data_value,
data_size,
offset: 0,
})
}
}
fn navigate_path<'a>(mut value: &'a DataValue, path: &[&str]) -> Option<&'a DataValue> {
for key in path {
match value {
DataValue::Map(m) => {
value = m.get(*key)?;
}
DataValue::Array(a) => {
let idx: usize = key.parse().ok()?;
value = a.get(idx)?;
}
_ => return None,
}
}
Some(value)
}
#[no_mangle]
pub unsafe extern "C" fn matchy_result_get_entry(
result: *const matchy_result_t,
entry: *mut matchy_entry_s,
) -> i32 {
if result.is_null() || entry.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let res = &*result;
if !res.found {
return MATCHY_ERROR_NO_DATA;
}
(*entry).db = res._db_ref;
(*entry)._data_offset = res._data_offset;
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_aget_value(
entry: *const matchy_entry_s,
entry_data: *mut matchy_entry_data_t,
path: *const *const c_char,
) -> i32 {
if entry.is_null() || entry_data.is_null() || path.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let mut path_vec = Vec::new();
let mut i = 0;
loop {
let ptr = *path.offset(i);
if ptr.is_null() {
break;
}
match CStr::from_ptr(ptr).to_str() {
Ok(s) => path_vec.push(s),
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
}
i += 1;
}
let db = (*entry).db;
if db.is_null() {
(*entry_data) = matchy_entry_data_t::empty();
return MATCHY_ERROR_NO_DATA;
}
let internal = matchy_t::as_internal(db);
let data = match internal.database.decode_at_offset((*entry)._data_offset) {
Ok(d) => d,
Err(_) => {
(*entry_data) = matchy_entry_data_t::empty();
return MATCHY_ERROR_DATA_PARSE;
}
};
let target = match navigate_path(&data, &path_vec) {
Some(v) => v,
None => {
(*entry_data) = matchy_entry_data_t::empty();
return MATCHY_ERROR_LOOKUP_PATH_INVALID;
}
};
let mut string_cache = Vec::new();
match matchy_entry_data_t::from_data_value(target, &mut string_cache) {
Some(d) => {
(*entry_data) = d;
std::mem::forget(string_cache);
MATCHY_SUCCESS
}
None => {
(*entry_data) = matchy_entry_data_t::empty();
MATCHY_ERROR_DATA_PARSE
}
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_get_entry_data_list(
entry: *const matchy_entry_s,
entry_data_list: *mut *mut matchy_entry_data_list_t,
) -> i32 {
if entry.is_null() || entry_data_list.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let db = (*entry).db;
if db.is_null() {
return MATCHY_ERROR_NO_DATA;
}
let internal = matchy_t::as_internal(db);
let data = match internal.database.decode_at_offset((*entry)._data_offset) {
Ok(d) => d,
Err(_) => return MATCHY_ERROR_DATA_PARSE,
};
let mut string_cache = Vec::new();
let mut list_head: *mut matchy_entry_data_list_t = ptr::null_mut();
let mut list_tail: *mut matchy_entry_data_list_t = ptr::null_mut();
let mut add_node = |entry_data: matchy_entry_data_t| {
let node = Box::new(matchy_entry_data_list_t {
entry_data,
next: ptr::null_mut(),
});
let node_ptr = Box::into_raw(node);
if list_head.is_null() {
list_head = node_ptr;
list_tail = node_ptr;
} else {
(*list_tail).next = node_ptr;
list_tail = node_ptr;
}
};
fn flatten_data(
value: &DataValue,
string_cache: &mut Vec<CString>,
add_node: &mut impl FnMut(matchy_entry_data_t),
) {
if let Some(entry_data) =
unsafe { matchy_entry_data_t::from_data_value(value, string_cache) }
{
add_node(entry_data);
}
match value {
DataValue::Map(m) => {
for (_key, val) in m.iter() {
flatten_data(val, string_cache, add_node);
}
}
DataValue::Array(a) => {
for val in a.iter() {
flatten_data(val, string_cache, add_node);
}
}
_ => {}
}
}
flatten_data(&data, &mut string_cache, &mut add_node);
std::mem::forget(string_cache);
*entry_data_list = list_head;
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_free_entry_data_list(list: *mut matchy_entry_data_list_t) {
if list.is_null() {
return;
}
let mut current = list;
while !current.is_null() {
let next = (*current).next;
let _ = Box::from_raw(current);
current = next;
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_get_update_url(db: *const matchy_t) -> *mut c_char {
if db.is_null() {
return ptr::null_mut();
}
let internal = matchy_t::as_internal(db);
match internal.database.update_url() {
Some(url) => match CString::new(url) {
Ok(c_str) => c_str.into_raw(),
Err(_) => ptr::null_mut(),
},
None => ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn matchy_has_auto_update() -> bool {
#[cfg(feature = "auto-update")]
{
true
}
#[cfg(not(feature = "auto-update"))]
{
false
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_result_to_json(result: *const matchy_result_t) -> *mut c_char {
if result.is_null() || !(*result).found || (*result)._db_ref.is_null() {
return ptr::null_mut();
}
let internal = matchy_t::as_internal((*result)._db_ref);
let data = match internal.database.decode_at_offset((*result)._data_offset) {
Ok(d) => d,
Err(_) => return ptr::null_mut(),
};
let json_str = match serde_json::to_string(&data) {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
match CString::new(json_str) {
Ok(c_str) => c_str.into_raw(),
Err(_) => ptr::null_mut(),
}
}
pub const MATCHY_VALIDATION_STANDARD: i32 = 0;
pub const MATCHY_VALIDATION_STRICT: i32 = 1;
#[no_mangle]
pub unsafe extern "C" fn matchy_validate(
filename: *const c_char,
level: i32,
error_message: *mut *mut c_char,
) -> i32 {
use crate::validation::{validate_database, ValidationLevel};
use std::path::Path;
if filename.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let path_str = match CStr::from_ptr(filename).to_str() {
Ok(s) => s,
Err(_) => return MATCHY_ERROR_INVALID_PARAM,
};
let validation_level = match level {
MATCHY_VALIDATION_STANDARD => ValidationLevel::Standard,
MATCHY_VALIDATION_STRICT => ValidationLevel::Strict,
_ => return MATCHY_ERROR_INVALID_PARAM,
};
match validate_database(Path::new(path_str), validation_level) {
Ok(report) => {
if report.is_valid() {
MATCHY_SUCCESS
} else {
if !error_message.is_null() {
let error_text = if report.errors.is_empty() {
"Validation failed (no error details)".to_string()
} else {
report.errors.join("; ")
};
if let Ok(c_str) = CString::new(error_text) {
*error_message = c_str.into_raw();
} else {
*error_message = ptr::null_mut();
}
}
MATCHY_ERROR_CORRUPT_DATA
}
}
Err(_) => {
if !error_message.is_null() {
if let Ok(c_str) = CString::new("Failed to validate database") {
*error_message = c_str.into_raw();
}
}
MATCHY_ERROR_IO
}
}
}
use crate::extractor::{ExtractedItem, Extractor, ExtractorBuilder, HashType};
pub const MATCHY_EXTRACT_DOMAINS: u32 = 1 << 0;
pub const MATCHY_EXTRACT_EMAILS: u32 = 1 << 1;
pub const MATCHY_EXTRACT_IPV4: u32 = 1 << 2;
pub const MATCHY_EXTRACT_IPV6: u32 = 1 << 3;
pub const MATCHY_EXTRACT_HASHES: u32 = 1 << 4;
pub const MATCHY_EXTRACT_BITCOIN: u32 = 1 << 5;
pub const MATCHY_EXTRACT_ETHEREUM: u32 = 1 << 6;
pub const MATCHY_EXTRACT_MONERO: u32 = 1 << 7;
pub const MATCHY_EXTRACT_ALL: u32 = 0xFF;
pub const MATCHY_ITEM_TYPE_DOMAIN: u8 = 0;
pub const MATCHY_ITEM_TYPE_EMAIL: u8 = 1;
pub const MATCHY_ITEM_TYPE_IPV4: u8 = 2;
pub const MATCHY_ITEM_TYPE_IPV6: u8 = 3;
pub const MATCHY_ITEM_TYPE_MD5: u8 = 4;
pub const MATCHY_ITEM_TYPE_SHA1: u8 = 5;
pub const MATCHY_ITEM_TYPE_SHA256: u8 = 6;
pub const MATCHY_ITEM_TYPE_SHA384: u8 = 7;
pub const MATCHY_ITEM_TYPE_SHA512: u8 = 8;
pub const MATCHY_ITEM_TYPE_BITCOIN: u8 = 9;
pub const MATCHY_ITEM_TYPE_ETHEREUM: u8 = 10;
pub const MATCHY_ITEM_TYPE_MONERO: u8 = 11;
#[repr(C)]
pub struct matchy_extractor_t {
_private: [u8; 0],
}
#[repr(C)]
pub struct matchy_match_t {
pub item_type: u8,
pub value: *const c_char,
pub start: usize,
pub end: usize,
}
#[repr(C)]
pub struct matchy_matches_t {
pub items: *const matchy_match_t,
pub count: usize,
_internal: *mut c_void,
}
struct MatchesInternal {
matches: Vec<matchy_match_t>,
#[allow(dead_code)] strings: Vec<CString>,
}
impl matchy_extractor_t {
fn from_internal(internal: Box<Extractor>) -> *mut Self {
Box::into_raw(internal).cast::<Self>()
}
unsafe fn to_internal(ptr: *mut Self) -> Box<Extractor> {
Box::from_raw(ptr.cast::<Extractor>())
}
unsafe fn as_internal(ptr: *const Self) -> &'static Extractor {
&*ptr.cast::<Extractor>()
}
}
fn item_type_from_extracted(item: &ExtractedItem) -> u8 {
match item {
ExtractedItem::Domain(_) => MATCHY_ITEM_TYPE_DOMAIN,
ExtractedItem::Email(_) => MATCHY_ITEM_TYPE_EMAIL,
ExtractedItem::Ipv4(_) => MATCHY_ITEM_TYPE_IPV4,
ExtractedItem::Ipv6(_) => MATCHY_ITEM_TYPE_IPV6,
ExtractedItem::Hash(HashType::Md5, _) => MATCHY_ITEM_TYPE_MD5,
ExtractedItem::Hash(HashType::Sha1, _) => MATCHY_ITEM_TYPE_SHA1,
ExtractedItem::Hash(HashType::Sha256, _) => MATCHY_ITEM_TYPE_SHA256,
ExtractedItem::Hash(HashType::Sha384, _) => MATCHY_ITEM_TYPE_SHA384,
ExtractedItem::Hash(HashType::Sha512, _) => MATCHY_ITEM_TYPE_SHA512,
ExtractedItem::Bitcoin(_) => MATCHY_ITEM_TYPE_BITCOIN,
ExtractedItem::Ethereum(_) => MATCHY_ITEM_TYPE_ETHEREUM,
ExtractedItem::Monero(_) => MATCHY_ITEM_TYPE_MONERO,
}
}
#[no_mangle]
pub extern "C" fn matchy_extractor_create(flags: u32) -> *mut matchy_extractor_t {
let builder = ExtractorBuilder::new()
.extract_domains((flags & MATCHY_EXTRACT_DOMAINS) != 0)
.extract_emails((flags & MATCHY_EXTRACT_EMAILS) != 0)
.extract_ipv4((flags & MATCHY_EXTRACT_IPV4) != 0)
.extract_ipv6((flags & MATCHY_EXTRACT_IPV6) != 0)
.extract_hashes((flags & MATCHY_EXTRACT_HASHES) != 0)
.extract_bitcoin((flags & MATCHY_EXTRACT_BITCOIN) != 0)
.extract_ethereum((flags & MATCHY_EXTRACT_ETHEREUM) != 0)
.extract_monero((flags & MATCHY_EXTRACT_MONERO) != 0);
match builder.build() {
Ok(extractor) => matchy_extractor_t::from_internal(Box::new(extractor)),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_extractor_extract_chunk(
extractor: *const matchy_extractor_t,
data: *const u8,
len: usize,
matches: *mut matchy_matches_t,
) -> i32 {
if extractor.is_null() || data.is_null() || matches.is_null() {
return MATCHY_ERROR_INVALID_PARAM;
}
let ext = matchy_extractor_t::as_internal(extractor);
let chunk = slice::from_raw_parts(data, len);
let rust_matches = ext.extract_from_chunk(chunk);
let mut strings = Vec::with_capacity(rust_matches.len());
let mut c_matches = Vec::with_capacity(rust_matches.len());
for m in rust_matches {
let value_str = m.item.as_value();
let c_string = match CString::new(value_str) {
Ok(s) => s,
Err(_) => continue, };
c_matches.push(matchy_match_t {
item_type: item_type_from_extracted(&m.item),
value: c_string.as_ptr(),
start: m.span.0,
end: m.span.1,
});
strings.push(c_string);
}
let internal = Box::new(MatchesInternal {
matches: c_matches,
strings,
});
(*matches).items = internal.matches.as_ptr();
(*matches).count = internal.matches.len();
(*matches)._internal = Box::into_raw(internal).cast::<c_void>();
MATCHY_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn matchy_matches_free(matches: *mut matchy_matches_t) {
if matches.is_null() {
return;
}
if !(*matches)._internal.is_null() {
let _ = Box::from_raw((*matches)._internal.cast::<MatchesInternal>());
(*matches)._internal = ptr::null_mut();
(*matches).items = ptr::null();
(*matches).count = 0;
}
}
#[no_mangle]
pub unsafe extern "C" fn matchy_extractor_free(extractor: *mut matchy_extractor_t) {
if !extractor.is_null() {
let _ = matchy_extractor_t::to_internal(extractor);
}
}
#[no_mangle]
pub extern "C" fn matchy_item_type_name(item_type: u8) -> *const c_char {
static DOMAIN: &[u8] = b"Domain\0";
static EMAIL: &[u8] = b"Email\0";
static IPV4: &[u8] = b"IPv4\0";
static IPV6: &[u8] = b"IPv6\0";
static MD5: &[u8] = b"MD5\0";
static SHA1: &[u8] = b"SHA1\0";
static SHA256: &[u8] = b"SHA256\0";
static SHA384: &[u8] = b"SHA384\0";
static SHA512: &[u8] = b"SHA512\0";
static BITCOIN: &[u8] = b"Bitcoin\0";
static ETHEREUM: &[u8] = b"Ethereum\0";
static MONERO: &[u8] = b"Monero\0";
static UNKNOWN: &[u8] = b"Unknown\0";
let name = match item_type {
MATCHY_ITEM_TYPE_DOMAIN => DOMAIN,
MATCHY_ITEM_TYPE_EMAIL => EMAIL,
MATCHY_ITEM_TYPE_IPV4 => IPV4,
MATCHY_ITEM_TYPE_IPV6 => IPV6,
MATCHY_ITEM_TYPE_MD5 => MD5,
MATCHY_ITEM_TYPE_SHA1 => SHA1,
MATCHY_ITEM_TYPE_SHA256 => SHA256,
MATCHY_ITEM_TYPE_SHA384 => SHA384,
MATCHY_ITEM_TYPE_SHA512 => SHA512,
MATCHY_ITEM_TYPE_BITCOIN => BITCOIN,
MATCHY_ITEM_TYPE_ETHEREUM => ETHEREUM,
MATCHY_ITEM_TYPE_MONERO => MONERO,
_ => UNKNOWN,
};
name.as_ptr().cast::<c_char>()
}