#![allow(clippy::missing_safety_doc)]
#![expect(
clippy::undocumented_unsafe_blocks,
reason = "module-wide FFI safety contract documented in the # Safety preamble above"
)]
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::c_char;
use std::os::raw::c_int;
use serde_json::{json, Value};
use super::NetError;
use crate::adapter::net::behavior::{
ClauseTrace, EvalContext, PredicateDebugReport, PredicateWire, Tag,
};
fn clause_trace_to_wire(t: &ClauseTrace) -> Value {
json!({
"label": t.label,
"result": t.result,
"children": t.children.iter().map(clause_trace_to_wire).collect::<Vec<_>>(),
})
}
fn report_to_wire(report: &PredicateDebugReport) -> Value {
let stats: Vec<Value> = report
.clause_stats
.values()
.map(|s| {
json!({
"label": s.label,
"evaluated": s.evaluated,
"matched": s.matched,
})
})
.collect();
json!({
"total_candidates": report.total_candidates,
"matched": report.matched,
"clause_stats": stats,
})
}
fn parse_tag_array(tags_json_str: &str) -> Option<Vec<Tag>> {
let strings: Vec<String> = serde_json::from_str(tags_json_str).ok()?;
strings
.iter()
.map(|s| Tag::parse(s))
.collect::<Result<_, _>>()
.ok()
}
fn parse_metadata(metadata_json_str: &str) -> Option<BTreeMap<String, String>> {
serde_json::from_str(metadata_json_str).ok()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_evaluate_with_trace(
predicate_json: *const c_char,
tags_json: *const c_char,
metadata_json: *const c_char,
out_result: *mut c_int,
out_trace_json: *mut *mut c_char,
out_trace_len: *mut usize,
) -> c_int {
if predicate_json.is_null()
|| tags_json.is_null()
|| metadata_json.is_null()
|| out_result.is_null()
|| out_trace_json.is_null()
|| out_trace_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 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 Some(tags) = parse_tag_array(&tags_s) else {
return NetError::InvalidJson.into();
};
let Some(metadata) = parse_metadata(&meta_s) else {
return NetError::InvalidJson.into();
};
let ctx = EvalContext::new(&tags, &metadata);
let (result, trace) = predicate.evaluate_with_trace(&ctx);
unsafe {
*out_result = if result { 1 } else { 0 };
}
let payload = clause_trace_to_wire(&trace);
super::mesh::write_string_out(
serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
out_trace_json,
out_trace_len,
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_aggregate_debug_report(
predicate_json: *const c_char,
contexts_json: *const c_char,
out_report_json: *mut *mut c_char,
out_report_len: *mut usize,
) -> c_int {
if predicate_json.is_null()
|| contexts_json.is_null()
|| out_report_json.is_null()
|| out_report_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 ctx_s = match unsafe { super::mesh::c_str_to_string(contexts_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(),
};
#[derive(serde::Deserialize)]
struct CtxJson {
tags: Vec<String>,
metadata: BTreeMap<String, String>,
}
let raw_contexts: Vec<CtxJson> = match serde_json::from_str(&ctx_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let mut owned: Vec<(Vec<Tag>, BTreeMap<String, String>)> =
Vec::with_capacity(raw_contexts.len());
for c in raw_contexts {
let tags: Result<Vec<Tag>, _> = c.tags.iter().map(|s| Tag::parse(s)).collect();
let Ok(tags) = tags else {
return NetError::InvalidJson.into();
};
owned.push((tags, c.metadata));
}
let report = PredicateDebugReport::from_evaluations(
&predicate,
owned
.iter()
.map(|(tags, meta)| EvalContext::new(tags, meta)),
);
let payload = report_to_wire(&report);
super::mesh::write_string_out(
serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
out_report_json,
out_report_len,
)
}
fn strip_label<'a>(label: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
label
.strip_prefix(prefix)
.and_then(|rest| rest.strip_suffix(suffix))
}
fn find_redactable_key_split(
inner: &str,
separator: &str,
keys: &BTreeSet<String>,
) -> Option<usize> {
let mut search_start = 0usize;
while let Some(rel) = inner[search_start..].find(separator) {
let abs = search_start + rel;
if keys.contains(&inner[..abs]) {
return Some(abs);
}
search_start = abs + separator.len();
if search_start > inner.len() {
break;
}
}
None
}
fn redact_label(label: &str, keys: &BTreeSet<String>) -> String {
if let Some(inner) = strip_label(label, "MetadataEquals(", ")") {
if let Some(eq_idx) = find_redactable_key_split(inner, "=", keys) {
let key = &inner[..eq_idx];
return format!("MetadataEquals({key}=<redacted>)");
}
return label.to_string();
}
if let Some(inner) = strip_label(label, "MetadataMatches(", ")") {
let needle = " contains \"";
if let Some(at) = find_redactable_key_split(inner, needle, keys) {
if inner.ends_with('"') {
let key = &inner[..at];
return format!("MetadataMatches({key} contains \"<redacted>\")");
}
}
return label.to_string();
}
if let Some(inner) = strip_label(label, "MetadataNumericAtLeast(", ")") {
let needle = " >= ";
if let Some(at) = find_redactable_key_split(inner, needle, keys) {
let key = &inner[..at];
return format!("MetadataNumericAtLeast({key} >= <redacted>)");
}
return label.to_string();
}
label.to_string()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_redact_metadata_keys(
report_json: *const c_char,
keys_json: *const c_char,
out_redacted_json: *mut *mut c_char,
out_redacted_len: *mut usize,
) -> c_int {
if report_json.is_null()
|| keys_json.is_null()
|| out_redacted_json.is_null()
|| out_redacted_len.is_null()
{
return NetError::NullPointer.into();
}
let report_s = match unsafe { super::mesh::c_str_to_string(report_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let keys_s = match unsafe { super::mesh::c_str_to_string(keys_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let report: Value = match serde_json::from_str(&report_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let keys_vec: Vec<String> = match serde_json::from_str(&keys_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let keys: BTreeSet<String> = keys_vec.into_iter().collect();
let stats = match report.get("clause_stats").and_then(|s| s.as_array()) {
Some(s) => s,
None => return NetError::InvalidJson.into(),
};
let mut merged: BTreeMap<String, (u64, u64)> = BTreeMap::new();
for entry in stats {
let label = match entry.get("label").and_then(|l| l.as_str()) {
Some(l) => l.to_string(),
None => return NetError::InvalidJson.into(),
};
let evaluated = entry.get("evaluated").and_then(|n| n.as_u64()).unwrap_or(0);
let matched = entry.get("matched").and_then(|n| n.as_u64()).unwrap_or(0);
let new_label = redact_label(&label, &keys);
let slot = merged.entry(new_label).or_insert((0, 0));
slot.0 += evaluated;
slot.1 += matched;
}
let new_stats: Vec<Value> = merged
.into_iter()
.map(|(label, (evaluated, matched))| {
json!({
"label": label,
"evaluated": evaluated,
"matched": matched,
})
})
.collect();
let total = report
.get("total_candidates")
.and_then(|n| n.as_u64())
.unwrap_or(0);
let matched = report.get("matched").and_then(|n| n.as_u64()).unwrap_or(0);
let payload = json!({
"total_candidates": total,
"matched": matched,
"clause_stats": new_stats,
});
super::mesh::write_string_out(
serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
out_redacted_json,
out_redacted_len,
)
}
fn redact_trace_value(node: &Value, keys: &BTreeSet<String>) -> Value {
let label = node
.get("label")
.and_then(|l| l.as_str())
.unwrap_or_default();
let result = node.get("result").cloned().unwrap_or(Value::Null);
let children: Vec<Value> = node
.get("children")
.and_then(|c| c.as_array())
.map(|arr| arr.iter().map(|c| redact_trace_value(c, keys)).collect())
.unwrap_or_default();
json!({
"label": redact_label(label, keys),
"result": result,
"children": children,
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_predicate_redact_trace_metadata_keys(
trace_json: *const c_char,
keys_json: *const c_char,
out_redacted_json: *mut *mut c_char,
out_redacted_len: *mut usize,
) -> c_int {
if trace_json.is_null()
|| keys_json.is_null()
|| out_redacted_json.is_null()
|| out_redacted_len.is_null()
{
return NetError::NullPointer.into();
}
let trace_s = match unsafe { super::mesh::c_str_to_string(trace_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let keys_s = match unsafe { super::mesh::c_str_to_string(keys_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let trace: Value = match serde_json::from_str(&trace_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let keys_vec: Vec<String> = match serde_json::from_str(&keys_s) {
Ok(v) => v,
Err(_) => return NetError::InvalidJson.into(),
};
let keys: BTreeSet<String> = keys_vec.into_iter().collect();
let redacted = redact_trace_value(&trace, &keys);
super::mesh::write_string_out(
serde_json::to_string(&redacted).unwrap_or_else(|_| "{}".to_string()),
out_redacted_json,
out_redacted_len,
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::{CStr, CString};
fn read_and_free(ptr: *mut c_char) -> String {
assert!(!ptr.is_null());
let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_string();
unsafe {
let _ = CString::from_raw(ptr);
}
s
}
#[test]
fn evaluate_with_trace_records_full_tree() {
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 mut result: c_int = -1;
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
net_predicate_evaluate_with_trace(
pred.as_ptr(),
tags.as_ptr(),
meta.as_ptr(),
&mut result,
&mut out_ptr,
&mut out_len,
)
};
assert_eq!(rc, 0);
assert_eq!(result, 1);
let trace_json = read_and_free(out_ptr);
let v: Value = serde_json::from_str(&trace_json).unwrap();
assert!(v["label"].as_str().unwrap().starts_with("And"));
assert_eq!(v["result"], true);
let children = v["children"].as_array().unwrap();
assert_eq!(children.len(), 2);
assert!(children.iter().all(|c| c["result"] == true));
}
#[test]
fn aggregate_debug_report_rolls_up_per_clause_stats() {
let pred = CString::new(
r#"{"nodes":[
{"kind":"metadata_equals","key":"region","value":"us-east"}
],"root_idx":0}"#,
)
.unwrap();
let contexts = CString::new(
r#"[
{"tags":[],"metadata":{"region":"us-east"}},
{"tags":[],"metadata":{"region":"us-west"}},
{"tags":[],"metadata":{"region":"us-east"}}
]"#,
)
.unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
net_predicate_aggregate_debug_report(
pred.as_ptr(),
contexts.as_ptr(),
&mut out_ptr,
&mut out_len,
)
};
assert_eq!(rc, 0);
let report_json = read_and_free(out_ptr);
let v: Value = serde_json::from_str(&report_json).unwrap();
assert_eq!(v["total_candidates"], 3);
assert_eq!(v["matched"], 2);
let stats = v["clause_stats"].as_array().unwrap();
assert_eq!(stats.len(), 1);
assert_eq!(stats[0]["evaluated"], 3);
assert_eq!(stats[0]["matched"], 2);
}
#[test]
fn redact_metadata_keys_rewrites_targeted_labels() {
let report = CString::new(
r#"{
"total_candidates": 10,
"matched": 4,
"clause_stats": [
{"label": "MetadataEquals(api_key=sk-secret-1)", "evaluated": 10, "matched": 4},
{"label": "MetadataEquals(region=us-east)", "evaluated": 10, "matched": 7},
{"label": "Exists(hardware.gpu)", "evaluated": 10, "matched": 8}
]
}"#,
)
.unwrap();
let keys = CString::new(r#"["api_key"]"#).unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
net_predicate_redact_metadata_keys(
report.as_ptr(),
keys.as_ptr(),
&mut out_ptr,
&mut out_len,
)
};
assert_eq!(rc, 0);
let redacted = read_and_free(out_ptr);
let v: Value = serde_json::from_str(&redacted).unwrap();
assert_eq!(v["total_candidates"], 10);
assert_eq!(v["matched"], 4);
let stats = v["clause_stats"].as_array().unwrap();
let labels: Vec<&str> = stats.iter().map(|s| s["label"].as_str().unwrap()).collect();
assert!(labels.contains(&"MetadataEquals(api_key=<redacted>)"));
assert!(labels.contains(&"MetadataEquals(region=us-east)"));
assert!(labels.contains(&"Exists(hardware.gpu)"));
}
#[test]
fn redact_metadata_keys_is_idempotent() {
let report = CString::new(
r#"{
"total_candidates": 5,
"matched": 2,
"clause_stats": [
{"label": "MetadataEquals(secret=foo)", "evaluated": 5, "matched": 2}
]
}"#,
)
.unwrap();
let keys = CString::new(r#"["secret"]"#).unwrap();
let mut out1: *mut c_char = std::ptr::null_mut();
let mut len1: usize = 0;
unsafe {
net_predicate_redact_metadata_keys(report.as_ptr(), keys.as_ptr(), &mut out1, &mut len1)
};
let pass1 = read_and_free(out1);
let pass1_cs = CString::new(pass1.clone()).unwrap();
let mut out2: *mut c_char = std::ptr::null_mut();
let mut len2: usize = 0;
unsafe {
net_predicate_redact_metadata_keys(
pass1_cs.as_ptr(),
keys.as_ptr(),
&mut out2,
&mut len2,
)
};
let pass2 = read_and_free(out2);
assert_eq!(pass1, pass2, "redaction must be idempotent");
}
#[test]
fn redact_label_handles_keys_containing_separator() {
let mut keys = BTreeSet::new();
keys.insert("weird=key".to_string());
let label = "MetadataEquals(weird=key=sk-secret)";
let redacted = redact_label(label, &keys);
assert_eq!(redacted, "MetadataEquals(weird=key=<redacted>)");
assert!(
!redacted.contains("sk-secret"),
"secret leaked through label-parser heuristic: {redacted}"
);
let mut keys = BTreeSet::new();
keys.insert("a >= b".to_string());
let label = "MetadataNumericAtLeast(a >= b >= 42)";
let redacted = redact_label(label, &keys);
assert_eq!(redacted, "MetadataNumericAtLeast(a >= b >= <redacted>)");
let label = "MetadataEquals(region=us-east)";
let redacted = redact_label(label, &keys);
assert_eq!(redacted, label);
}
#[test]
fn redact_trace_metadata_keys_rewrites_recursively() {
let trace = CString::new(
r#"{
"label": "And(2)",
"result": true,
"children": [
{"label": "MetadataEquals(api_key=sk-secret-1)", "result": true, "children": []},
{"label": "Exists(hardware.gpu)", "result": true, "children": []}
]
}"#,
)
.unwrap();
let keys = CString::new(r#"["api_key"]"#).unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
net_predicate_redact_trace_metadata_keys(
trace.as_ptr(),
keys.as_ptr(),
&mut out_ptr,
&mut out_len,
)
};
assert_eq!(rc, 0);
let redacted = read_and_free(out_ptr);
let v: Value = serde_json::from_str(&redacted).unwrap();
assert_eq!(v["label"], "And(2)");
assert_eq!(v["result"], true);
let children = v["children"].as_array().unwrap();
assert_eq!(children.len(), 2);
assert_eq!(
children[0]["label"], "MetadataEquals(api_key=<redacted>)",
"targeted leaf must be redacted"
);
assert_eq!(
children[1]["label"], "Exists(hardware.gpu)",
"non-metadata leaf must pass through"
);
assert!(
!redacted.contains("sk-secret-1"),
"secret value still present in redacted trace: {redacted}"
);
}
#[test]
fn redact_trace_metadata_keys_is_idempotent() {
let trace = CString::new(
r#"{
"label": "MetadataEquals(secret=foo)",
"result": false,
"children": []
}"#,
)
.unwrap();
let keys = CString::new(r#"["secret"]"#).unwrap();
let mut out1: *mut c_char = std::ptr::null_mut();
let mut len1: usize = 0;
unsafe {
net_predicate_redact_trace_metadata_keys(
trace.as_ptr(),
keys.as_ptr(),
&mut out1,
&mut len1,
)
};
let pass1 = read_and_free(out1);
let pass1_cs = CString::new(pass1.clone()).unwrap();
let mut out2: *mut c_char = std::ptr::null_mut();
let mut len2: usize = 0;
unsafe {
net_predicate_redact_trace_metadata_keys(
pass1_cs.as_ptr(),
keys.as_ptr(),
&mut out2,
&mut len2,
)
};
let pass2 = read_and_free(out2);
assert_eq!(pass1, pass2);
}
#[test]
fn null_inputs_return_null_pointer_across_all_three() {
let pred = CString::new(r#"{"nodes":[],"root_idx":0}"#).unwrap();
let tags = CString::new(r#"[]"#).unwrap();
let meta = CString::new(r#"{}"#).unwrap();
let ctxs = CString::new(r#"[]"#).unwrap();
let report =
CString::new(r#"{"total_candidates":0,"matched":0,"clause_stats":[]}"#).unwrap();
let keys = CString::new(r#"[]"#).unwrap();
let mut result: c_int = 0;
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
assert!(
unsafe {
net_predicate_evaluate_with_trace(
std::ptr::null(),
tags.as_ptr(),
meta.as_ptr(),
&mut result,
&mut out_ptr,
&mut out_len,
)
} < 0
);
assert!(
unsafe {
net_predicate_aggregate_debug_report(
pred.as_ptr(),
std::ptr::null(),
&mut out_ptr,
&mut out_len,
)
} < 0
);
assert!(
unsafe {
net_predicate_redact_metadata_keys(
report.as_ptr(),
std::ptr::null(),
&mut out_ptr,
&mut out_len,
)
} < 0
);
assert!(
unsafe {
net_predicate_redact_metadata_keys(
std::ptr::null(),
keys.as_ptr(),
&mut out_ptr,
&mut out_len,
)
} < 0
);
let _ = ctxs;
}
}