Skip to main content

doublecrypt_core/
ffi.rs

1//! Minimal C ABI for Swift interop.
2//!
3//! # Handle-based API
4//!
5//! All functions operate on an opaque `FsHandle` obtained from `fs_create`.
6//! The caller must eventually call `fs_destroy` to free the handle.
7//!
8//! # Buffer ownership
9//!
10//! - Buffers passed *into* Rust (e.g. `data` in `fs_write_file`) are borrowed
11//!   for the duration of the call. The caller retains ownership.
12//! - Buffers returned *from* Rust (e.g. `fs_list_root` JSON string) are allocated
13//!   by Rust. The caller must free them with `fs_free_string`.
14//! - For `fs_read_file`, the caller provides the output buffer and its capacity.
15
16use std::ffi::{CStr, CString};
17use std::os::raw::c_char;
18use std::ptr;
19use std::sync::Arc;
20
21use crate::block_store::{DeviceBlockStore, DiskBlockStore, MemoryBlockStore};
22use crate::cached_store::CachedBlockStore;
23use crate::crypto::ChaChaEngine;
24use crate::error::FsErrorCode;
25use crate::fs::FilesystemCore;
26use crate::model::DEFAULT_BLOCK_SIZE;
27use crate::network_store::NetworkBlockStoreConfig;
28
29/// Opaque handle to a FilesystemCore instance.
30pub struct FsHandle {
31    core: FilesystemCore,
32}
33
34// ── Lifecycle ──
35
36/// Create a new in-memory filesystem handle.
37///
38/// `total_blocks`: number of blocks in the virtual block device.
39/// `master_key`: pointer to the master encryption key bytes.
40/// `master_key_len`: length of master_key in bytes (should be 32).
41///
42/// Returns a pointer to an opaque handle, or null on failure.
43///
44/// # Safety
45/// `master_key` must point to `master_key_len` valid bytes.
46#[no_mangle]
47pub unsafe extern "C" fn fs_create(
48    total_blocks: u64,
49    master_key: *const u8,
50    master_key_len: usize,
51) -> *mut FsHandle {
52    if master_key.is_null() || master_key_len == 0 {
53        return ptr::null_mut();
54    }
55    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
56
57    let store = Arc::new(MemoryBlockStore::new(DEFAULT_BLOCK_SIZE, total_blocks));
58    let crypto = match ChaChaEngine::new(key_slice) {
59        Ok(c) => Arc::new(c),
60        Err(_) => return ptr::null_mut(),
61    };
62
63    let core = FilesystemCore::new(store, crypto);
64    let handle = Box::new(FsHandle { core });
65    Box::into_raw(handle)
66}
67
68/// Create a filesystem handle backed by a file on disk.
69///
70/// `path`: null-terminated path to the image file.
71/// `total_blocks`: number of blocks. Pass 0 to infer from file size.
72/// `block_size`: block size in bytes. Pass 0 to use the default (65536).
73/// `create_new`: if nonzero, create a new file (fails if it already exists).
74///               if zero, open an existing file.
75/// `master_key`: pointer to the master encryption key bytes.
76/// `master_key_len`: length of master_key in bytes (should be 32).
77///
78/// Returns a pointer to an opaque handle, or null on failure.
79///
80/// # Safety
81/// - `path` must be a valid null-terminated C string.
82/// - `master_key` must point to `master_key_len` valid bytes.
83#[no_mangle]
84pub unsafe extern "C" fn fs_create_disk(
85    path: *const c_char,
86    total_blocks: u64,
87    block_size: u32,
88    create_new: i32,
89    master_key: *const u8,
90    master_key_len: usize,
91) -> *mut FsHandle {
92    if master_key.is_null() || master_key_len == 0 {
93        return ptr::null_mut();
94    }
95    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
96        Some(s) => s,
97        None => return ptr::null_mut(),
98    };
99    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
100
101    let bs = if block_size == 0 {
102        DEFAULT_BLOCK_SIZE
103    } else {
104        block_size as usize
105    };
106
107    let store = if create_new != 0 {
108        match DiskBlockStore::create(path_str, bs, total_blocks) {
109            Ok(s) => Arc::new(s),
110            Err(_) => return ptr::null_mut(),
111        }
112    } else {
113        match DiskBlockStore::open(path_str, bs, total_blocks) {
114            Ok(s) => Arc::new(s),
115            Err(_) => return ptr::null_mut(),
116        }
117    };
118
119    let crypto = match ChaChaEngine::new(key_slice) {
120        Ok(c) => Arc::new(c),
121        Err(_) => return ptr::null_mut(),
122    };
123
124    let core = FilesystemCore::new(store, crypto);
125    let handle = Box::new(FsHandle { core });
126    Box::into_raw(handle)
127}
128
129/// Create a filesystem handle backed by a raw block device (e.g. `/dev/xvdf`).
130///
131/// `path`: null-terminated path to the block device.
132/// `total_blocks`: number of blocks. Pass 0 to infer from the device size.
133/// `block_size`: block size in bytes. Pass 0 to use the default (65536).
134/// `initialize`: if nonzero, fill the device with random data first (slow on
135///               large devices). If zero, open the device as-is.
136/// `master_key`: pointer to the master encryption key bytes.
137/// `master_key_len`: length of master_key in bytes (should be 32).
138///
139/// Returns a pointer to an opaque handle, or null on failure.
140///
141/// # Safety
142/// - `path` must be a valid null-terminated C string.
143/// - `master_key` must point to `master_key_len` valid bytes.
144#[no_mangle]
145pub unsafe extern "C" fn fs_create_device(
146    path: *const c_char,
147    total_blocks: u64,
148    block_size: u32,
149    initialize: i32,
150    master_key: *const u8,
151    master_key_len: usize,
152) -> *mut FsHandle {
153    if master_key.is_null() || master_key_len == 0 {
154        return ptr::null_mut();
155    }
156    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
157        Some(s) => s,
158        None => return ptr::null_mut(),
159    };
160    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
161
162    let bs = if block_size == 0 {
163        DEFAULT_BLOCK_SIZE
164    } else {
165        block_size as usize
166    };
167
168    let store = if initialize != 0 {
169        match DeviceBlockStore::initialize(path_str, bs, total_blocks) {
170            Ok(s) => Arc::new(s),
171            Err(_) => return ptr::null_mut(),
172        }
173    } else {
174        match DeviceBlockStore::open(path_str, bs, total_blocks) {
175            Ok(s) => Arc::new(s),
176            Err(_) => return ptr::null_mut(),
177        }
178    };
179
180    let crypto = match ChaChaEngine::new(key_slice) {
181        Ok(c) => Arc::new(c),
182        Err(_) => return ptr::null_mut(),
183    };
184
185    let core = FilesystemCore::new(store, crypto);
186    let handle = Box::new(FsHandle { core });
187    Box::into_raw(handle)
188}
189
190/// Create a filesystem handle backed by a remote `doublecrypt-server` over TLS.
191///
192/// The connection uses key-derived authentication (HKDF from the master key)
193/// and wraps the network store in a write-back LRU cache.
194///
195/// `addr`: null-terminated server address, e.g. `"10.0.0.5:9100"`.
196/// `server_name`: null-terminated TLS SNI hostname, e.g. `"dc-server"`.
197/// `ca_cert_path`: null-terminated path to the CA certificate PEM file.
198/// `cache_blocks`: number of blocks to cache locally (0 = default 256).
199/// `master_key`: pointer to the master encryption key bytes.
200/// `master_key_len`: length of master_key in bytes (should be 32).
201///
202/// Returns a pointer to an opaque handle, or null on failure (connection
203/// refused, TLS error, authentication failure, etc.).
204///
205/// # Safety
206/// - `addr`, `server_name`, and `ca_cert_path` must be valid null-terminated C strings.
207/// - `master_key` must point to `master_key_len` valid bytes.
208#[no_mangle]
209pub unsafe extern "C" fn fs_create_network(
210    addr: *const c_char,
211    server_name: *const c_char,
212    ca_cert_path: *const c_char,
213    cache_blocks: u32,
214    master_key: *const u8,
215    master_key_len: usize,
216) -> *mut FsHandle {
217    if master_key.is_null() || master_key_len == 0 {
218        return ptr::null_mut();
219    }
220    let addr_str = match unsafe { unsafe_cstr_to_str(addr) } {
221        Some(s) => s,
222        None => return ptr::null_mut(),
223    };
224    let sni_str = match unsafe { unsafe_cstr_to_str(server_name) } {
225        Some(s) => s,
226        None => return ptr::null_mut(),
227    };
228    let ca_str = match unsafe { unsafe_cstr_to_str(ca_cert_path) } {
229        Some(s) => s,
230        None => return ptr::null_mut(),
231    };
232    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
233
234    let config = NetworkBlockStoreConfig::new(addr_str, sni_str)
235        .ca_cert(ca_str)
236        .auth_token(key_slice);
237
238    let net_store = match crate::network_store::NetworkBlockStore::from_config(config) {
239        Ok(s) => s,
240        Err(_) => return ptr::null_mut(),
241    };
242
243    let cap = if cache_blocks == 0 {
244        256
245    } else {
246        cache_blocks as usize
247    };
248    let store = Arc::new(CachedBlockStore::new(net_store, cap));
249
250    let crypto = match ChaChaEngine::new(key_slice) {
251        Ok(c) => Arc::new(c),
252        Err(_) => return ptr::null_mut(),
253    };
254
255    let core = FilesystemCore::new(store, crypto);
256    let handle = Box::new(FsHandle { core });
257    Box::into_raw(handle)
258}
259
260/// Destroy a filesystem handle and free all associated resources.
261///
262/// # Safety
263/// `handle` must be a valid pointer returned by `fs_create` or `fs_create_disk`,
264/// and must not be used after this call.
265#[no_mangle]
266pub unsafe extern "C" fn fs_destroy(handle: *mut FsHandle) {
267    if !handle.is_null() {
268        unsafe {
269            drop(Box::from_raw(handle));
270        }
271    }
272}
273
274// ── Filesystem operations ──
275
276/// Initialize a new filesystem on the block store.
277///
278/// # Safety
279/// `handle` must be a valid pointer returned by `fs_create`.
280#[no_mangle]
281pub unsafe extern "C" fn fs_init_filesystem(handle: *mut FsHandle) -> i32 {
282    let Some(h) = (unsafe { handle.as_mut() }) else {
283        return FsErrorCode::InvalidArgument as i32;
284    };
285    match h.core.init_filesystem() {
286        Ok(()) => FsErrorCode::Ok as i32,
287        Err(ref e) => FsErrorCode::from(e) as i32,
288    }
289}
290
291/// Open / mount an existing filesystem from the block store.
292///
293/// # Safety
294/// `handle` must be a valid pointer returned by `fs_create`.
295#[no_mangle]
296pub unsafe extern "C" fn fs_open(handle: *mut FsHandle) -> i32 {
297    let Some(h) = (unsafe { handle.as_mut() }) else {
298        return FsErrorCode::InvalidArgument as i32;
299    };
300    match h.core.open() {
301        Ok(()) => FsErrorCode::Ok as i32,
302        Err(ref e) => FsErrorCode::from(e) as i32,
303    }
304}
305
306/// Create a file at the given path.
307///
308/// Parent directories must already exist.  The `name` argument may be
309/// a `/`-separated path such as `"a/b/file.txt"`.
310///
311/// # Safety
312/// `name` must be a valid null-terminated C string.
313#[no_mangle]
314pub unsafe extern "C" fn fs_create_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
315    let (h, name_str) = match validate_handle_and_name(handle, name) {
316        Ok(v) => v,
317        Err(code) => return code,
318    };
319    match h.core.create_file(name_str) {
320        Ok(()) => FsErrorCode::Ok as i32,
321        Err(ref e) => FsErrorCode::from(e) as i32,
322    }
323}
324
325/// Write data to a file at the given path.
326///
327/// # Safety
328/// - `name` must be a valid null-terminated C string (may contain `/` separators).
329/// - `data` must point to `data_len` valid bytes.
330#[no_mangle]
331pub unsafe extern "C" fn fs_write_file(
332    handle: *mut FsHandle,
333    name: *const c_char,
334    offset: u64,
335    data: *const u8,
336    data_len: usize,
337) -> i32 {
338    let (h, name_str) = match validate_handle_and_name(handle, name) {
339        Ok(v) => v,
340        Err(code) => return code,
341    };
342    if data.is_null() && data_len > 0 {
343        return FsErrorCode::InvalidArgument as i32;
344    }
345    let slice = if data_len > 0 {
346        unsafe { std::slice::from_raw_parts(data, data_len) }
347    } else {
348        &[]
349    };
350    match h.core.write_file(name_str, offset, slice) {
351        Ok(()) => FsErrorCode::Ok as i32,
352        Err(ref e) => FsErrorCode::from(e) as i32,
353    }
354}
355
356/// Read file data into a caller-provided buffer.
357///
358/// On success, writes the actual number of bytes read to `*out_len` and returns 0.
359/// If the buffer is too small, returns `BufferTooSmall` and sets `*out_len` to the required size.
360///
361/// # Safety
362/// - `name` must be a valid null-terminated C string.
363/// - `out_buf` must point to at least `buf_capacity` writable bytes.
364/// - `out_len` must be a valid pointer.
365#[no_mangle]
366pub unsafe extern "C" fn fs_read_file(
367    handle: *mut FsHandle,
368    name: *const c_char,
369    offset: u64,
370    len: usize,
371    out_buf: *mut u8,
372    out_len: *mut usize,
373) -> i32 {
374    let (h, name_str) = match validate_handle_and_name(handle, name) {
375        Ok(v) => v,
376        Err(code) => return code,
377    };
378    if out_buf.is_null() || out_len.is_null() {
379        return FsErrorCode::InvalidArgument as i32;
380    }
381
382    match h.core.read_file(name_str, offset, len) {
383        Ok(data) => {
384            let buf_capacity = len;
385            if data.len() > buf_capacity {
386                unsafe { *out_len = data.len() };
387                return FsErrorCode::BufferTooSmall as i32;
388            }
389            unsafe {
390                ptr::copy_nonoverlapping(data.as_ptr(), out_buf, data.len());
391                *out_len = data.len();
392            }
393            FsErrorCode::Ok as i32
394        }
395        Err(ref e) => FsErrorCode::from(e) as i32,
396    }
397}
398
399/// List the root directory. Returns a JSON string.
400///
401/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
402/// On error, returns null and writes the error code to `*out_error`.
403///
404/// For listing a subdirectory, use `fs_list_dir` instead.
405///
406/// # Safety
407/// - `handle` must be a valid pointer.
408/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
409#[no_mangle]
410pub unsafe extern "C" fn fs_list_root(handle: *mut FsHandle, out_error: *mut i32) -> *mut c_char {
411    let Some(h) = (unsafe { handle.as_mut() }) else {
412        if !out_error.is_null() {
413            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
414        }
415        return ptr::null_mut();
416    };
417
418    match h.core.list_directory("") {
419        Ok(entries) => {
420            let json = match serde_json::to_string(&entries) {
421                Ok(j) => j,
422                Err(_) => {
423                    if !out_error.is_null() {
424                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
425                    }
426                    return ptr::null_mut();
427                }
428            };
429            if !out_error.is_null() {
430                unsafe { *out_error = FsErrorCode::Ok as i32 };
431            }
432            match CString::new(json) {
433                Ok(cs) => cs.into_raw(),
434                Err(_) => {
435                    if !out_error.is_null() {
436                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
437                    }
438                    ptr::null_mut()
439                }
440            }
441        }
442        Err(ref e) => {
443            if !out_error.is_null() {
444                unsafe { *out_error = FsErrorCode::from(e) as i32 };
445            }
446            ptr::null_mut()
447        }
448    }
449}
450
451/// List a directory at the given path. Returns a JSON string.
452///
453/// Pass an empty string or `"/"` to list the root directory.
454///
455/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
456/// On error, returns null and writes the error code to `*out_error`.
457///
458/// # Safety
459/// - `handle` must be a valid pointer.
460/// - `path` must be a valid null-terminated C string (may contain `/` separators).
461/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
462#[no_mangle]
463pub unsafe extern "C" fn fs_list_dir(
464    handle: *mut FsHandle,
465    path: *const c_char,
466    out_error: *mut i32,
467) -> *mut c_char {
468    let Some(h) = (unsafe { handle.as_mut() }) else {
469        if !out_error.is_null() {
470            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
471        }
472        return ptr::null_mut();
473    };
474    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
475        Some(s) => s,
476        None => {
477            if !out_error.is_null() {
478                unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
479            }
480            return ptr::null_mut();
481        }
482    };
483
484    match h.core.list_directory(path_str) {
485        Ok(entries) => {
486            let json = match serde_json::to_string(&entries) {
487                Ok(j) => j,
488                Err(_) => {
489                    if !out_error.is_null() {
490                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
491                    }
492                    return ptr::null_mut();
493                }
494            };
495            if !out_error.is_null() {
496                unsafe { *out_error = FsErrorCode::Ok as i32 };
497            }
498            match CString::new(json) {
499                Ok(cs) => cs.into_raw(),
500                Err(_) => {
501                    if !out_error.is_null() {
502                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
503                    }
504                    ptr::null_mut()
505                }
506            }
507        }
508        Err(ref e) => {
509            if !out_error.is_null() {
510                unsafe { *out_error = FsErrorCode::from(e) as i32 };
511            }
512            ptr::null_mut()
513        }
514    }
515}
516
517/// Create a directory at the given path.
518///
519/// Parent directories must already exist.  The `name` argument may be
520/// a `/`-separated path such as `"a/b/newdir"`.
521///
522/// # Safety
523/// `name` must be a valid null-terminated C string.
524#[no_mangle]
525pub unsafe extern "C" fn fs_create_dir(handle: *mut FsHandle, name: *const c_char) -> i32 {
526    let (h, name_str) = match validate_handle_and_name(handle, name) {
527        Ok(v) => v,
528        Err(code) => return code,
529    };
530    match h.core.create_directory(name_str) {
531        Ok(()) => FsErrorCode::Ok as i32,
532        Err(ref e) => FsErrorCode::from(e) as i32,
533    }
534}
535
536/// Remove a file (or empty directory) at the given path.
537///
538/// # Safety
539/// `name` must be a valid null-terminated C string (may contain `/` separators).
540#[no_mangle]
541pub unsafe extern "C" fn fs_remove_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
542    let (h, name_str) = match validate_handle_and_name(handle, name) {
543        Ok(v) => v,
544        Err(code) => return code,
545    };
546    match h.core.remove_file(name_str) {
547        Ok(()) => FsErrorCode::Ok as i32,
548        Err(ref e) => FsErrorCode::from(e) as i32,
549    }
550}
551
552/// Rename a file or directory.  Both paths must share the same parent directory.
553///
554/// # Safety
555/// `old_name` and `new_name` must be valid null-terminated C strings (may contain `/` separators).
556#[no_mangle]
557pub unsafe extern "C" fn fs_rename(
558    handle: *mut FsHandle,
559    old_name: *const c_char,
560    new_name: *const c_char,
561) -> i32 {
562    let Some(h) = (unsafe { handle.as_mut() }) else {
563        return FsErrorCode::InvalidArgument as i32;
564    };
565    let old_str = match unsafe_cstr_to_str(old_name) {
566        Some(s) => s,
567        None => return FsErrorCode::InvalidArgument as i32,
568    };
569    let new_str = match unsafe_cstr_to_str(new_name) {
570        Some(s) => s,
571        None => return FsErrorCode::InvalidArgument as i32,
572    };
573    match h.core.rename(old_str, new_str) {
574        Ok(()) => FsErrorCode::Ok as i32,
575        Err(ref e) => FsErrorCode::from(e) as i32,
576    }
577}
578
579/// Flush buffered writes to the block store **without** calling fsync.
580///
581/// Use this for FUSE `write`/`release` handlers.  Call [`fs_sync`] only
582/// for explicit fsync requests.
583///
584/// # Safety
585/// `handle` must be a valid pointer.
586#[no_mangle]
587pub unsafe extern "C" fn fs_flush(handle: *mut FsHandle) -> i32 {
588    let Some(h) = (unsafe { handle.as_mut() }) else {
589        return FsErrorCode::InvalidArgument as i32;
590    };
591    match h.core.flush() {
592        Ok(()) => FsErrorCode::Ok as i32,
593        Err(ref e) => FsErrorCode::from(e) as i32,
594    }
595}
596
597/// Sync / flush the filesystem (flush + fsync).
598///
599/// # Safety
600/// `handle` must be a valid pointer.
601#[no_mangle]
602pub unsafe extern "C" fn fs_sync(handle: *mut FsHandle) -> i32 {
603    let Some(h) = (unsafe { handle.as_mut() }) else {
604        return FsErrorCode::InvalidArgument as i32;
605    };
606    match h.core.sync() {
607        Ok(()) => FsErrorCode::Ok as i32,
608        Err(ref e) => FsErrorCode::from(e) as i32,
609    }
610}
611
612/// Stat metadata for a single file/directory.
613///
614/// Returns `0` on success and populates the out-parameters.  Much cheaper
615/// than `fs_list_dir` for FUSE `getattr` / `lookup`.
616///
617/// `out_size`:  file size in bytes (or 0 for directories).
618/// `out_kind`:  0 = file, 1 = directory.
619/// `out_inode_id`: the logical inode id.
620///
621/// # Safety
622/// `handle`, `name`, and all `out_*` pointers must be valid.
623#[no_mangle]
624pub unsafe extern "C" fn fs_stat(
625    handle: *mut FsHandle,
626    name: *const c_char,
627    out_size: *mut u64,
628    out_kind: *mut i32,
629    out_inode_id: *mut u64,
630) -> i32 {
631    let (h, name_str) = match validate_handle_and_name(handle, name) {
632        Ok(v) => v,
633        Err(code) => return code,
634    };
635    if out_size.is_null() || out_kind.is_null() || out_inode_id.is_null() {
636        return FsErrorCode::InvalidArgument as i32;
637    }
638    match h.core.stat(name_str) {
639        Ok(entry) => {
640            unsafe {
641                *out_size = entry.size;
642                *out_kind = match entry.kind {
643                    crate::model::InodeKind::File => 0,
644                    crate::model::InodeKind::Directory => 1,
645                };
646                *out_inode_id = entry.inode_id;
647            }
648            FsErrorCode::Ok as i32
649        }
650        Err(ref e) => FsErrorCode::from(e) as i32,
651    }
652}
653
654/// Fill all unused blocks with cryptographically random data.
655///
656/// # Safety
657/// `handle` must be a valid pointer.
658#[no_mangle]
659pub unsafe extern "C" fn fs_scrub_free_blocks(handle: *mut FsHandle) -> i32 {
660    let Some(h) = (unsafe { handle.as_mut() }) else {
661        return FsErrorCode::InvalidArgument as i32;
662    };
663    match h.core.scrub_free_blocks() {
664        Ok(()) => FsErrorCode::Ok as i32,
665        Err(ref e) => FsErrorCode::from(e) as i32,
666    }
667}
668
669/// Free a string previously returned by `fs_list_root`.
670///
671/// # Safety
672/// `s` must be a pointer previously returned by a `fs_*` function, or null.
673#[no_mangle]
674pub unsafe extern "C" fn fs_free_string(s: *mut c_char) {
675    if !s.is_null() {
676        unsafe {
677            drop(CString::from_raw(s));
678        }
679    }
680}
681
682// ── Internal helpers ──
683
684/// # Safety
685/// `ptr` must be a valid null-terminated C string or null.
686unsafe fn unsafe_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
687    if ptr.is_null() {
688        return None;
689    }
690    unsafe { CStr::from_ptr(ptr) }.to_str().ok()
691}
692
693unsafe fn validate_handle_and_name<'a>(
694    handle: *mut FsHandle,
695    name: *const c_char,
696) -> Result<(&'a mut FsHandle, &'a str), i32> {
697    let h = unsafe { handle.as_mut() }.ok_or(FsErrorCode::InvalidArgument as i32)?;
698    let name_str =
699        unsafe { unsafe_cstr_to_str(name) }.ok_or(FsErrorCode::InvalidArgument as i32)?;
700    Ok((h, name_str))
701}