#![allow(clippy::missing_safety_doc)]
#![expect(
clippy::undocumented_unsafe_blocks,
reason = "module-wide FFI safety contract documented in the # Safety preamble above"
)]
#![expect(
clippy::multiple_unsafe_ops_per_block,
reason = "FFI entry points deref input pointers together with out-parameter writes under the same caller contract"
)]
use std::collections::BTreeMap;
use std::ffi::c_char;
use std::os::raw::c_int;
use super::NetError;
use crate::adapter::net::behavior::{
predicate_to_rpc_header, EvalContext, PredicateWire, Tag, MAX_PREDICATE_RPC_HEADER_VALUE_LEN,
RPC_WHERE_HEADER,
};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_evaluate(
predicate_json: *const c_char,
tags_json: *const c_char,
metadata_json: *const c_char,
) -> c_int {
if predicate_json.is_null() || tags_json.is_null() || metadata_json.is_null() {
return NetError::NullPointer.into();
}
let pred_s = match unsafe { super::mesh::c_str_to_string(predicate_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let tags_s = match unsafe { super::mesh::c_str_to_string(tags_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let meta_s = match unsafe { super::mesh::c_str_to_string(metadata_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let wire: PredicateWire = match serde_json::from_str(&pred_s) {
Ok(w) => w,
Err(_) => return NetError::InvalidJson.into(),
};
let predicate = match wire.into_predicate() {
Ok(p) => p,
Err(_) => return NetError::InvalidJson.into(),
};
let tag_strings: Vec<String> = match serde_json::from_str(&tags_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let tags: Result<Vec<Tag>, _> = tag_strings.iter().map(|s| Tag::parse(s)).collect();
let tags = match tags {
Ok(t) => t,
Err(_) => return NetError::InvalidJson.into(),
};
let metadata: BTreeMap<String, String> = match serde_json::from_str(&meta_s) {
Ok(m) => m,
Err(_) => return NetError::InvalidJson.into(),
};
let ctx = EvalContext::new(&tags, &metadata);
if predicate.evaluate_unplanned(&ctx) {
1
} else {
0
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_to_where_header(
predicate_json: *const c_char,
out_header_name: *mut *mut c_char,
out_header_name_len: *mut usize,
out_value_ptr: *mut *mut c_char,
out_value_len: *mut usize,
) -> c_int {
if predicate_json.is_null()
|| out_header_name.is_null()
|| out_header_name_len.is_null()
|| out_value_ptr.is_null()
|| out_value_len.is_null()
{
return NetError::NullPointer.into();
}
let pred_s = match unsafe { super::mesh::c_str_to_string(predicate_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let wire: PredicateWire = match serde_json::from_str(&pred_s) {
Ok(w) => w,
Err(_) => return NetError::InvalidJson.into(),
};
let predicate = match wire.into_predicate() {
Ok(p) => p,
Err(_) => return NetError::InvalidJson.into(),
};
let (name, value_bytes) = match predicate_to_rpc_header(&predicate) {
Ok(pair) => pair,
Err(_) => return NetError::InvalidJson.into(),
};
debug_assert_eq!(name, RPC_WHERE_HEADER);
debug_assert!(value_bytes.len() <= MAX_PREDICATE_RPC_HEADER_VALUE_LEN);
let name_rc = super::mesh::write_string_out(name, out_header_name, out_header_name_len);
if name_rc != 0 {
return name_rc;
}
let value_string = match String::from_utf8(value_bytes) {
Ok(s) => s,
Err(_) => return NetError::InvalidUtf8.into(),
};
let value_rc = super::mesh::write_string_out(value_string, out_value_ptr, out_value_len);
if value_rc != 0 {
unsafe {
if !(*out_header_name).is_null() {
let _ = std::ffi::CString::from_raw(*out_header_name);
*out_header_name = std::ptr::null_mut();
*out_header_name_len = 0;
}
}
return value_rc;
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn evaluates_true_for_matching_context() {
let pred = CString::new(
r#"{"nodes":[
{"kind":"exists","key":{"axis":"hardware","key":"gpu"}},
{"kind":"metadata_equals","key":"region","value":"us-east"},
{"kind":"and","children":[0,1]}
],"root_idx":2}"#,
)
.unwrap();
let tags = CString::new(r#"["hardware.gpu"]"#).unwrap();
let meta = CString::new(r#"{"region":"us-east"}"#).unwrap();
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), tags.as_ptr(), meta.as_ptr()) };
assert_eq!(rc, 1);
}
#[test]
fn evaluates_false_when_metadata_differs() {
let pred = CString::new(
r#"{"nodes":[
{"kind":"metadata_equals","key":"region","value":"us-east"}
],"root_idx":0}"#,
)
.unwrap();
let tags = CString::new(r#"[]"#).unwrap();
let meta = CString::new(r#"{"region":"us-west"}"#).unwrap();
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), tags.as_ptr(), meta.as_ptr()) };
assert_eq!(rc, 0);
}
#[test]
fn returns_null_pointer_on_any_null_input() {
let pred = CString::new(r#"{"nodes":[],"root_idx":0}"#).unwrap();
let tags = CString::new(r#"[]"#).unwrap();
let meta = CString::new(r#"{}"#).unwrap();
let rc = unsafe { net_predicate_evaluate(std::ptr::null(), tags.as_ptr(), meta.as_ptr()) };
assert!(rc < 0);
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), std::ptr::null(), meta.as_ptr()) };
assert!(rc < 0);
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), tags.as_ptr(), std::ptr::null()) };
assert!(rc < 0);
}
#[test]
fn returns_invalid_json_on_unparseable_predicate() {
let pred = CString::new(r#"{"nodes":[],not-json"#).unwrap();
let tags = CString::new(r#"[]"#).unwrap();
let meta = CString::new(r#"{}"#).unwrap();
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), tags.as_ptr(), meta.as_ptr()) };
assert!(rc < 0);
}
#[test]
fn accepts_reserved_prefix_tags() {
let pred = CString::new(
r#"{"nodes":[
{"kind":"metadata_exists","key":"region"}
],"root_idx":0}"#,
)
.unwrap();
let tags = CString::new(r#"["scope:tenant:foo","hardware.gpu"]"#).unwrap();
let meta = CString::new(r#"{}"#).unwrap();
let rc = unsafe { net_predicate_evaluate(pred.as_ptr(), tags.as_ptr(), meta.as_ptr()) };
assert!(
rc >= 0,
"reserved-prefix tag must parse via privileged path, got {rc}",
);
}
#[test]
fn to_where_header_emits_canonical_name_and_round_trip_value() {
use std::ffi::CStr;
let pred = CString::new(
r#"{"nodes":[
{"kind":"exists","key":{"axis":"hardware","key":"gpu"}}
],"root_idx":0}"#,
)
.unwrap();
let mut out_name: *mut c_char = std::ptr::null_mut();
let mut name_len: usize = 0;
let mut out_value: *mut c_char = std::ptr::null_mut();
let mut value_len: usize = 0;
let rc = unsafe {
net_predicate_to_where_header(
pred.as_ptr(),
&mut out_name,
&mut name_len,
&mut out_value,
&mut value_len,
)
};
assert_eq!(rc, 0);
let name = unsafe { CStr::from_ptr(out_name) }
.to_str()
.unwrap()
.to_string();
assert_eq!(name, "net-where");
assert_eq!(name_len, "net-where".len());
let value = unsafe { CStr::from_ptr(out_value) }
.to_str()
.unwrap()
.to_string();
assert_eq!(value_len, value.len());
let parsed: PredicateWire = serde_json::from_str(&value).unwrap();
let original: PredicateWire = serde_json::from_str(
r#"{"nodes":[{"kind":"exists","key":{"axis":"hardware","key":"gpu"}}],"root_idx":0}"#,
)
.unwrap();
assert_eq!(parsed.nodes.len(), original.nodes.len());
assert_eq!(parsed.root_idx, original.root_idx);
unsafe {
let _ = CString::from_raw(out_name);
let _ = CString::from_raw(out_value);
}
}
#[test]
fn to_where_header_rejects_malformed_predicate() {
let pred = CString::new(r#"{"nodes":[],not-json"#).unwrap();
let mut out_name: *mut c_char = std::ptr::null_mut();
let mut name_len: usize = 0;
let mut out_value: *mut c_char = std::ptr::null_mut();
let mut value_len: usize = 0;
let rc = unsafe {
net_predicate_to_where_header(
pred.as_ptr(),
&mut out_name,
&mut name_len,
&mut out_value,
&mut value_len,
)
};
assert!(rc < 0);
assert!(out_name.is_null());
assert!(out_value.is_null());
}
#[test]
fn to_where_header_null_inputs_return_null_pointer() {
let pred = CString::new(r#"{"nodes":[],"root_idx":0}"#).unwrap();
let mut out_name: *mut c_char = std::ptr::null_mut();
let mut name_len: usize = 0;
let mut out_value: *mut c_char = std::ptr::null_mut();
let mut value_len: usize = 0;
let rc = unsafe {
net_predicate_to_where_header(
std::ptr::null(),
&mut out_name,
&mut name_len,
&mut out_value,
&mut value_len,
)
};
assert!(rc < 0);
let rc = unsafe {
net_predicate_to_where_header(
pred.as_ptr(),
std::ptr::null_mut(),
&mut name_len,
&mut out_value,
&mut value_len,
)
};
assert!(rc < 0);
}
}