#![allow(clippy::missing_safety_doc)]
#![expect(
clippy::undocumented_unsafe_blocks,
reason = "module-wide FFI safety contract documented in the # Safety preamble above"
)]
use std::ffi::c_char;
use std::os::raw::c_int;
use serde_json::{json, Value};
use super::NetError;
use crate::adapter::net::behavior::{
validate_capabilities, CapabilitySet, SchemaError, ValidationWarning, ValueType,
};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn net_validate_capabilities(
caps_json: *const c_char,
out_report_json: *mut *mut c_char,
out_report_len: *mut usize,
) -> c_int {
if caps_json.is_null() || out_report_json.is_null() || out_report_len.is_null() {
return NetError::NullPointer.into();
}
let caps_s = match unsafe { super::mesh::c_str_to_string(caps_json) } {
Some(s) => s,
None => return NetError::InvalidUtf8.into(),
};
let caps: CapabilitySet = match serde_json::from_str(&caps_s) {
Ok(c) => c,
Err(_) => return NetError::InvalidJson.into(),
};
let report = validate_capabilities(&caps);
let mut errors: Vec<Value> = report.errors.iter().map(schema_error_to_wire).collect();
let mut warnings: Vec<Value> = report
.warnings
.iter()
.map(validation_warning_to_wire)
.collect();
canonical_sort(&mut errors);
canonical_sort(&mut warnings);
let payload = json!({
"errors": errors,
"warnings": warnings,
});
super::mesh::write_string_out(
serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
out_report_json,
out_report_len,
)
}
fn value_type_to_wire(t: ValueType) -> &'static str {
match t {
ValueType::Presence => "presence",
ValueType::Number => "number",
ValueType::String => "string",
ValueType::Enumeration => "enumeration",
ValueType::Bool => "bool",
ValueType::Csv => "csv",
}
}
fn schema_error_to_wire(e: &SchemaError) -> Value {
match e {
SchemaError::UnknownAxis { axis_prefix, tag } => json!({
"kind": "unknown_axis",
"axis_prefix": axis_prefix,
"tag": tag,
}),
SchemaError::TypeMismatch {
axis,
key,
expected,
actual,
} => json!({
"kind": "type_mismatch",
"axis": axis.as_str(),
"key": key,
"expected": value_type_to_wire(*expected),
"actual": actual,
}),
SchemaError::IndexMalformed {
axis,
prefix,
index,
tag,
} => json!({
"kind": "index_malformed",
"axis": axis.as_str(),
"prefix": prefix,
"index": index,
"tag": tag,
}),
}
}
fn validation_warning_to_wire(w: &ValidationWarning) -> Value {
match w {
ValidationWarning::UnknownKey { axis, key } => json!({
"kind": "unknown_key",
"axis": axis.as_str(),
"key": key,
}),
ValidationWarning::MetadataOversize {
soft_cap_bytes,
actual_bytes,
} => json!({
"kind": "metadata_oversize",
"soft_cap_bytes": soft_cap_bytes,
"actual_bytes": actual_bytes,
}),
ValidationWarning::LegacyTag { tag } => json!({
"kind": "legacy_tag",
"tag": tag,
}),
ValidationWarning::MetadataReservedKey { key } => json!({
"kind": "metadata_reserved_key",
"key": key,
}),
ValidationWarning::MetadataReservedPrefix { key, prefix } => json!({
"kind": "metadata_reserved_prefix",
"key": key,
"prefix": prefix,
}),
}
}
fn canonical_sort(v: &mut [Value]) {
v.sort_by_key(|x| x.to_string());
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::{CStr, CString};
fn validate(caps_json: &str) -> String {
let cs = CString::new(caps_json).unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe { net_validate_capabilities(cs.as_ptr(), &mut out_ptr, &mut out_len) };
assert_eq!(rc, 0, "expected ok, got {rc}");
assert!(!out_ptr.is_null());
let out = unsafe { CStr::from_ptr(out_ptr) }
.to_str()
.unwrap()
.to_string();
unsafe {
let _ = CString::from_raw(out_ptr);
}
out
}
#[test]
fn empty_caps_produces_clean_report() {
let out = validate(r#"{"tags": [], "metadata": {}}"#);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["errors"].as_array().unwrap().len(), 0);
assert_eq!(v["warnings"].as_array().unwrap().len(), 0);
}
#[test]
fn unknown_axis_shape_surfaces_as_legacy_warning() {
let out = validate(r#"{"tags": ["compute.gpu"], "metadata": {}}"#);
let v: Value = serde_json::from_str(&out).unwrap();
let warnings = v["warnings"].as_array().unwrap();
assert_eq!(v["errors"].as_array().unwrap().len(), 0);
assert_eq!(warnings.len(), 1, "report = {v}");
assert_eq!(warnings[0]["kind"], "legacy_tag");
assert_eq!(warnings[0]["tag"], "compute.gpu");
}
#[test]
fn numeric_key_with_garbage_value_emits_type_mismatch() {
let out = validate(r#"{"tags": ["hardware.memory_gb=lots"], "metadata": {}}"#);
let v: Value = serde_json::from_str(&out).unwrap();
let errors = v["errors"].as_array().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0]["kind"], "type_mismatch");
assert_eq!(errors[0]["axis"], "hardware");
assert_eq!(errors[0]["expected"], "number");
assert_eq!(errors[0]["actual"], "lots");
}
#[test]
fn null_inputs_return_null_pointer() {
let cs = CString::new("{}").unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe { net_validate_capabilities(std::ptr::null(), &mut out_ptr, &mut out_len) };
assert!(rc < 0);
let rc =
unsafe { net_validate_capabilities(cs.as_ptr(), std::ptr::null_mut(), &mut out_len) };
assert!(rc < 0);
let rc =
unsafe { net_validate_capabilities(cs.as_ptr(), &mut out_ptr, std::ptr::null_mut()) };
assert!(rc < 0);
}
#[test]
fn malformed_caps_returns_invalid_json() {
let cs = CString::new(r#"{"tags": [not-json"#).unwrap();
let mut out_ptr: *mut c_char = std::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe { net_validate_capabilities(cs.as_ptr(), &mut out_ptr, &mut out_len) };
assert!(rc < 0);
}
}