use super::uniffi_types::{ForeignBytes, RustBuffer, RustCallStatus, CALL_SUCCESS};
use crate::error::{Error, Result};
use libloading::{Library, Symbol};
use std::ffi::c_void;
use std::path::{Path, PathBuf};
use std::sync::Arc;
const SYMBOL_INIT_CLIENT: &[u8] = b"uniffi_op_uniffi_core_fn_func_init_client\0";
const SYMBOL_INVOKE_SYNC: &[u8] = b"uniffi_op_uniffi_core_fn_func_invoke_sync\0";
const SYMBOL_RELEASE_CLIENT: &[u8] = b"uniffi_op_uniffi_core_fn_func_release_client\0";
const SYMBOL_RUSTBUFFER_FREE: &[u8] = b"ffi_op_uniffi_core_rustbuffer_free\0";
const SYMBOL_RUSTBUFFER_FROM_BYTES: &[u8] = b"ffi_op_uniffi_core_rustbuffer_from_bytes\0";
const SYMBOL_FUTURE_POLL_RUST_BUFFER: &[u8] = b"ffi_op_uniffi_core_rust_future_poll_rust_buffer\0";
const SYMBOL_FUTURE_COMPLETE_RUST_BUFFER: &[u8] =
b"ffi_op_uniffi_core_rust_future_complete_rust_buffer\0";
const SYMBOL_FUTURE_FREE_RUST_BUFFER: &[u8] = b"ffi_op_uniffi_core_rust_future_free_rust_buffer\0";
type InitClientFn = unsafe extern "C" fn(config: RustBuffer) -> *mut c_void;
type InvokeSyncFn =
unsafe extern "C" fn(method: RustBuffer, call_status: *mut RustCallStatus) -> RustBuffer;
type ReleaseClientFn =
unsafe extern "C" fn(client_id: RustBuffer, call_status: *mut RustCallStatus);
type RustBufferFreeFn = unsafe extern "C" fn(buf: RustBuffer, call_status: *mut RustCallStatus);
type RustBufferFromBytesFn =
unsafe extern "C" fn(bytes: ForeignBytes, call_status: *mut RustCallStatus) -> RustBuffer;
type FutureContinuationCallback = extern "C" fn(callback_data: usize, poll_code: i8);
type FuturePollRustBufferFn = unsafe extern "C" fn(
handle: *mut c_void,
callback: FutureContinuationCallback,
callback_data: usize,
);
type FutureCompleteRustBufferFn =
unsafe extern "C" fn(handle: *mut c_void, call_status: *mut RustCallStatus) -> RustBuffer;
type FutureFreeRustBufferFn = unsafe extern "C" fn(handle: *mut c_void);
pub(crate) struct NativeLibrary {
#[allow(dead_code)]
library: Library,
init_client: InitClientFn,
invoke_sync: InvokeSyncFn,
release_client: ReleaseClientFn,
rustbuffer_free: RustBufferFreeFn,
rustbuffer_from_bytes: RustBufferFromBytesFn,
future_poll_rust_buffer: FuturePollRustBufferFn,
future_complete_rust_buffer: FutureCompleteRustBufferFn,
future_free_rust_buffer: FutureFreeRustBufferFn,
}
unsafe impl Send for NativeLibrary {}
unsafe impl Sync for NativeLibrary {}
impl NativeLibrary {
pub fn load(path: &Path) -> Result<Self> {
let library = unsafe { Library::new(path) }.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load library at {}: {}", path.display(), e),
})?;
let init_client: InitClientFn = unsafe {
let sym: Symbol<InitClientFn> =
library
.get(SYMBOL_INIT_CLIENT)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load init_client: {e}"),
})?;
*sym
};
let invoke_sync: InvokeSyncFn = unsafe {
let sym: Symbol<InvokeSyncFn> =
library
.get(SYMBOL_INVOKE_SYNC)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load invoke_sync: {e}"),
})?;
*sym
};
let release_client: ReleaseClientFn = unsafe {
let sym: Symbol<ReleaseClientFn> =
library
.get(SYMBOL_RELEASE_CLIENT)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load release_client: {e}"),
})?;
*sym
};
let rustbuffer_free: RustBufferFreeFn = unsafe {
let sym: Symbol<RustBufferFreeFn> =
library
.get(SYMBOL_RUSTBUFFER_FREE)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load rustbuffer_free: {e}"),
})?;
*sym
};
let rustbuffer_from_bytes: RustBufferFromBytesFn = unsafe {
let sym: Symbol<RustBufferFromBytesFn> = library
.get(SYMBOL_RUSTBUFFER_FROM_BYTES)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load rustbuffer_from_bytes: {e}"),
})?;
*sym
};
let future_poll_rust_buffer: FuturePollRustBufferFn = unsafe {
let sym: Symbol<FuturePollRustBufferFn> = library
.get(SYMBOL_FUTURE_POLL_RUST_BUFFER)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load rust_future_poll_rust_buffer: {e}"),
})?;
*sym
};
let future_complete_rust_buffer: FutureCompleteRustBufferFn = unsafe {
let sym: Symbol<FutureCompleteRustBufferFn> = library
.get(SYMBOL_FUTURE_COMPLETE_RUST_BUFFER)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load rust_future_complete_rust_buffer: {e}"),
})?;
*sym
};
let future_free_rust_buffer: FutureFreeRustBufferFn = unsafe {
let sym: Symbol<FutureFreeRustBufferFn> = library
.get(SYMBOL_FUTURE_FREE_RUST_BUFFER)
.map_err(|e| Error::LibraryLoadError {
message: format!("failed to load rust_future_free_rust_buffer: {e}"),
})?;
*sym
};
Ok(Self {
library,
init_client,
invoke_sync,
release_client,
rustbuffer_free,
rustbuffer_from_bytes,
future_poll_rust_buffer,
future_complete_rust_buffer,
future_free_rust_buffer,
})
}
fn string_to_rustbuffer(&self, s: &str) -> Result<RustBuffer> {
let bytes = s.as_bytes();
let foreign_bytes = ForeignBytes {
len: bytes.len() as i32,
data: bytes.as_ptr(),
};
let mut call_status = RustCallStatus::new();
let buffer = unsafe { (self.rustbuffer_from_bytes)(foreign_bytes, &mut call_status) };
if !call_status.is_success() {
let error_msg = self.extract_error_message(&call_status);
let error_buf = unsafe { call_status.take_error_buf() };
if !error_buf.is_empty() {
self.free_rustbuffer(error_buf);
}
return Err(Error::SdkError {
message: error_msg.unwrap_or_else(|| "failed to allocate buffer".to_string()),
});
}
Ok(buffer)
}
fn rustbuffer_to_string(&self, buffer: RustBuffer) -> Option<String> {
if buffer.is_empty() {
return None;
}
let bytes = unsafe { buffer.as_slice() };
let result = String::from_utf8_lossy(bytes).into_owned();
self.free_rustbuffer(buffer);
Some(result)
}
fn free_rustbuffer(&self, buffer: RustBuffer) {
if buffer.is_empty() {
return;
}
let mut call_status = RustCallStatus::new();
unsafe {
(self.rustbuffer_free)(buffer, &mut call_status);
}
}
fn extract_error_message(&self, status: &RustCallStatus) -> Option<String> {
if status.code == CALL_SUCCESS {
return None;
}
let error_buf = unsafe { status.error_buf() };
if error_buf.is_empty() {
return Some(match status.code {
1 => "SDK error (no details)".to_string(),
2 => "SDK panicked".to_string(),
_ => format!("Unknown error code: {}", status.code),
});
}
let bytes = unsafe { error_buf.as_slice() };
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub fn init_client(&self, config_json: &str) -> Result<String> {
let config_buffer = self.string_to_rustbuffer(config_json)?;
let future_handle = unsafe { (self.init_client)(config_buffer) };
if future_handle.is_null() {
return Err(Error::AuthenticationFailed {
message: "init_client returned null future handle".to_string(),
});
}
self.poll_future_blocking(future_handle);
let mut call_status = RustCallStatus::new();
let result_buffer =
unsafe { (self.future_complete_rust_buffer)(future_handle, &mut call_status) };
unsafe { (self.future_free_rust_buffer)(future_handle) };
if !call_status.is_success() {
let error_msg = self.extract_error_message(&call_status);
let error_buf = unsafe { call_status.take_error_buf() };
if !error_buf.is_empty() {
self.free_rustbuffer(error_buf);
}
return Err(Error::AuthenticationFailed {
message: error_msg.unwrap_or_else(|| "unknown error".to_string()),
});
}
self.rustbuffer_to_string(result_buffer)
.ok_or_else(|| Error::AuthenticationFailed {
message: "empty client_id from SDK".to_string(),
})
}
fn poll_future_blocking(&self, future_handle: *mut c_void) {
use std::sync::atomic::{AtomicI8, Ordering};
const POLL_READY: i8 = 0;
const POLL_MAYBE_READY: i8 = 1;
let poll_result = AtomicI8::new(-1);
extern "C" fn poll_callback(callback_data: usize, poll_code: i8) {
let result_ptr = callback_data as *const AtomicI8;
unsafe { (*result_ptr).store(poll_code, Ordering::SeqCst) };
}
loop {
poll_result.store(-1, Ordering::SeqCst);
let callback_data = &poll_result as *const AtomicI8 as usize;
unsafe {
(self.future_poll_rust_buffer)(future_handle, poll_callback, callback_data);
}
loop {
let result = poll_result.load(Ordering::SeqCst);
if result >= 0 {
if result == POLL_READY {
return; } else if result == POLL_MAYBE_READY {
break; }
}
std::thread::yield_now();
}
}
}
pub fn invoke_sync(&self, method_json: &str) -> Result<String> {
let method_buffer = self.string_to_rustbuffer(method_json)?;
let mut call_status = RustCallStatus::new();
let result_buffer = unsafe { (self.invoke_sync)(method_buffer, &mut call_status) };
if !call_status.is_success() {
let error_msg = self.extract_error_message(&call_status);
let error_buf = unsafe { call_status.take_error_buf() };
if !error_buf.is_empty() {
self.free_rustbuffer(error_buf);
}
return Err(Error::SdkError {
message: error_msg.unwrap_or_else(|| "invocation failed".to_string()),
});
}
let response = self.rustbuffer_to_string(result_buffer);
response.ok_or_else(|| Error::SdkError {
message: "empty response from SDK".to_string(),
})
}
pub fn release_client(&self, client_id: &str) {
let client_id_buffer = match self.string_to_rustbuffer(client_id) {
Ok(buf) => buf,
Err(_) => return, };
let mut call_status = RustCallStatus::new();
unsafe {
(self.release_client)(client_id_buffer, &mut call_status);
}
if !call_status.is_success() {
let error_buf = unsafe { call_status.take_error_buf() };
if !error_buf.is_empty() {
self.free_rustbuffer(error_buf);
}
}
}
}
pub(crate) fn find_library_path() -> Result<PathBuf> {
if let Ok(path_str) = std::env::var("ONEPASSWORD_LIB_PATH") {
let path = PathBuf::from(&path_str);
if !path.exists() {
return Err(Error::LibraryLoadError {
message: format!(
"ONEPASSWORD_LIB_PATH points to non-existent file: {}",
path.display()
),
});
}
let canonical = path.canonicalize().map_err(|e| Error::LibraryLoadError {
message: format!("failed to canonicalize ONEPASSWORD_LIB_PATH: {e}"),
})?;
let expected_name = library_filename();
let actual_name = canonical.file_name().and_then(|n| n.to_str()).unwrap_or("");
if actual_name != expected_name {
return Err(Error::LibraryLoadError {
message: format!(
"ONEPASSWORD_LIB_PATH must point to '{expected_name}', got '{actual_name}'"
),
});
}
return Ok(canonical);
}
let lib_name = library_filename();
let platform_dir = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
let bundled_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("libs")
.join(&platform_dir)
.join(&lib_name);
if bundled_path.exists() {
return Ok(bundled_path);
}
let out_dir_lib = PathBuf::from(env!("OUT_DIR")).join("lib").join(&lib_name);
if out_dir_lib.exists() {
return Ok(out_dir_lib);
}
if std::env::var("ONEPASSWORD_ALLOW_SYSTEM_LIB").is_ok() {
let system_paths = [PathBuf::from("/usr/local/lib"), PathBuf::from("/usr/lib")];
for dir in &system_paths {
let path = dir.join(&lib_name);
if path.exists() {
return Ok(path);
}
}
}
Err(Error::LibraryLoadError {
message: format!(
"could not find {lib_name}. Set ONEPASSWORD_LIB_PATH to a custom path, \
rebuild the crate with bundled libraries, or set ONEPASSWORD_ALLOW_SYSTEM_LIB=1 \
to search system paths (not recommended for security reasons)."
),
})
}
fn library_filename() -> String {
#[cfg(target_os = "linux")]
{
"libop_uniffi_core.so".to_string()
}
#[cfg(target_os = "macos")]
{
"libop_uniffi_core.dylib".to_string()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
compile_error!("Unsupported platform. Only Linux and macOS are supported.");
}
}
pub(crate) fn load_library() -> Result<Arc<NativeLibrary>> {
let path = find_library_path()?;
let library = NativeLibrary::load(&path)?;
Ok(Arc::new(library))
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn test_library_filename_format() {
let name = library_filename();
assert!(name.starts_with("libop_uniffi_core"));
assert!(name.ends_with(".so") || name.ends_with(".dylib"));
}
#[test]
fn test_find_library_path_finds_bundled() {
let path = find_library_path().expect("should find bundled library");
assert!(path.exists(), "library path should exist: {path:?}");
assert!(
path.to_string_lossy().contains("libop_uniffi_core"),
"path should contain library name"
);
}
#[test]
#[serial]
fn test_load_library_loads_all_symbols() {
let library = load_library().expect("should load library");
assert!(Arc::strong_count(&library) >= 1);
}
#[test]
fn test_native_library_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NativeLibrary>();
}
#[test]
fn test_native_library_load_from_bundled_path() {
let path = find_library_path().unwrap();
let library = NativeLibrary::load(&path).expect("should load from bundled path");
let _ = &library;
}
#[test]
#[serial]
fn test_find_library_path_with_invalid_env_var() {
let original = std::env::var("ONEPASSWORD_LIB_PATH").ok();
unsafe {
std::env::set_var(
"ONEPASSWORD_LIB_PATH",
"/nonexistent/path/libop_uniffi_core.so",
);
}
let result = find_library_path();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("non-existent"));
unsafe {
match original {
Some(val) => std::env::set_var("ONEPASSWORD_LIB_PATH", val),
None => std::env::remove_var("ONEPASSWORD_LIB_PATH"),
}
}
}
#[test]
#[serial]
fn test_find_library_path_with_wrong_filename() {
let original = std::env::var("ONEPASSWORD_LIB_PATH").ok();
unsafe {
std::env::set_var("ONEPASSWORD_LIB_PATH", "/etc/passwd");
}
let result = find_library_path();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("must point to"));
unsafe {
match original {
Some(val) => std::env::set_var("ONEPASSWORD_LIB_PATH", val),
None => std::env::remove_var("ONEPASSWORD_LIB_PATH"),
}
}
}
fn get_test_token() -> Option<String> {
std::env::var("OP_SERVICE_ACCOUNT_TOKEN").ok()
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_init_client_with_valid_token() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
let config = serde_json::json!({
"serviceAccountToken": token,
"programmingLanguage": "Rust",
"sdkVersion": "0030201",
"integrationName": "test-loader",
"integrationVersion": "0.1.0",
"requestLibraryName": "reqwest",
"requestLibraryVersion": "0.11",
"os": std::env::consts::OS,
"osVersion": "0.0.0",
"architecture": std::env::consts::ARCH
});
let config_json = serde_json::to_string(&config).unwrap();
let client_id = library
.init_client(&config_json)
.expect("should init client");
assert!(!client_id.is_empty(), "client_id should not be empty");
assert!(
client_id.parse::<u64>().is_ok(),
"client_id should be numeric: {client_id}"
);
library.release_client(&client_id);
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_init_client_with_invalid_token() {
let library = load_library().expect("should load library");
let config = serde_json::json!({
"serviceAccountToken": "invalid-token-12345",
"programmingLanguage": "Rust",
"sdkVersion": "0030201",
"integrationName": "test-loader",
"integrationVersion": "0.1.0",
"requestLibraryName": "reqwest",
"requestLibraryVersion": "0.11",
"os": std::env::consts::OS,
"osVersion": "0.0.0",
"architecture": std::env::consts::ARCH
});
let config_json = serde_json::to_string(&config).unwrap();
let result = library.init_client(&config_json);
assert!(result.is_err(), "should fail with invalid token");
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN and TEST_SECRET_REF"]
fn test_invoke_sync_resolves_secret() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let secret_ref = std::env::var("TEST_SECRET_REF").expect("TEST_SECRET_REF required");
let library = load_library().expect("should load library");
let config = serde_json::json!({
"serviceAccountToken": token,
"programmingLanguage": "Rust",
"sdkVersion": "0030201",
"integrationName": "test-loader",
"integrationVersion": "0.1.0",
"requestLibraryName": "reqwest",
"requestLibraryVersion": "0.11",
"os": std::env::consts::OS,
"osVersion": "0.0.0",
"architecture": std::env::consts::ARCH
});
let config_json = serde_json::to_string(&config).unwrap();
let client_id_str = library.init_client(&config_json).unwrap();
let client_id: u64 = client_id_str.parse().unwrap();
let invocation = serde_json::json!({
"invocation": {
"clientId": client_id,
"parameters": {
"name": "SecretsResolve",
"parameters": {
"secret_reference": secret_ref
}
}
}
});
let request_json = serde_json::to_string(&invocation).unwrap();
let response = library
.invoke_sync(&request_json)
.expect("should resolve secret");
assert!(!response.is_empty(), "response should not be empty");
library.release_client(&client_id_str);
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_release_client_succeeds() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
let config = serde_json::json!({
"serviceAccountToken": token,
"programmingLanguage": "Rust",
"sdkVersion": "0030201",
"integrationName": "test-loader",
"integrationVersion": "0.1.0",
"requestLibraryName": "reqwest",
"requestLibraryVersion": "0.11",
"os": std::env::consts::OS,
"osVersion": "0.0.0",
"architecture": std::env::consts::ARCH
});
let config_json = serde_json::to_string(&config).unwrap();
let client_id = library.init_client(&config_json).unwrap();
library.release_client(&client_id);
library.release_client(&client_id);
}
}