cpeforge-ubl-validator 0.1.0

Validador XSD 1.0 de documentos UBL 2.0 y 2.1 con recursos locales verificados.
//! Único módulo con `unsafe`: puente C de libxml2, con buffers y callback acotados.
#![allow(unsafe_code)]

use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::Mutex;

use crate::{InfrastructureError, XmlError};

static XML_LOCK: Mutex<()> = Mutex::new(());

unsafe extern "C" {
    fn cpeforge_preflight_xml(
        xml: *const u8,
        length: c_int,
        max_nodes: c_int,
        max_depth: c_int,
    ) -> c_int;
    fn cpeforge_probe_xml(
        xml: *const u8,
        length: c_int,
        name: *mut c_char,
        name_cap: c_int,
        version: *mut c_char,
        version_cap: c_int,
    ) -> c_int;
    fn cpeforge_schema_validate(
        xml: *const u8,
        length: c_int,
        schema_path: *const c_char,
        callback: unsafe extern "C" fn(*mut c_void, c_int, c_int, c_int, c_int, *const c_char),
        user: *mut c_void,
    ) -> c_int;
}

pub(crate) fn preflight(
    xml: &[u8],
    max_nodes: usize,
    max_depth: usize,
) -> Result<(), InfrastructureError> {
    let _guard = XML_LOCK
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let length = c_int::try_from(xml.len()).map_err(|_| InfrastructureError::XmlLimit)?;
    let max_nodes = c_int::try_from(max_nodes).map_err(|_| InfrastructureError::XmlLimit)?;
    let max_depth = c_int::try_from(max_depth).map_err(|_| InfrastructureError::XmlLimit)?;
    // SAFETY: el slice permanece vivo durante la llamada y C libera el DOM.
    let result = unsafe { cpeforge_preflight_xml(xml.as_ptr(), length, max_nodes, max_depth) };
    match result {
        1 => Ok(()),
        0 => Err(InfrastructureError::XmlParse),
        -1 => Err(InfrastructureError::DtdForbidden),
        _ => Err(InfrastructureError::XmlLimit),
    }
}

pub(crate) fn probe(xml: &[u8]) -> Result<Option<(String, String)>, InfrastructureError> {
    let _guard = XML_LOCK
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let length = c_int::try_from(xml.len()).map_err(|_| InfrastructureError::XmlLimit)?;
    let mut name = [0 as c_char; 1024];
    let mut version = [0 as c_char; 1024];
    // SAFETY: los dos buffers son mutables, con capacidad declarada exacta;
    // C siempre termina con NUL antes de devolver 1 y libera el DOM.
    let result = unsafe {
        cpeforge_probe_xml(
            xml.as_ptr(),
            length,
            name.as_mut_ptr(),
            1024,
            version.as_mut_ptr(),
            1024,
        )
    };
    match result {
        0 => Ok(None),
        -2 => Err(InfrastructureError::DtdForbidden),
        1 => {
            // SAFETY: el puente C escribió terminadores NUL dentro de ambos buffers.
            let name = unsafe { CStr::from_ptr(name.as_ptr()) }
                .to_string_lossy()
                .into_owned();
            // SAFETY: mismo contrato del buffer `version`.
            let version = unsafe { CStr::from_ptr(version.as_ptr()) }
                .to_string_lossy()
                .into_owned();
            Ok(Some((name, version)))
        }
        _ => Err(InfrastructureError::XmlLimit),
    }
}

unsafe extern "C" fn collect(
    user: *mut c_void,
    level: c_int,
    code: c_int,
    column: c_int,
    line: c_int,
    message: *const c_char,
) {
    if user.is_null() || message.is_null() {
        return;
    }
    // SAFETY: `user` points to the live Vec passed synchronously below; C
    // never retains it and libxml invokes the callback before returning.
    let errors = unsafe { &mut *(user.cast::<Vec<XmlError>>()) };
    if errors.len() >= 256 {
        return;
    }
    // SAFETY: libxml owns a NUL-terminated message for this callback's lifetime.
    let message = unsafe { CStr::from_ptr(message) }
        .to_string_lossy()
        .into_owned();
    errors.push(XmlError {
        level,
        code,
        column,
        line,
        message,
        file: None,
    });
}

pub(crate) fn validate(
    xml: &[u8],
    path: &str,
) -> Result<(bool, Vec<XmlError>), InfrastructureError> {
    let _guard = XML_LOCK
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let path = CString::new(path).map_err(|_| InfrastructureError::UnsafePath)?;
    let length = c_int::try_from(xml.len()).map_err(|_| InfrastructureError::XmlLimit)?;
    let mut errors = Vec::<XmlError>::new();
    // SAFETY: xml is live for the entire call, path is NUL-terminated, the
    // callback only writes to `errors` synchronously, and C releases every
    // parser/schema/document context before returning.
    let result = unsafe {
        cpeforge_schema_validate(
            xml.as_ptr(),
            length,
            path.as_ptr(),
            collect,
            (&mut errors as *mut Vec<XmlError>).cast::<c_void>(),
        )
    };
    match result {
        1 => Ok((true, errors)),
        0 => Ok((false, errors)),
        -1 => Err(InfrastructureError::XmlParse),
        -2 => Err(InfrastructureError::SchemaCompile(errors)),
        -4 => Err(InfrastructureError::DtdForbidden),
        _ => Err(InfrastructureError::SchemaValidate(errors)),
    }
}