eloqstore-sys 1.1.0

Low-level Rust FFI bindings for EloqStore
//! Embedded library loader for `libeloqstore_combine.so`.
//!
//! When the crate is built with an embedded `.so` (e.g. no system install),
//! this module locates or extracts the shared library and makes it available
//! for the runtime linker. It first looks in standard paths; if not found,
//! it extracts the bytes embedded at compile time into a unique temp directory
//! and loads the library via `dlopen`.

use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, Once};
use tempfile::{Builder, TempDir};

#[cfg(unix)]
use libc::{c_char, c_int, c_void};

#[cfg(unix)]
unsafe extern "C" {
    fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
    fn dlerror() -> *const c_char;
}

/// One-time initialization: runs extraction at most once per process.
static INIT: Once = Once::new();

/// Path to the extracted or found `libeloqstore_combine.so`; set after first successful load.
static EXTRACTED_LIB_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);

/// Holds the temp directory where the `.so` was extracted; keeps it alive for the process lifetime.
static TEMP_DIR: Mutex<Option<TempDir>> = Mutex::new(None);

/// Ensures the C++ shared library is available and returns its path.
///
/// Runs extraction/lookup at most once (via `INIT`). If the library was already
/// found or extracted, returns the cached path. Used by the crate root before
/// resolving FFI symbols so that the dynamic linker can load the library.
pub(crate) fn ensure_library_available() -> Result<PathBuf, String> {
    INIT.call_once(|| {
        if let Ok(path) = extract_embedded_library() {
            if let Ok(mut guard) = EXTRACTED_LIB_PATH.lock() {
                *guard = path;
            }
        }
    });

    EXTRACTED_LIB_PATH
        .lock()
        .map_err(|e| format!("Mutex poison: {}", e))?
        .as_ref()
        .ok_or_else(|| "Library extraction failed".to_string())
        .map(|p| p.clone())
}

/// Locates or extracts the shared library and returns its path.
///
/// 1. Tries to find an existing `libeloqstore_combine.so` via `find_library_in_standard_paths()`.
/// 2. If not found, extracts the bytes embedded at compile time into a unique temp dir,
///    writes `libeloqstore_combine.so`, sets permissions, keeps the dir in `TEMP_DIR`,
///    and on Unix calls `dlopen` so the library is loaded before symbol resolution.
/// Returns `Ok(Some(path))` on success, `Ok(None)` only on internal failure (e.g. lock poison).
fn extract_embedded_library() -> Result<Option<PathBuf>, Box<dyn std::error::Error + Send + Sync>> {
    if let Some(path) = find_library_in_standard_paths() {
        return Ok(Some(path));
    }

    // Extract from embedded data (compile-time include_bytes! from OUT_DIR)
    // The embedded library will be generated by build.rs in OUT_DIR
    // We use include_bytes! to embed it at compile time
    // Note: include_bytes! reads the file at compile time, not runtime
    let embedded_data = include_bytes!(concat!(env!("OUT_DIR"), "/libeloqstore_combine.so"));

    // Create a unique temporary directory for the extracted library
    // Using tempfile::Builder ensures a unique, unpredictable path to prevent
    // symlink hijacking and TOCTOU race conditions
    let temp_dir = Builder::new()
        .prefix("eloqstore_libs_")
        .tempdir()
        .map_err(|e| format!("Failed to create temp directory: {}", e))?;

    let lib_path = temp_dir.path().join("libeloqstore_combine.so");

    // Atomically create the file using create_new(true) to prevent TOCTOU races
    // This ensures the file is created atomically and fails if it already exists
    let mut file = fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&lib_path)
        .map_err(|e| format!("Failed to create library file atomically: {}", e))?;

    file.write_all(embedded_data)
        .map_err(|e| format!("Failed to write library data: {}", e))?;
    file.sync_all()
        .map_err(|e| format!("Failed to sync library file: {}", e))?;
    drop(file);

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&lib_path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&lib_path, perms)?;
    }

    // Keep the temp directory alive for the lifetime of the process
    // This prevents the directory from being deleted while the library is in use
    if let Ok(mut guard) = TEMP_DIR.lock() {
        *guard = Some(temp_dir);
    }
    // If we can't store the temp dir, we'll leak it to ensure the library remains available
    // This is acceptable since the directory will be cleaned up when the process exits

    // On Unix: load the extracted .so with dlopen so symbols are available before FFI resolution.
    #[cfg(unix)]
    {
        use std::ffi::CString;
        let lib_path_cstr = CString::new(lib_path.to_string_lossy().as_ref())
            .map_err(|e| format!("Invalid library path: {}", e))?;

        unsafe {
            let handle = dlopen(lib_path_cstr.as_ptr(), libc::RTLD_LAZY | libc::RTLD_GLOBAL);
            if handle.is_null() {
                let err = dlerror();
                let error_msg = if err.is_null() {
                    "Unknown error".to_string()
                } else {
                    std::ffi::CStr::from_ptr(err).to_string_lossy().into_owned()
                };
                return Err(format!(
                    "Failed to dlopen library {}: {}",
                    lib_path.display(),
                    error_msg
                )
                .into());
            }
        }
    }

    Ok(Some(lib_path))
}

/// Looks for `libeloqstore_combine.so` in standard locations without extracting.
///
/// Checks, in order: same directory as the current executable, parent of that directory,
/// then `/usr/local/lib` and `/usr/lib`. Returns the first path where the file exists.
fn find_library_in_standard_paths() -> Option<PathBuf> {
    if let Ok(exe) = env::current_exe() {
        if let Some(exe_dir) = exe.parent() {
            // Check same directory
            let lib = exe_dir.join("libeloqstore_combine.so");
            if lib.exists() {
                return Some(lib);
            }
            // Check parent directory
            let lib = exe_dir.parent().map(|p| p.join("libeloqstore_combine.so"));
            if let Some(ref lib) = lib {
                if lib.exists() {
                    return Some(lib.clone());
                }
            }
        }
    }

    // Check system paths
    for path in ["/usr/local/lib", "/usr/lib"] {
        let lib = PathBuf::from(path).join("libeloqstore_combine.so");
        if lib.exists() {
            return Some(lib);
        }
    }

    None
}