Skip to main content

sdk_rust/
ffi.rs

1use std::{
2    cell::RefCell,
3    ffi::{CStr, CString, c_char},
4    ptr,
5};
6
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8
9use crate::{
10    builder::ClientBuilder,
11    client::Client,
12    error::SdkError,
13    models::{
14        SdkArtifactRegisterRequest, SdkEvidenceIngestRequest, SdkKeyAccessPlanRequest,
15        SdkPolicyResolveRequest, SdkProtectionPlanRequest,
16    },
17};
18
19thread_local! {
20    static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
21}
22
23pub struct ClientHandle {
24    pub client: Client,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28#[serde(rename_all = "snake_case")]
29struct FfiClientOptions {
30    base_url: String,
31    bearer_token: Option<String>,
32    client_id: Option<String>,
33    client_secret: Option<String>,
34    tenant_id: Option<String>,
35    user_id: Option<String>,
36    timeout_secs: Option<u64>,
37    token_exchange_path: Option<String>,
38    #[serde(default)]
39    requested_scopes: Vec<String>,
40    #[serde(default)]
41    headers: std::collections::BTreeMap<String, String>,
42}
43
44#[unsafe(no_mangle)]
45pub extern "C" fn lattix_sdk_version() -> *mut c_char {
46    into_c_string(env!("CARGO_PKG_VERSION"))
47}
48
49#[unsafe(no_mangle)]
50pub extern "C" fn lattix_sdk_last_error_message() -> *mut c_char {
51    LAST_ERROR.with(|slot| match slot.borrow().as_deref() {
52        Some(message) => into_c_string(message),
53        None => into_c_string(""),
54    })
55}
56
57/// Frees a string previously allocated by this library and returned over the FFI boundary.
58///
59/// # Safety
60///
61/// `value` must either be null or a pointer returned by one of this library's FFI functions
62/// that transfer ownership of a `CString` to the caller. Passing any other pointer, or freeing
63/// the same pointer more than once, is undefined behavior.
64#[unsafe(no_mangle)]
65pub unsafe extern "C" fn lattix_sdk_string_free(value: *mut c_char) {
66    if value.is_null() {
67        return;
68    }
69
70    drop(unsafe { CString::from_raw(value) });
71}
72
73#[unsafe(no_mangle)]
74pub extern "C" fn lattix_sdk_client_new(options_json: *const c_char) -> *mut ClientHandle {
75    match ffi_result(|| {
76        let options: FfiClientOptions = parse_json_arg(options_json)?;
77        let mut builder = ClientBuilder::new(options.base_url);
78
79        if let Some(bearer_token) = options.bearer_token {
80            builder = builder.with_bearer_token(bearer_token);
81        }
82        if let Some(client_id) = options.client_id {
83            builder = builder.with_client_id(client_id);
84        }
85        if let Some(client_secret) = options.client_secret {
86            builder = builder.with_client_secret(client_secret);
87        }
88        if let Some(tenant_id) = options.tenant_id {
89            builder = builder.with_tenant_id(tenant_id);
90        }
91        if let Some(user_id) = options.user_id {
92            builder = builder.with_user_id(user_id);
93        }
94        if let Some(timeout_secs) = options.timeout_secs {
95            builder = builder.with_timeout_secs(timeout_secs);
96        }
97        if let Some(token_exchange_path) = options.token_exchange_path {
98            builder = builder.with_token_exchange_path(token_exchange_path);
99        }
100        if !options.requested_scopes.is_empty() {
101            builder = builder.with_requested_scopes(options.requested_scopes);
102        }
103        for (name, value) in options.headers {
104            builder = builder.with_header(name, value);
105        }
106
107        Ok(Box::into_raw(Box::new(ClientHandle {
108            client: builder.build()?,
109        })))
110    }) {
111        Ok(handle) => handle,
112        Err(_) => ptr::null_mut(),
113    }
114}
115
116/// Frees a client handle previously allocated by `lattix_sdk_client_new`.
117///
118/// # Safety
119///
120/// `handle` must either be null or a pointer returned by `lattix_sdk_client_new` that has not
121/// already been freed. Passing any other pointer, or freeing the same pointer more than once,
122/// is undefined behavior.
123#[unsafe(no_mangle)]
124pub unsafe extern "C" fn lattix_sdk_client_free(handle: *mut ClientHandle) {
125    if handle.is_null() {
126        return;
127    }
128
129    drop(unsafe { Box::from_raw(handle) });
130}
131
132macro_rules! ffi_get_method {
133    ($name:ident, $method:ident) => {
134        #[unsafe(no_mangle)]
135        pub extern "C" fn $name(handle: *mut ClientHandle) -> *mut c_char {
136            match ffi_result(|| {
137                let client = client_from_handle(handle)?;
138                let response = client.$method()?;
139                serialize_json(&response)
140            }) {
141                Ok(value) => value,
142                Err(_) => ptr::null_mut(),
143            }
144        }
145    };
146}
147
148macro_rules! ffi_post_method {
149    ($name:ident, $method:ident, $request_ty:ty) => {
150        #[unsafe(no_mangle)]
151        pub extern "C" fn $name(
152            handle: *mut ClientHandle,
153            request_json: *const c_char,
154        ) -> *mut c_char {
155            match ffi_result(|| {
156                let client = client_from_handle(handle)?;
157                let request: $request_ty = parse_json_arg(request_json)?;
158                let response = client.$method(&request)?;
159                serialize_json(&response)
160            }) {
161                Ok(value) => value,
162                Err(_) => ptr::null_mut(),
163            }
164        }
165    };
166}
167
168ffi_get_method!(lattix_sdk_capabilities, capabilities);
169ffi_get_method!(lattix_sdk_whoami, whoami);
170ffi_get_method!(lattix_sdk_bootstrap, bootstrap);
171ffi_get_method!(lattix_sdk_exchange_session, exchange_session);
172ffi_post_method!(
173    lattix_sdk_protection_plan,
174    protection_plan,
175    SdkProtectionPlanRequest
176);
177ffi_post_method!(
178    lattix_sdk_policy_resolve,
179    policy_resolve,
180    SdkPolicyResolveRequest
181);
182ffi_post_method!(
183    lattix_sdk_key_access_plan,
184    key_access_plan,
185    SdkKeyAccessPlanRequest
186);
187ffi_post_method!(
188    lattix_sdk_artifact_register,
189    artifact_register,
190    SdkArtifactRegisterRequest
191);
192ffi_post_method!(lattix_sdk_evidence, evidence, SdkEvidenceIngestRequest);
193
194fn ffi_result<T>(action: impl FnOnce() -> Result<T, SdkError>) -> Result<T, ()> {
195    match action() {
196        Ok(value) => {
197            clear_last_error();
198            Ok(value)
199        }
200        Err(err) => {
201            set_last_error(err.to_string());
202            Err(())
203        }
204    }
205}
206
207fn client_from_handle(handle: *mut ClientHandle) -> Result<&'static Client, SdkError> {
208    if handle.is_null() {
209        return Err(SdkError::InvalidInput(
210            "sdk-rust client handle cannot be null".to_string(),
211        ));
212    }
213
214    let handle = unsafe { &*handle };
215    Ok(&handle.client)
216}
217
218fn parse_json_arg<T>(value: *const c_char) -> Result<T, SdkError>
219where
220    T: DeserializeOwned,
221{
222    if value.is_null() {
223        return Err(SdkError::InvalidInput(
224            "JSON argument cannot be null".to_string(),
225        ));
226    }
227
228    let raw = unsafe { CStr::from_ptr(value) };
229    let raw = raw
230        .to_str()
231        .map_err(|err| SdkError::InvalidInput(format!("invalid UTF-8 argument: {err}")))?;
232    serde_json::from_str(raw)
233        .map_err(|err| SdkError::Serialization(format!("invalid JSON argument: {err}")))
234}
235
236fn serialize_json<T>(value: &T) -> Result<*mut c_char, SdkError>
237where
238    T: Serialize,
239{
240    let payload = serde_json::to_string(value)
241        .map_err(|err| SdkError::Serialization(format!("failed to serialize response: {err}")))?;
242    Ok(into_c_string(payload))
243}
244
245fn set_last_error(message: String) {
246    LAST_ERROR.with(|slot| {
247        *slot.borrow_mut() = Some(message);
248    });
249}
250
251fn clear_last_error() {
252    LAST_ERROR.with(|slot| {
253        *slot.borrow_mut() = None;
254    });
255}
256
257fn into_c_string(value: impl AsRef<str>) -> *mut c_char {
258    let sanitized = value.as_ref().replace('\0', " ");
259    CString::new(sanitized)
260        .expect("CString::new should succeed after null sanitization")
261        .into_raw()
262}