aam-rs 2.0.2

A Rust implementation of the Abstract Alias Mapping (AAM) framework for aliasing and maping aam files.
Documentation
//! C FFI bindings for aam-rs.
//!
//! Compile with `--features ffi` (implied when building as `cdylib`).

#![allow(clippy::missing_safety_doc)]

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

use crate::aam::AAM;
use crate::error::AamlError;
use crate::pipeline::formatter::FormattingOptions as FormatterRules;

fn first_error(errors: Vec<AamlError>) -> AamlError {
    errors.into_iter().next().unwrap_or(AamlError::ParseError {
        line: 1,
        content: String::new(),
        details: "unexpected empty parse error list".to_string(),
        diagnostics: None,
    })
}

// ── Opaque handle ────────────────────────────────────────────────────────────

pub struct AamHandle {
    inner: AAM,
    last_error: Option<CString>,
}

impl AamHandle {
    fn set_error(&mut self, err: impl ToString) {
        let msg = err.to_string().replace('\0', "<NUL>");
        self.last_error = CString::new(msg).ok();
    }

    fn clear_error(&mut self) {
        self.last_error = None;
    }
}

// ── Lifecycle ────────────────────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub extern "C" fn aam_new() -> *mut AamHandle {
    Box::into_raw(Box::new(AamHandle {
        inner: AAM::new(),
        last_error: None,
    }))
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_free(handle: *mut AamHandle) {
    if !handle.is_null() {
        unsafe { drop(Box::from_raw(handle)) };
    }
}

// ── Parsing & Formatting ─────────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_parse(handle: *mut AamHandle, content: *const c_char) -> i32 {
    if handle.is_null() || content.is_null() {
        return -1;
    }
    let handle = unsafe { &mut *handle };

    let content = match unsafe { CStr::from_ptr(content) }.to_str() {
        Ok(s) => s,
        Err(e) => {
            handle.set_error(e);
            return -1;
        }
    };

    match AAM::parse(content) {
        Ok(aam) => {
            handle.inner = aam;
            handle.clear_error();
            0
        }
        Err(e) => {
            handle.set_error(first_error(e));
            -1
        }
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_load(handle: *mut AamHandle, path: *const c_char) -> i32 {
    if handle.is_null() || path.is_null() {
        return -1;
    }
    let handle = unsafe { &mut *handle };

    let path = match unsafe { CStr::from_ptr(path) }.to_str() {
        Ok(s) => s,
        Err(e) => {
            handle.set_error(e);
            return -1;
        }
    };

    match AAM::load(path) {
        Ok(aam) => {
            handle.inner = aam;
            handle.clear_error();
            0
        }
        Err(e) => {
            handle.set_error(first_error(e));
            -1
        }
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_format(handle: *mut AamHandle, content: *const c_char) -> *mut c_char {
    if handle.is_null() || content.is_null() {
        return std::ptr::null_mut();
    }
    let handle_ref = unsafe { &mut *handle };

    let content_str = match unsafe { CStr::from_ptr(content) }.to_str() {
        Ok(s) => s,
        Err(e) => {
            handle_ref.set_error(e);
            return std::ptr::null_mut();
        }
    };

    let rules = FormatterRules::default();
    match handle_ref.inner.format(content_str, &rules) {
        Ok(formatted) => to_c_string(&formatted),
        Err(e) => {
            handle_ref.set_error(e);
            std::ptr::null_mut()
        }
    }
}

// ── Lookup ───────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_get(handle: *const AamHandle, key: *const c_char) -> *mut c_char {
    if handle.is_null() || key.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    let key = match unsafe { CStr::from_ptr(key) }.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };

    match handle.inner.get(key) {
        Some(v) => to_c_string(v),
        None => std::ptr::null_mut(),
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_find(handle: *const AamHandle, query: *const c_char) -> *mut c_char {
    if handle.is_null() || query.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    let query = match unsafe { CStr::from_ptr(query) }.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };

    to_c_string_map(handle.inner.find(query))
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_deep_search(
    handle: *const AamHandle,
    pattern: *const c_char,
) -> *mut c_char {
    if handle.is_null() || pattern.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    let pattern = match unsafe { CStr::from_ptr(pattern) }.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };

    to_c_string_map(handle.inner.deep_search(pattern))
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_reverse_search(
    handle: *const AamHandle,
    value: *const c_char,
) -> *mut c_char {
    if handle.is_null() || value.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    let value = match unsafe { CStr::from_ptr(value) }.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };

    to_c_string_list(handle.inner.reverse_search(value))
}

// ── Schemas & Types ──────────────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_schema_names(handle: *const AamHandle) -> *mut c_char {
    if handle.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    if let Some(schemas) = handle.inner.schemas() {
        let keys: Vec<&str> = schemas.keys().map(|k| k.as_str()).collect();
        to_c_string_list(keys)
    } else {
        std::ptr::null_mut()
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_type_names(handle: *const AamHandle) -> *mut c_char {
    if handle.is_null() {
        return std::ptr::null_mut();
    }
    let handle = unsafe { &*handle };

    if let Some(types) = handle.inner.types() {
        let keys: Vec<&str> = types.keys().map(|k| k.as_str()).collect();
        to_c_string_list(keys)
    } else {
        std::ptr::null_mut()
    }
}

// ── Memory management ────────────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_string_free(s: *mut c_char) {
    if !s.is_null() {
        unsafe { drop(CString::from_raw(s)) };
    }
}

// ── Error reporting ──────────────────────────────────────────────────────────

#[unsafe(no_mangle)]
pub unsafe extern "C" fn aam_last_error(handle: *const AamHandle) -> *const c_char {
    if handle.is_null() {
        return std::ptr::null();
    }
    let handle = unsafe { &*handle };
    match &handle.last_error {
        Some(cs) => cs.as_ptr(),
        None => std::ptr::null(),
    }
}

// ── Private helpers ──────────────────────────────────────────────────────────

fn to_c_string(s: &str) -> *mut c_char {
    let safe = s.replace('\0', "<NUL>");
    match CString::new(safe) {
        Ok(cs) => cs.into_raw(),
        Err(_) => std::ptr::null_mut(),
    }
}

fn to_c_string_list(list: Vec<&str>) -> *mut c_char {
    if list.is_empty() {
        return std::ptr::null_mut();
    }
    let joined = list.join(",");
    to_c_string(&joined)
}

fn to_c_string_map(map: Vec<(&str, &str)>) -> *mut c_char {
    if map.is_empty() {
        return std::ptr::null_mut();
    }
    let joined = map
        .into_iter()
        .map(|(k, v)| format!("{}={}", k, v))
        .collect::<Vec<_>>()
        .join("\n");
    to_c_string(&joined)
}