use super::vtables::{FfiBootstrapEvent, FfiQueryResult, FfiSourceEvent};
use bytes::Bytes;
use chrono::DateTime;
use drasi_core::models::SourceChange;
use drasi_lib::channels::events::{BootstrapEvent, SourceEvent, SourceEventWrapper};
use serde::{Deserialize, Serialize};
pub const MAX_FFI_PAYLOAD_BYTES: usize = 256 * 1024 * 1024;
pub unsafe fn take_ffi_payload<T>(
ptr: *const u8,
len: usize,
drop_fn: Option<extern "C" fn(*mut u8, usize)>,
decode: impl FnOnce(&[u8]) -> Option<T>,
) -> Option<T> {
let decoded = if ptr.is_null() || len == 0 {
None
} else if len > MAX_FFI_PAYLOAD_BYTES {
log::error!("Rejecting oversized FFI payload ({len} bytes > {MAX_FFI_PAYLOAD_BYTES} max)");
None
} else {
decode(unsafe { std::slice::from_raw_parts(ptr, len) })
};
if !ptr.is_null() {
match drop_fn {
Some(free) => free(ptr as *mut u8, len),
None => {
log::error!("FFI contract violation: null payload_drop_fn; leaking {len} bytes")
}
}
}
decoded
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SourceEventPayload {
pub source_id: String,
pub event: SourceEvent,
pub timestamp_us: i64,
pub sequence: Option<u64>,
pub source_position: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BootstrapEventPayload {
pub source_id: String,
pub change: SourceChange,
pub timestamp_us: i64,
pub sequence: u64,
}
impl SourceEventPayload {
pub fn from_wrapper(wrapper: &SourceEventWrapper) -> Self {
let timestamp_us = wrapper
.timestamp
.timestamp_nanos_opt()
.map(|n| n / 1000)
.unwrap_or(0);
Self {
source_id: wrapper.source_id.clone(),
event: wrapper.event.clone(),
timestamp_us,
sequence: wrapper.sequence,
source_position: wrapper.source_position.as_ref().map(|b| b.to_vec()),
}
}
pub fn into_wrapper(self) -> SourceEventWrapper {
let timestamp = DateTime::from_timestamp_micros(self.timestamp_us)
.unwrap_or(DateTime::UNIX_EPOCH);
SourceEventWrapper {
source_id: self.source_id,
event: self.event,
timestamp,
profiling: None,
sequence: self.sequence,
source_position: self.source_position.map(Bytes::from),
}
}
}
impl BootstrapEventPayload {
pub fn from_event(record: &BootstrapEvent) -> Self {
let timestamp_us = record
.timestamp
.timestamp_nanos_opt()
.map(|n| n / 1000)
.unwrap_or(0);
Self {
source_id: record.source_id.clone(),
change: record.change.clone(),
timestamp_us,
sequence: record.sequence,
}
}
pub fn into_event(self) -> BootstrapEvent {
let timestamp = DateTime::from_timestamp_micros(self.timestamp_us)
.unwrap_or(DateTime::UNIX_EPOCH);
BootstrapEvent {
source_id: self.source_id,
change: self.change,
timestamp,
sequence: self.sequence,
}
}
}
pub fn decode_source_event_payload(bytes: &[u8]) -> Option<SourceEventWrapper> {
match rmp_serde::from_slice::<SourceEventPayload>(bytes) {
Ok(p) => Some(p.into_wrapper()),
Err(e) => {
log::error!("Failed to decode FFI source event payload: {e}");
None
}
}
}
pub fn decode_bootstrap_event_payload(bytes: &[u8]) -> Option<BootstrapEvent> {
match rmp_serde::from_slice::<BootstrapEventPayload>(bytes) {
Ok(p) => Some(p.into_event()),
Err(e) => {
log::error!("Failed to decode FFI bootstrap event payload: {e}");
None
}
}
}
pub fn encode_query_result(result: &drasi_lib::channels::QueryResult) -> Vec<u8> {
rmp_serde::to_vec_named(result)
.expect("BUG: failed to encode FFI query result payload (non-serializable field added?)")
}
pub fn decode_query_result(bytes: &[u8]) -> Option<drasi_lib::channels::QueryResult> {
match rmp_serde::from_slice::<drasi_lib::channels::QueryResult>(bytes) {
Ok(r) => Some(r),
Err(e) => {
log::error!("Failed to decode FFI query result payload: {e}");
None
}
}
}
pub unsafe fn consume_source_event(ffi: &FfiSourceEvent) -> Option<SourceEventWrapper> {
unsafe {
take_ffi_payload(
ffi.payload_ptr,
ffi.payload_len,
ffi.payload_drop_fn,
decode_source_event_payload,
)
}
}
pub unsafe fn consume_bootstrap_event(ffi: &FfiBootstrapEvent) -> Option<BootstrapEvent> {
unsafe {
take_ffi_payload(
ffi.payload_ptr,
ffi.payload_len,
ffi.payload_drop_fn,
decode_bootstrap_event_payload,
)
}
}
pub unsafe fn consume_query_result(
ptr: *mut FfiQueryResult,
) -> Option<drasi_lib::channels::QueryResult> {
let ffi = unsafe { &*ptr };
let decoded = unsafe {
take_ffi_payload(
ffi.payload_ptr,
ffi.payload_len,
ffi.payload_drop_fn,
decode_query_result,
)
};
unsafe { drop(Box::from_raw(ptr)) };
decoded
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use chrono::Utc;
use drasi_core::models::{
Element, ElementMetadata, ElementPropertyMap, ElementReference, ElementValue, SourceChange,
};
use drasi_lib::channels::events::SourceEvent;
use std::sync::Arc;
fn sample_node() -> Element {
let mut props = ElementPropertyMap::new();
props.insert("plate", ElementValue::String(Arc::from("A1234")));
props.insert("speed", ElementValue::Integer(55));
Element::Node {
metadata: ElementMetadata {
reference: ElementReference::new("src-1", "vehicles:A1234"),
labels: Arc::from(vec![Arc::from("vehicles")]),
effective_from: 1_771_000_000_000,
},
properties: props,
}
}
#[test]
fn source_event_payload_roundtrips_via_named_encoding() {
let wrapper = SourceEventWrapper {
source_id: "src-1".to_string(),
event: SourceEvent::Change(SourceChange::Insert {
element: sample_node(),
}),
timestamp: Utc::now(),
profiling: None,
sequence: Some(42),
source_position: Some(Bytes::from_static(b"binlog:000003:1766")),
};
let payload = SourceEventPayload::from_wrapper(&wrapper);
let bytes = rmp_serde::to_vec_named(&payload).expect("serialize");
let decoded = decode_source_event_payload(&bytes).expect("decode");
assert_eq!(decoded.source_id, wrapper.source_id);
assert_eq!(decoded.sequence, wrapper.sequence);
assert_eq!(decoded.event, wrapper.event);
assert_eq!(
decoded.source_position.as_deref(),
Some(&b"binlog:000003:1766"[..])
);
assert_eq!(
decoded.timestamp.timestamp_micros(),
wrapper.timestamp.timestamp_micros()
);
}
#[test]
fn bootstrap_event_payload_roundtrips() {
let record = BootstrapEvent {
source_id: "src-1".to_string(),
change: SourceChange::Delete {
metadata: ElementMetadata {
reference: ElementReference::new("src-1", "vehicles:B5678"),
labels: Arc::from(vec![Arc::from("vehicles")]),
effective_from: 9,
},
},
timestamp: Utc::now(),
sequence: 7,
};
let payload = BootstrapEventPayload::from_event(&record);
let bytes = rmp_serde::to_vec_named(&payload).expect("serialize");
let decoded = decode_bootstrap_event_payload(&bytes).expect("decode");
assert_eq!(decoded.source_id, record.source_id);
assert_eq!(decoded.sequence, record.sequence);
assert_eq!(decoded.change, record.change);
}
#[test]
fn query_result_roundtrips_with_skipped_profiling_field() {
let qr = drasi_lib::channels::QueryResult::new(
"q-1".to_string(),
3,
Utc::now(),
Vec::new(),
std::collections::HashMap::new(),
);
assert!(qr.profiling.is_none());
let bytes = encode_query_result(&qr);
let decoded = decode_query_result(&bytes).expect("decode QueryResult");
assert_eq!(decoded.query_id, qr.query_id);
assert_eq!(decoded.sequence, qr.sequence);
assert!(decoded.profiling.is_none());
}
use std::sync::atomic::{AtomicUsize, Ordering};
static DROPS_DECODE: AtomicUsize = AtomicUsize::new(0);
static DROPS_OVERSIZED: AtomicUsize = AtomicUsize::new(0);
extern "C" fn count_drop_decode(_ptr: *mut u8, _len: usize) {
DROPS_DECODE.fetch_add(1, Ordering::SeqCst);
}
extern "C" fn count_drop_oversized(_ptr: *mut u8, _len: usize) {
DROPS_OVERSIZED.fetch_add(1, Ordering::SeqCst);
}
fn encoded_source_event() -> Vec<u8> {
let wrapper = SourceEventWrapper {
source_id: "src-1".to_string(),
event: SourceEvent::Change(SourceChange::Insert {
element: sample_node(),
}),
timestamp: Utc::now(),
profiling: None,
sequence: Some(1),
source_position: None,
};
let payload = SourceEventPayload::from_wrapper(&wrapper);
rmp_serde::to_vec_named(&payload).unwrap()
}
#[test]
fn take_ffi_payload_decodes_and_calls_drop() {
let bytes = encoded_source_event();
let len = bytes.len();
let ptr = Box::into_raw(bytes.into_boxed_slice()) as *const u8;
let decoded = unsafe {
take_ffi_payload(
ptr,
len,
Some(count_drop_decode),
decode_source_event_payload,
)
};
assert!(decoded.is_some());
assert_eq!(DROPS_DECODE.load(Ordering::SeqCst), 1);
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr as *mut u8,
len,
)))
};
}
#[test]
fn take_ffi_payload_rejects_oversized_without_reading() {
let bytes = vec![0u8; 16];
let real_len = bytes.len();
let ptr = Box::into_raw(bytes.into_boxed_slice()) as *const u8;
let decoded = unsafe {
take_ffi_payload(
ptr,
MAX_FFI_PAYLOAD_BYTES + 1,
Some(count_drop_oversized),
decode_source_event_payload,
)
};
assert!(decoded.is_none(), "oversized payload must be rejected");
assert_eq!(
DROPS_OVERSIZED.load(Ordering::SeqCst),
1,
"buffer must still be freed"
);
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr as *mut u8,
real_len,
)))
};
}
#[test]
fn take_ffi_payload_null_drop_fn_does_not_crash() {
let bytes = encoded_source_event();
let len = bytes.len();
let ptr = Box::into_raw(bytes.into_boxed_slice()) as *const u8;
let decoded = unsafe { take_ffi_payload(ptr, len, None, decode_source_event_payload) };
assert!(
decoded.is_some(),
"decode should still succeed; only the free is skipped"
);
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr as *mut u8,
len,
)))
};
}
static QR_DROPS_VALID: AtomicUsize = AtomicUsize::new(0);
static QR_DROPS_INVALID: AtomicUsize = AtomicUsize::new(0);
extern "C" fn count_drop_qr_valid(ptr: *mut u8, len: usize) {
QR_DROPS_VALID.fetch_add(1, Ordering::SeqCst);
free_raw_bytes(ptr, len);
}
extern "C" fn count_drop_qr_invalid(ptr: *mut u8, len: usize) {
QR_DROPS_INVALID.fetch_add(1, Ordering::SeqCst);
free_raw_bytes(ptr, len);
}
fn free_raw_bytes(ptr: *mut u8, len: usize) {
if !ptr.is_null() && len > 0 {
unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))) };
}
}
fn make_ffi_query_result(
bytes: Vec<u8>,
drop_fn: extern "C" fn(*mut u8, usize),
) -> *mut FfiQueryResult {
let payload_len = bytes.len();
let payload_ptr = Box::into_raw(bytes.into_boxed_slice()) as *const u8;
Box::into_raw(Box::new(FfiQueryResult {
payload_ptr,
payload_len,
payload_drop_fn: Some(drop_fn),
}))
}
#[test]
fn consume_query_result_decodes_and_frees_buffer_once() {
let qr = drasi_lib::channels::QueryResult::new(
"q-1".to_string(),
7,
Utc::now(),
Vec::new(),
std::collections::HashMap::new(),
);
let raw = make_ffi_query_result(encode_query_result(&qr), count_drop_qr_valid);
let decoded = unsafe { consume_query_result(raw) }.expect("owned QueryResult");
assert_eq!(decoded.query_id, "q-1");
assert_eq!(decoded.sequence, 7);
assert_eq!(QR_DROPS_VALID.load(Ordering::SeqCst), 1);
}
#[test]
fn consume_query_result_handles_undecodable_payload_without_leak() {
let raw = make_ffi_query_result(vec![0xFFu8; 8], count_drop_qr_invalid);
let decoded = unsafe { consume_query_result(raw) };
assert!(decoded.is_none(), "garbage payload must not decode");
assert_eq!(QR_DROPS_INVALID.load(Ordering::SeqCst), 1);
}
}