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#[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#[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}