native-ossl 0.1.1

Native Rust idiomatic bindings to OpenSSL
Documentation
//! OpenSSL error queue — `Error`, `ErrorStack`, `ErrState` (OpenSSL 3.2+).
//!
//! Every OpenSSL operation that can fail pushes one or more records onto a
//! thread-local error queue.  `ErrorStack::drain()` pops the entire queue and
//! returns it as a `Vec<Error>`.  Every public function in this crate that can
//! fail returns `Result<T, ErrorStack>`.

use native_ossl_sys as sys;
use std::ffi::CStr;
use std::fmt;

// ── Single error record ───────────────────────────────────────────────────────

/// A single record from the OpenSSL error queue.
#[derive(Debug, Clone)]
pub struct Error {
    /// Raw packed error code (use `lib()` / `reason()` to decompose).
    code: u64,
    /// Human-readable reason string, if OpenSSL knows one.
    reason: Option<String>,
    /// Library name string, if OpenSSL knows one.
    lib: Option<String>,
    /// Source file (from the error record, not Rust).
    file: Option<String>,
    /// Function name (from the error record).
    func: Option<String>,
    /// Caller-supplied data string (e.g. key file path).
    data: Option<String>,
}

impl Error {
    /// The packed error code.
    #[must_use]
    pub fn code(&self) -> u64 {
        self.code
    }

    /// Library component that generated this error, if known.
    #[must_use]
    pub fn lib(&self) -> Option<&str> {
        self.lib.as_deref()
    }

    /// Reason string, if known.
    #[must_use]
    pub fn reason(&self) -> Option<&str> {
        self.reason.as_deref()
    }

    /// C source file where the error was raised (may be absent in release builds).
    #[must_use]
    pub fn file(&self) -> Option<&str> {
        self.file.as_deref()
    }

    /// C function where the error was raised.
    #[must_use]
    pub fn func(&self) -> Option<&str> {
        self.func.as_deref()
    }

    /// Caller-supplied data string attached to the error record.
    #[must_use]
    pub fn data(&self) -> Option<&str> {
        self.data.as_deref()
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(r) = &self.reason {
            write!(f, "{r}")?;
        } else {
            write!(f, "error:{:#010x}", self.code)?;
        }
        if let Some(lib) = &self.lib {
            write!(f, " (lib:{lib})")?;
        }
        if let Some(func) = &self.func {
            write!(f, " in {func}")?;
        }
        if let Some(data) = &self.data {
            write!(f, ": {data}")?;
        }
        Ok(())
    }
}

// ── Error queue drain ─────────────────────────────────────────────────────────

/// A snapshot of all records that were on the thread-local OpenSSL error queue.
///
/// Returned as the `Err` variant of every `Result<T, ErrorStack>` in this crate.
/// The queue is cleared when this is constructed.
#[derive(Debug, Clone)]
pub struct ErrorStack(Vec<Error>);

impl ErrorStack {
    /// Drain the current thread's OpenSSL error queue into a new `ErrorStack`.
    ///
    /// After this call the queue is empty.  This is the canonical way to
    /// turn an OpenSSL failure into a Rust error value.
    #[must_use]
    pub fn drain() -> Self {
        let mut errors = Vec::new();

        loop {
            let mut file: *const std::os::raw::c_char = std::ptr::null();
            let mut func: *const std::os::raw::c_char = std::ptr::null();
            let mut data: *const std::os::raw::c_char = std::ptr::null();
            let mut line: std::os::raw::c_int = 0;
            let mut flags: std::os::raw::c_int = 0;

            let code = unsafe {
                sys::ERR_get_error_all(
                    std::ptr::addr_of_mut!(file),
                    std::ptr::addr_of_mut!(line),
                    std::ptr::addr_of_mut!(func),
                    std::ptr::addr_of_mut!(data),
                    std::ptr::addr_of_mut!(flags),
                )
            };

            if code == 0 {
                break;
            }

            // Reason and lib strings — static C strings, safe to borrow briefly.
            let reason = unsafe {
                let p = sys::ERR_reason_error_string(code);
                if p.is_null() {
                    None
                } else {
                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
                }
            };

            let lib_name = unsafe {
                let p = sys::ERR_lib_error_string(code);
                if p.is_null() {
                    None
                } else {
                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
                }
            };

            let file_str = unsafe {
                if file.is_null() {
                    None
                } else {
                    Some(CStr::from_ptr(file).to_string_lossy().into_owned())
                }
            };

            let func_str = unsafe {
                if func.is_null() {
                    None
                } else {
                    Some(CStr::from_ptr(func).to_string_lossy().into_owned())
                }
            };

            // `ERR_TXT_STRING` flag means data is a human-readable string.
            let data_str = unsafe {
                if data.is_null() || (flags & 0x02) == 0 {
                    None
                } else {
                    Some(CStr::from_ptr(data).to_string_lossy().into_owned())
                }
            };

            errors.push(Error {
                code,
                reason,
                lib: lib_name,
                file: file_str,
                func: func_str,
                data: data_str,
            });
        }

        ErrorStack(errors)
    }

    /// Returns `true` if the stack contains no errors.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Number of error records.
    #[must_use]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Iterate over the individual error records (oldest first).
    pub fn errors(&self) -> impl Iterator<Item = &Error> {
        self.0.iter()
    }
}

impl fmt::Display for ErrorStack {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (i, e) in self.0.iter().enumerate() {
            if i > 0 {
                f.write_str("; ")?;
            }
            fmt::Display::fmt(e, f)?;
        }
        Ok(())
    }
}

impl std::error::Error for ErrorStack {}

// ── Cross-thread error state ──────────────────────────────────────────────────

/// A snapshot of the thread-local error queue, suitable for moving across
/// thread boundaries.
///
/// Use this when an OpenSSL error occurs on a worker thread and needs to be
/// reported to the caller thread.
///
/// Requires OpenSSL 3.2+ (`OSSL_ERR_STATE_new/save/restore/free`).
#[cfg(ossl320)]
pub struct ErrState {
    ptr: *mut sys::ERR_STATE,
}

#[cfg(ossl320)]
impl ErrState {
    /// Capture the current thread's error queue into a new `ErrState`.
    ///
    /// Returns `None` if OpenSSL cannot allocate the state object.
    #[must_use]
    pub fn capture() -> Option<Self> {
        let ptr = unsafe { sys::OSSL_ERR_STATE_new() };
        if ptr.is_null() {
            return None;
        }
        unsafe { sys::OSSL_ERR_STATE_save(ptr) };
        Some(ErrState { ptr })
    }

    /// Restore this state onto the current thread's error queue, then drain it.
    ///
    /// Consumes `self`.
    #[must_use]
    pub fn restore_and_drain(self) -> ErrorStack {
        unsafe { sys::OSSL_ERR_STATE_restore(self.ptr) };
        // Prevent Drop from double-freeing — we handle free here.
        let ptr = self.ptr;
        std::mem::forget(self);
        unsafe { sys::OSSL_ERR_STATE_free(ptr) };
        ErrorStack::drain()
    }
}

#[cfg(ossl320)]
impl Drop for ErrState {
    fn drop(&mut self) {
        unsafe { sys::OSSL_ERR_STATE_free(self.ptr) };
    }
}

// SAFETY: `OSSL_ERR_STATE` is designed for cross-thread transfer.
#[cfg(ossl320)]
unsafe impl Send for ErrState {}

// ── Convenience macros ────────────────────────────────────────────────────────

/// Call an OpenSSL function that returns 1 on success.
///
/// On failure drains the error queue and returns `Err(ErrorStack)`.
///
/// ```ignore
/// ossl_call!(EVP_DigestInit_ex2(ctx.ptr, alg.as_ptr(), params_ptr))?;
/// ```
// SAFETY rationale for `macro_metavars_in_unsafe`: both `ossl_call!` and
// `ossl_ptr!` are crate-internal wrappers for OpenSSL FFI functions.  Every
// macro invocation in this crate passes a literal `sys::*` FFI call; no
// caller-controlled safe expression is ever expanded inside `unsafe {}`.
#[macro_export]
macro_rules! ossl_call {
    ($expr:expr) => {{
        #[allow(clippy::macro_metavars_in_unsafe)]
        let rc = unsafe { $expr };
        if rc == 1 {
            Ok(())
        } else {
            Err($crate::error::ErrorStack::drain())
        }
    }};
}

/// Call an OpenSSL function that returns a non-null pointer on success.
///
/// On null returns `Err(ErrorStack)`.
///
/// ```ignore
/// let ptr = ossl_ptr!(EVP_MD_CTX_new())?;
/// ```
#[macro_export]
macro_rules! ossl_ptr {
    ($expr:expr) => {{
        #[allow(clippy::macro_metavars_in_unsafe)]
        let ptr = unsafe { $expr };
        if ptr.is_null() {
            Err($crate::error::ErrorStack::drain())
        } else {
            Ok(ptr)
        }
    }};
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn drain_empty_queue_gives_empty_stack() {
        // Clear any residual errors from previous tests.
        unsafe { sys::ERR_clear_error() };
        let stack = ErrorStack::drain();
        assert!(stack.is_empty());
    }

    #[test]
    fn failed_fetch_populates_error_stack() {
        unsafe { sys::ERR_clear_error() };

        // EVP_MD_fetch with a nonexistent algorithm always fails and pushes errors.
        let ptr = unsafe {
            sys::EVP_MD_fetch(
                std::ptr::null_mut(),
                c"NONEXISTENT_ALGO_XYZ".as_ptr(),
                std::ptr::null(),
            )
        };
        assert!(ptr.is_null());

        let stack = ErrorStack::drain();
        assert!(!stack.is_empty(), "expected at least one error record");
        // After drain the queue must be empty again.
        let second = ErrorStack::drain();
        assert!(second.is_empty());
    }

    #[cfg(ossl320)]
    #[test]
    fn err_state_round_trip() {
        unsafe { sys::ERR_clear_error() };

        // Generate an error on this thread.
        unsafe {
            sys::EVP_MD_fetch(
                std::ptr::null_mut(),
                c"NONEXISTENT_ALGO_XYZ".as_ptr(),
                std::ptr::null(),
            );
        }

        let state = ErrState::capture().expect("OSSL_ERR_STATE_new failed");
        // After capture, this thread's queue may or may not be cleared depending
        // on OpenSSL version.  At minimum the restore must succeed.
        let stack = state.restore_and_drain();
        assert!(!stack.is_empty());
    }
}