#![no_std]

//! This library uses lookup tables generated by `wasm-tools component link` to
//! provide runtime library and symbol resolution via `dlopen` and `dlsym`.  The
//! tables are provided via a call to `__wasm_set_libraries`, which `wasm-tools
//! component link` arranges as part of component instantiation.

use core::{
    ffi::{CStr, c_char, c_int, c_void},
    ptr, slice,
};

const RTLD_LAZY: c_int = 1;
const RTLD_NOW: c_int = 2;
const RTLD_GLOBAL: c_int = 256;

const RTLD_NEXT: isize = -1;
const RTLD_DEFAULT: isize = 0;

#[repr(C)]
pub struct Name {
    length: u32,
    data: *const u8,
}

#[repr(C)]
pub struct Symbol {
    name: Name,
    address: *const c_void,
}

#[repr(C)]
pub struct Symbols {
    count: u32,
    symbols: *const Symbol,
}

#[repr(C)]
pub struct Library {
    name: Name,
    symbols: Symbols,
}

#[repr(C)]
pub struct Libraries {
    count: u32,
    libraries: *const Library,
}

static mut ERROR: *const c_char = ptr::null();
static mut LIBRARIES: *const Libraries = ptr::null();

unsafe fn invalid_handle(library: *const c_void) -> bool {
    if LIBRARIES.is_null() {
        panic!(
            "`__wasm_set_libraries` should have been called during \
             instantiation with a non-NULL value"
        );
    }

    let library = library as *const Library;
    if (0..(*LIBRARIES).count)
        .any(|index| (*LIBRARIES).libraries.add(usize::try_from(index).unwrap()) == library)
    {
        false
    } else {
        ERROR = c"invalid library handle".as_ptr();
        true
    }
}

/// # Safety
///
/// `library` must be a valid, not-yet-closed library pointer returned by
/// `dlopen`.
#[no_mangle]
pub unsafe extern "C" fn dlclose(library: *mut c_void) -> c_int {
    if invalid_handle(library) { -1 } else { 0 }
}

#[no_mangle]
pub extern "C" fn dlerror() -> *const c_char {
    unsafe {
        let value = ERROR;
        ERROR = ptr::null();
        value
    }
}

/// # Safety
///
/// `name` must be a valid pointer to a null-terminated string.
#[no_mangle]
pub unsafe extern "C" fn dlopen(name: *const c_char, flags: c_int) -> *const c_void {
    if LIBRARIES.is_null() {
        panic!(
            "`__wasm_set_libraries` should have been called during \
             instantiation with a non-NULL value"
        );
    }

    if (flags & !(RTLD_LAZY | RTLD_NOW | RTLD_GLOBAL)) != 0 {
        // TODO
        ERROR = c"dlopen flags not yet supported".as_ptr();
        return ptr::null();
    }

    let name = CStr::from_ptr(name);
    let name = name.to_bytes();
    let libraries = slice::from_raw_parts(
        (*LIBRARIES).libraries,
        usize::try_from((*LIBRARIES).count).unwrap(),
    );
    if let Ok(index) = libraries.binary_search_by(|library| {
        slice::from_raw_parts(
            library.name.data,
            usize::try_from(library.name.length).unwrap(),
        )
        .cmp(name)
    }) {
        &libraries[index] as *const _ as _
    } else {
        ERROR = c"library not found".as_ptr();
        ptr::null()
    }
}

/// # Safety
///
/// `library` must be a valid, not-yet-closed library pointer returned by
/// `dlopen`, and `name` must be a valid pointer to a null-terminated string.
#[no_mangle]
pub unsafe extern "C" fn dlsym(library: *const c_void, name: *const c_char) -> *const c_void {
    if library as isize == RTLD_NEXT || library as isize == RTLD_DEFAULT {
        // TODO
        ERROR = c"dlsym RTLD_NEXT and RTLD_DEFAULT not yet supported".as_ptr();
        return ptr::null();
    }

    if invalid_handle(library) {
        return ptr::null();
    }

    let library = library as *const Library;
    let name = CStr::from_ptr(name);
    let name = name.to_bytes();
    let symbols = slice::from_raw_parts(
        (*library).symbols.symbols,
        usize::try_from((*library).symbols.count).unwrap(),
    );
    if let Ok(index) = symbols.binary_search_by(|symbol| {
        slice::from_raw_parts(
            symbol.name.data,
            usize::try_from(symbol.name.length).unwrap(),
        )
        .cmp(name)
    }) {
        symbols[index].address
    } else {
        ERROR = c"symbol not found".as_ptr();
        ptr::null()
    }
}

/// # Safety
///
/// `libraries` must be a valid pointer to a `Libraries` object, and this
/// pointer must remain valid for the lifetime of the process.
#[no_mangle]
pub unsafe extern "C" fn __wasm_set_libraries(libraries: *const Libraries) {
    LIBRARIES = libraries;
}

#[cfg(target_arch = "wasm32")]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    core::arch::wasm32::unreachable()
}
