use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use libloading::{Library, Symbol};
use crate::audit::{AuditContext, AuditRule, FnScan};
use crate::plugin_abi::{
abi_major, SdktAuditFindingC, SdktAuditReportC, SDKT_AUDIT_ABI_MAJOR, SEVERITY_CRITICAL,
SEVERITY_INFO,
};
use crate::types::{AuditReport, Finding, Severity};
#[derive(Debug)]
pub enum PluginLoadError {
Io(std::io::Error),
DlOpen(String),
SymbolMissing(String),
AbiMismatch {
plugin_major: u32,
host_major: u32,
},
InitFailed(c_int),
Panic,
}
impl std::fmt::Display for PluginLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginLoadError::Io(e) => write!(f, "io error: {}", e),
PluginLoadError::DlOpen(e) => write!(f, "failed to load plugin: {}", e),
PluginLoadError::SymbolMissing(s) => write!(f, "missing plugin symbol: {}", s),
PluginLoadError::AbiMismatch {
plugin_major,
host_major,
} => write!(
f,
"plugin ABI mismatch (plugin v{}.x, host v{}.x) — rebuild plugin against sdkt-audit v{}.x",
plugin_major, host_major, host_major
),
PluginLoadError::InitFailed(c) => write!(f, "plugin init failed (code {})", c),
PluginLoadError::Panic => write!(f, "plugin panicked during execution"),
}
}
}
impl std::error::Error for PluginLoadError {}
type AbiVersionFn = unsafe extern "C" fn() -> u32;
type IdFn = unsafe extern "C" fn() -> *const c_char;
type SeverityFn = unsafe extern "C" fn() -> u32;
type DescFn = unsafe extern "C" fn() -> *const c_char;
type InitFn = unsafe extern "C" fn(*const c_char) -> c_int;
type CheckFn = unsafe extern "C" fn(*mut SdktAuditReportC) -> c_int;
type FreeFn = unsafe extern "C" fn();
unsafe fn cstr_to_string(p: *const c_char) -> String {
if p.is_null() {
String::new()
} else {
CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
fn sev_to_enum(s: u32) -> Severity {
match s {
SEVERITY_CRITICAL => Severity::Critical,
SEVERITY_INFO => Severity::Info,
_ => Severity::Warning,
}
}
pub struct PluginRule {
_lib: Arc<Library>,
id: &'static str,
severity: Severity,
description: &'static str,
#[allow(dead_code)]
source: String,
check: CheckFn,
free: FreeFn,
}
impl Drop for PluginRule {
fn drop(&mut self) {
unsafe {
(self.free)();
}
let _resident = Arc::into_raw(Arc::clone(&self._lib));
}
}
impl PluginRule {
pub fn load(path: &Path, source: &str) -> Result<Self, PluginLoadError> {
let lib = Arc::new(
unsafe { Library::new(path) }.map_err(|e| PluginLoadError::DlOpen(e.to_string()))?,
);
let abi_fn: Symbol<AbiVersionFn> = unsafe {
lib.get(b"sdkt_plugin_abi_version\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let id_fn: Symbol<IdFn> = unsafe {
lib.get(b"sdkt_plugin_id\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let sev_fn: Symbol<SeverityFn> = unsafe {
lib.get(b"sdkt_plugin_severity\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let desc_fn: Symbol<DescFn> = unsafe {
lib.get(b"sdkt_plugin_description\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let init_fn: Symbol<InitFn> = unsafe {
lib.get(b"sdkt_plugin_init\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let check_fn: Symbol<CheckFn> = unsafe {
lib.get(b"sdkt_plugin_check\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let free_fn: Symbol<FreeFn> = unsafe {
lib.get(b"sdkt_plugin_free\0")
.map_err(|e| PluginLoadError::SymbolMissing(e.to_string()))?
};
let version = unsafe { abi_fn() };
let plugin_major = abi_major(version);
if plugin_major != SDKT_AUDIT_ABI_MAJOR {
return Err(PluginLoadError::AbiMismatch {
plugin_major,
host_major: SDKT_AUDIT_ABI_MAJOR,
});
}
let id: &'static str = Box::leak(unsafe { cstr_to_string(id_fn()) }.into_boxed_str());
let description: &'static str =
Box::leak(unsafe { cstr_to_string(desc_fn()) }.into_boxed_str());
let severity = sev_to_enum(unsafe { sev_fn() });
let c_src = CString::new(source).map_err(|_| {
PluginLoadError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"source contains an interior NUL byte",
))
})?;
let code = unsafe { init_fn(c_src.as_ptr()) };
if code != 0 {
return Err(PluginLoadError::InitFailed(code));
}
let check: CheckFn = *check_fn;
let free: FreeFn = *free_fn;
Ok(Self {
_lib: lib,
id,
severity,
description,
source: source.to_string(),
check,
free,
})
}
}
impl AuditRule for PluginRule {
fn id(&self) -> &'static str {
self.id
}
fn severity(&self) -> Severity {
self.severity
}
fn description(&self) -> &'static str {
self.description
}
fn check(&self, _scans: &[FnScan], _ctx: &AuditContext, report: &mut AuditReport) {
let mut buf = SdktAuditReportC::default();
let code = unsafe { (self.check)(&mut buf) };
if code != 0 {
return;
}
let safe_count = buf.count.min(crate::plugin_abi::MAX_FINDINGS);
for i in 0..safe_count {
let f: SdktAuditFindingC = buf.findings[i];
if f.rule_id.is_null() {
continue;
}
let rule_id = unsafe { cstr_to_string(f.rule_id) };
let message = unsafe { cstr_to_string(f.message) };
let location = if f.location.is_null() {
None
} else {
Some(unsafe { cstr_to_string(f.location) })
};
report.add(Finding {
file: None,
rule_id,
severity: match f.severity {
SEVERITY_CRITICAL => Severity::Critical,
SEVERITY_INFO => Severity::Info,
_ => Severity::Warning,
},
message,
location,
});
}
}
}
pub fn load_and_register(path: &Path, source: &str) -> Result<PathBuf, PluginLoadError> {
let rule = PluginRule::load(path, source)?;
crate::registry::register_rule(Box::new(rule));
Ok(path.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin_abi::{abi_major, abi_minor, abi_version_pack, SDKT_AUDIT_ABI_MINOR};
#[test]
fn abi_version_pack_roundtrip() {
assert_eq!(abi_major(abi_version_pack()), SDKT_AUDIT_ABI_MAJOR);
assert_eq!(abi_minor(abi_version_pack()), SDKT_AUDIT_ABI_MINOR);
}
#[test]
fn severity_enum_mapping() {
assert_eq!(sev_to_enum(SEVERITY_CRITICAL), Severity::Critical);
assert_eq!(sev_to_enum(SEVERITY_INFO), Severity::Info);
assert_eq!(sev_to_enum(99), Severity::Warning);
}
#[test]
fn finding_buffer_default_is_empty() {
let buf = SdktAuditReportC::default();
assert_eq!(buf.count, 0);
}
#[test]
fn cstr_to_string_handles_null() {
assert_eq!(unsafe { cstr_to_string(std::ptr::null()) }, "");
}
#[test]
fn plugin_bounds_clamping() {
let buf = SdktAuditReportC {
count: 999999,
..Default::default()
};
let safe_count = buf.count.min(crate::plugin_abi::MAX_FINDINGS);
assert_eq!(safe_count, crate::plugin_abi::MAX_FINDINGS);
}
#[test]
fn load_rejects_missing_file() {
let err = PluginRule::load(Path::new("/nonexistent/plugin.so"), "fn x() {}");
assert!(matches!(err, Err(PluginLoadError::DlOpen(_))));
}
}