aster_forge_cloud_files_macos_bridge/
ffi.rs1use std::{panic::AssertUnwindSafe, ptr};
4
5use aster_forge_cloud_files_core::{
6 CloudItemId, CloudItemKey, CloudNamespaceId, CloudRootId, CloudScope,
7};
8
9use crate::identifier::{MAX_FILE_PROVIDER_IDENTIFIER_BYTES, MAX_IDENTITY_FIELD_BYTES};
10use crate::{
11 MacosBridgeError, MacosErrorCode, MacosExtensionRequestLease, MacosExtensionSession,
12 MacosFileProviderIdentifier, Result,
13};
14
15#[derive(Debug)]
18#[repr(C)]
19pub struct MacosFfiBuffer {
20 pub ptr: *mut u8,
22 pub len: usize,
24 pub capacity: usize,
26}
27
28impl MacosFfiBuffer {
29 const fn empty() -> Self {
30 Self {
31 ptr: ptr::null_mut(),
32 len: 0,
33 capacity: 0,
34 }
35 }
36
37 fn from_vec(mut bytes: Vec<u8>) -> Self {
38 if bytes.is_empty() {
39 return Self::empty();
40 }
41 let buffer = Self {
42 ptr: bytes.as_mut_ptr(),
43 len: bytes.len(),
44 capacity: bytes.capacity(),
45 };
46 std::mem::forget(bytes);
47 buffer
48 }
49}
50
51#[derive(Debug)]
53#[repr(C)]
54pub struct MacosFfiResult {
55 pub code: MacosErrorCode,
57 pub buffer: MacosFfiBuffer,
59}
60
61#[derive(Debug, Clone, Copy)]
63#[repr(C)]
64pub struct MacosFfiSessionHandle {
65 pub raw: *const (),
67}
68
69#[derive(Debug, Clone, Copy)]
71#[repr(C)]
72pub struct MacosFfiRequestHandle {
73 pub raw: *mut (),
75}
76
77#[derive(Debug)]
79#[repr(C)]
80pub struct MacosFfiSessionResult {
81 pub code: MacosErrorCode,
83 pub handle: MacosFfiSessionHandle,
85}
86
87#[derive(Debug)]
89#[repr(C)]
90pub struct MacosFfiRequestResult {
91 pub code: MacosErrorCode,
93 pub handle: MacosFfiRequestHandle,
95}
96
97impl MacosFfiResult {
98 fn success(bytes: Vec<u8>) -> Self {
99 Self {
100 code: MacosErrorCode::Success,
101 buffer: MacosFfiBuffer::from_vec(bytes),
102 }
103 }
104
105 const fn failure(code: MacosErrorCode) -> Self {
106 Self {
107 code,
108 buffer: MacosFfiBuffer::empty(),
109 }
110 }
111}
112
113#[unsafe(no_mangle)]
115pub extern "C" fn aster_forge_cloud_files_macos_session_create(
116 generation: u64,
117) -> MacosFfiSessionResult {
118 let result =
119 std::panic::catch_unwind(AssertUnwindSafe(|| {
120 let generation = aster_forge_cloud_files_core::SessionGeneration::new(generation)
121 .map_err(|_| MacosBridgeError::InvalidFfiInput {
122 reason: "extension session generation must be non-zero",
123 })?;
124 Ok::<_, MacosBridgeError>(MacosExtensionSession::new(generation).into_ffi_handle())
125 }));
126 match result {
127 Ok(Ok(raw)) => MacosFfiSessionResult {
128 code: MacosErrorCode::Success,
129 handle: MacosFfiSessionHandle { raw },
130 },
131 Ok(Err(error)) => MacosFfiSessionResult {
132 code: error.error_code(),
133 handle: MacosFfiSessionHandle { raw: ptr::null() },
134 },
135 Err(_) => MacosFfiSessionResult {
136 code: MacosErrorCode::Internal,
137 handle: MacosFfiSessionHandle { raw: ptr::null() },
138 },
139 }
140}
141
142#[unsafe(no_mangle)]
148pub unsafe extern "C" fn aster_forge_cloud_files_macos_session_begin_request(
149 session: MacosFfiSessionHandle,
150 generation: u64,
151) -> MacosFfiRequestResult {
152 let result =
153 std::panic::catch_unwind(AssertUnwindSafe(|| {
154 let session = unsafe { MacosExtensionSession::clone_from_ffi_handle(session.raw) }?;
156 let generation = aster_forge_cloud_files_core::SessionGeneration::new(generation)
157 .map_err(|_| MacosBridgeError::InvalidFfiInput {
158 reason: "request generation must be non-zero",
159 })?;
160 Ok::<_, MacosBridgeError>(session.begin_request(generation)?.into_ffi_handle())
161 }));
162 match result {
163 Ok(Ok(raw)) => MacosFfiRequestResult {
164 code: MacosErrorCode::Success,
165 handle: MacosFfiRequestHandle { raw },
166 },
167 Ok(Err(error)) => MacosFfiRequestResult {
168 code: error.error_code(),
169 handle: MacosFfiRequestHandle {
170 raw: ptr::null_mut(),
171 },
172 },
173 Err(_) => MacosFfiRequestResult {
174 code: MacosErrorCode::Internal,
175 handle: MacosFfiRequestHandle {
176 raw: ptr::null_mut(),
177 },
178 },
179 }
180}
181
182#[unsafe(no_mangle)]
188pub unsafe extern "C" fn aster_forge_cloud_files_macos_session_begin_closing(
189 session: MacosFfiSessionHandle,
190) -> MacosErrorCode {
191 ffi_code(|| {
192 let session = unsafe { MacosExtensionSession::clone_from_ffi_handle(session.raw) }?;
194 let _transitioned = session.begin_closing();
195 Ok(())
196 })
197}
198
199#[unsafe(no_mangle)]
205pub unsafe extern "C" fn aster_forge_cloud_files_macos_session_mark_disconnected(
206 session: MacosFfiSessionHandle,
207) -> MacosErrorCode {
208 ffi_code(|| {
209 let session = unsafe { MacosExtensionSession::clone_from_ffi_handle(session.raw) }?;
211 session.mark_disconnected()
212 })
213}
214
215#[unsafe(no_mangle)]
221pub unsafe extern "C" fn aster_forge_cloud_files_macos_request_release(
222 request: MacosFfiRequestHandle,
223) {
224 unsafe { MacosExtensionRequestLease::release_ffi_handle(request.raw) };
226}
227
228#[unsafe(no_mangle)]
234pub unsafe extern "C" fn aster_forge_cloud_files_macos_session_release(
235 session: MacosFfiSessionHandle,
236) {
237 unsafe { MacosExtensionSession::release_ffi_handle(session.raw) };
239}
240
241#[unsafe(no_mangle)]
248pub unsafe extern "C" fn aster_forge_cloud_files_macos_identifier_encode(
249 namespace_ptr: *const u8,
250 namespace_len: usize,
251 root_ptr: *const u8,
252 root_len: usize,
253 item_ptr: *const u8,
254 item_len: usize,
255) -> MacosFfiResult {
256 ffi_result(|| {
257 let namespace =
259 unsafe { read_utf8(namespace_ptr, namespace_len, MAX_IDENTITY_FIELD_BYTES) }?;
260 let root = unsafe { read_utf8(root_ptr, root_len, MAX_IDENTITY_FIELD_BYTES) }?;
262 let item = unsafe { read_utf8(item_ptr, item_len, MAX_IDENTITY_FIELD_BYTES) }?;
264 if namespace.contains('\0') || root.contains('\0') || item.contains('\0') {
265 return Err(MacosBridgeError::InvalidFfiInput {
266 reason: "FFI identity fields must not contain NUL",
267 });
268 }
269 let namespace =
270 CloudNamespaceId::new(namespace).map_err(|_| MacosBridgeError::InvalidFfiInput {
271 reason: "FFI namespace must not be empty",
272 })?;
273 let root = CloudRootId::new(root).map_err(|_| MacosBridgeError::InvalidFfiInput {
274 reason: "FFI root must not be empty",
275 })?;
276 let item = CloudItemId::new(item).map_err(|_| MacosBridgeError::InvalidFfiInput {
277 reason: "FFI item must not be empty",
278 })?;
279 let key = CloudItemKey::new(CloudScope::new(namespace, root), item);
280 Ok(MacosFileProviderIdentifier::encode(&key)?
281 .into_string()
282 .into_bytes())
283 })
284}
285
286#[unsafe(no_mangle)]
293pub unsafe extern "C" fn aster_forge_cloud_files_macos_identifier_decode(
294 identifier_ptr: *const u8,
295 identifier_len: usize,
296) -> MacosFfiResult {
297 ffi_result(|| {
298 let identifier = unsafe {
300 read_utf8(
301 identifier_ptr,
302 identifier_len,
303 MAX_FILE_PROVIDER_IDENTIFIER_BYTES,
304 )
305 }?;
306 let parsed = MacosFileProviderIdentifier::parse(identifier)?;
307 let key = parsed.item_key()?;
308 let fields = [
309 key.scope().namespace_id().as_str(),
310 key.scope().root_id().as_str(),
311 key.item_id().as_str(),
312 ];
313 if fields.iter().any(|field| field.contains('\0')) {
314 return Err(MacosBridgeError::InvalidFfiInput {
315 reason: "decoded FFI identity fields must not contain NUL",
316 });
317 }
318 let total_len = fields.iter().try_fold(2usize, |total, field| {
319 total
320 .checked_add(field.len())
321 .ok_or(MacosBridgeError::InvalidFfiInput {
322 reason: "decoded identifier fields exceed addressable memory",
323 })
324 })?;
325 let mut output = Vec::with_capacity(total_len);
326 for (index, field) in fields.into_iter().enumerate() {
327 if index != 0 {
328 output.push(0);
329 }
330 output.extend_from_slice(field.as_bytes());
331 }
332 Ok(output)
333 })
334}
335
336#[unsafe(no_mangle)]
342pub unsafe extern "C" fn aster_forge_cloud_files_macos_buffer_release(buffer: MacosFfiBuffer) {
343 if buffer.ptr.is_null() {
344 return;
345 }
346 if buffer.len > buffer.capacity || buffer.capacity == 0 {
347 return;
348 }
349 let bytes = unsafe { Vec::from_raw_parts(buffer.ptr, buffer.len, buffer.capacity) };
352 drop(bytes);
353}
354
355fn ffi_result(operation: impl FnOnce() -> Result<Vec<u8>>) -> MacosFfiResult {
356 match std::panic::catch_unwind(AssertUnwindSafe(operation)) {
357 Ok(Ok(bytes)) => MacosFfiResult::success(bytes),
358 Ok(Err(error)) => MacosFfiResult::failure(error.error_code()),
359 Err(_) => MacosFfiResult::failure(MacosErrorCode::Internal),
360 }
361}
362
363fn ffi_code(operation: impl FnOnce() -> Result<()>) -> MacosErrorCode {
364 match std::panic::catch_unwind(AssertUnwindSafe(operation)) {
365 Ok(Ok(())) => MacosErrorCode::Success,
366 Ok(Err(error)) => error.error_code(),
367 Err(_) => MacosErrorCode::Internal,
368 }
369}
370
371unsafe fn read_utf8(pointer: *const u8, length: usize, maximum_length: usize) -> Result<String> {
372 if length == 0 {
373 return Err(MacosBridgeError::InvalidFfiInput {
374 reason: "FFI string must not be empty",
375 });
376 }
377 if pointer.is_null() {
378 return Err(MacosBridgeError::InvalidFfiInput {
379 reason: "non-empty FFI string used a null pointer",
380 });
381 }
382 if length > maximum_length {
383 return Err(MacosBridgeError::InvalidFfiInput {
384 reason: "FFI string exceeds the accepted byte length",
385 });
386 }
387 let bytes = unsafe { std::slice::from_raw_parts(pointer, length) };
390 let text = std::str::from_utf8(bytes).map_err(|_| MacosBridgeError::InvalidFfiInput {
391 reason: "FFI string is not valid UTF-8",
392 })?;
393 Ok(text.to_owned())
394}