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/// Create a filesystem handle backed by a remote `doublecrypt-server` over TLS.
196///
197/// The connection uses key-derived authentication (HKDF from the master key)
198/// and wraps the network store in a write-back LRU cache.
199///
200/// `addr`: null-terminated server address, e.g. `"10.0.0.5:9100"`.
201/// `server_name`: null-terminated TLS SNI hostname, e.g. `"dc-server"`.
202/// `ca_cert_path`: null-terminated path to the CA certificate PEM file.
203/// `cache_blocks`: number of blocks to cache locally (0 = default 256).
204/// `auth_token`: pointer to 32 bytes of auth token, or null to derive from `master_key`.
205/// `auth_token_len`: length of auth_token in bytes (must be 32 if non-null, ignored if null).
206/// `master_key`: pointer to the master encryption key bytes.
207/// `master_key_len`: length of master_key in bytes (should be 32).
208///
209/// When `auth_token` is null, the auth token is derived from `master_key` via
210/// HKDF (the original behaviour).  When `auth_token` is provided, it is used
211/// directly and `master_key` is used only for encryption.
212///
213/// Returns a pointer to an opaque handle, or null on failure (connection
214/// refused, TLS error, authentication failure, etc.).
215///
216/// # Safety
217/// - `addr`, `server_name`, and `ca_cert_path` must be valid null-terminated C strings.
218/// - `auth_token`, if non-null, must point to `auth_token_len` valid bytes.
219/// - `master_key` must point to `master_key_len` valid bytes.
220#[no_mangle]
221pub unsafe extern "C" fn fs_create_network(
222    addr: *const c_char,
223    server_name: *const c_char,
224    ca_cert_path: *const c_char,
225    cache_blocks: u32,
226    auth_token: *const u8,
227    auth_token_len: usize,
228    master_key: *const u8,
229    master_key_len: usize,
230) -> *mut FsHandle {
231    if master_key.is_null() || master_key_len == 0 {
232        return ptr::null_mut();
233    }
234    let addr_str = match unsafe { unsafe_cstr_to_str(addr) } {
235        Some(s) => s,
236        None => return ptr::null_mut(),
237    };
238    let sni_str = match unsafe { unsafe_cstr_to_str(server_name) } {
239        Some(s) => s,
240        None => return ptr::null_mut(),
241    };
242    let ca_str = match unsafe { unsafe_cstr_to_str(ca_cert_path) } {
243        Some(s) => s,
244        None => return ptr::null_mut(),
245    };
246    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
247
248    let config = if !auth_token.is_null() && auth_token_len == 32 {
249        let token_slice = unsafe { std::slice::from_raw_parts(auth_token, 32) };
250        let mut token = [0u8; 32];
251        token.copy_from_slice(token_slice);
252        NetworkBlockStoreConfig::new(addr_str, sni_str)
253            .ca_cert(ca_str)
254            .auth_token_raw(token)
255    } else {
256        NetworkBlockStoreConfig::new(addr_str, sni_str)
257            .ca_cert(ca_str)
258            .auth_token(key_slice)
259    };
260
261    let net_store = match crate::network_store::NetworkBlockStore::from_config(config) {
262        Ok(s) => s,
263        Err(_) => return ptr::null_mut(),
264    };
265
266    let cap = if cache_blocks == 0 {
267        256
268    } else {
269        cache_blocks as usize
270    };
271    let store = Arc::new(CachedBlockStore::new(net_store, cap));
272
273    let crypto = match ChaChaEngine::new(key_slice) {
274        Ok(c) => Arc::new(c),
275        Err(_) => return ptr::null_mut(),
276    };
277
278    let core = FilesystemCore::new(store, crypto);
279    let handle = Box::new(FsHandle { core });
280    Box::into_raw(handle)
281}
282
283/// Destroy a filesystem handle and free all associated resources.
284///
285/// # Safety
286/// `handle` must be a valid pointer returned by `fs_create` or `fs_create_disk`,
287/// and must not be used after this call.
288#[no_mangle]
289pub unsafe extern "C" fn fs_destroy(handle: *mut FsHandle) {
290    if !handle.is_null() {
291        unsafe {
292            drop(Box::from_raw(handle));
293        }
294    }
295}
296
297// ── Filesystem operations ──
298
299/// Initialize a new filesystem on the block store.
300///
301/// # Safety
302/// `handle` must be a valid pointer returned by `fs_create`.
303#[no_mangle]
304pub unsafe extern "C" fn fs_init_filesystem(handle: *mut FsHandle) -> i32 {
305    let Some(h) = (unsafe { handle.as_mut() }) else {
306        return FsErrorCode::InvalidArgument as i32;
307    };
308    match h.core.init_filesystem() {
309        Ok(()) => FsErrorCode::Ok as i32,
310        Err(ref e) => FsErrorCode::from(e) as i32,
311    }
312}
313
314/// Open / mount an existing filesystem from the block store.
315///
316/// # Safety
317/// `handle` must be a valid pointer returned by `fs_create`.
318#[no_mangle]
319pub unsafe extern "C" fn fs_open(handle: *mut FsHandle) -> i32 {
320    let Some(h) = (unsafe { handle.as_mut() }) else {
321        return FsErrorCode::InvalidArgument as i32;
322    };
323    match h.core.open() {
324        Ok(()) => FsErrorCode::Ok as i32,
325        Err(ref e) => FsErrorCode::from(e) as i32,
326    }
327}
328
329/// Create a file at the given path.
330///
331/// Parent directories must already exist.  The `name` argument may be
332/// a `/`-separated path such as `"a/b/file.txt"`.
333///
334/// # Safety
335/// `name` must be a valid null-terminated C string.
336#[no_mangle]
337pub unsafe extern "C" fn fs_create_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
338    let (h, name_str) = match validate_handle_and_name(handle, name) {
339        Ok(v) => v,
340        Err(code) => return code,
341    };
342    match h.core.create_file(name_str) {
343        Ok(()) => FsErrorCode::Ok as i32,
344        Err(ref e) => FsErrorCode::from(e) as i32,
345    }
346}
347
348/// Write data to a file at the given path.
349///
350/// # Safety
351/// - `name` must be a valid null-terminated C string (may contain `/` separators).
352/// - `data` must point to `data_len` valid bytes.
353#[no_mangle]
354pub unsafe extern "C" fn fs_write_file(
355    handle: *mut FsHandle,
356    name: *const c_char,
357    offset: u64,
358    data: *const u8,
359    data_len: usize,
360) -> i32 {
361    let (h, name_str) = match validate_handle_and_name(handle, name) {
362        Ok(v) => v,
363        Err(code) => return code,
364    };
365    if data.is_null() && data_len > 0 {
366        return FsErrorCode::InvalidArgument as i32;
367    }
368    let slice = if data_len > 0 {
369        unsafe { std::slice::from_raw_parts(data, data_len) }
370    } else {
371        &[]
372    };
373    match h.core.write_file(name_str, offset, slice) {
374        Ok(()) => FsErrorCode::Ok as i32,
375        Err(ref e) => FsErrorCode::from(e) as i32,
376    }
377}
378
379/// Read file data into a caller-provided buffer.
380///
381/// On success, writes the actual number of bytes read to `*out_len` and returns 0.
382/// If the buffer is too small, returns `BufferTooSmall` and sets `*out_len` to the required size.
383///
384/// # Safety
385/// - `name` must be a valid null-terminated C string.
386/// - `out_buf` must point to at least `buf_capacity` writable bytes.
387/// - `out_len` must be a valid pointer.
388#[no_mangle]
389pub unsafe extern "C" fn fs_read_file(
390    handle: *mut FsHandle,
391    name: *const c_char,
392    offset: u64,
393    len: usize,
394    out_buf: *mut u8,
395    out_len: *mut usize,
396) -> i32 {
397    let (h, name_str) = match validate_handle_and_name(handle, name) {
398        Ok(v) => v,
399        Err(code) => return code,
400    };
401    if out_buf.is_null() || out_len.is_null() {
402        return FsErrorCode::InvalidArgument as i32;
403    }
404
405    match h.core.read_file(name_str, offset, len) {
406        Ok(data) => {
407            let buf_capacity = len;
408            if data.len() > buf_capacity {
409                unsafe { *out_len = data.len() };
410                return FsErrorCode::BufferTooSmall as i32;
411            }
412            unsafe {
413                ptr::copy_nonoverlapping(data.as_ptr(), out_buf, data.len());
414                *out_len = data.len();
415            }
416            FsErrorCode::Ok as i32
417        }
418        Err(ref e) => FsErrorCode::from(e) as i32,
419    }
420}
421
422/// List the root directory. Returns a JSON string.
423///
424/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
425/// On error, returns null and writes the error code to `*out_error`.
426///
427/// For listing a subdirectory, use `fs_list_dir` instead.
428///
429/// # Safety
430/// - `handle` must be a valid pointer.
431/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
432#[no_mangle]
433pub unsafe extern "C" fn fs_list_root(handle: *mut FsHandle, out_error: *mut i32) -> *mut c_char {
434    let Some(h) = (unsafe { handle.as_mut() }) else {
435        if !out_error.is_null() {
436            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
437        }
438        return ptr::null_mut();
439    };
440
441    match h.core.list_directory("") {
442        Ok(entries) => {
443            let json = match serde_json::to_string(&entries) {
444                Ok(j) => j,
445                Err(_) => {
446                    if !out_error.is_null() {
447                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
448                    }
449                    return ptr::null_mut();
450                }
451            };
452            if !out_error.is_null() {
453                unsafe { *out_error = FsErrorCode::Ok as i32 };
454            }
455            match CString::new(json) {
456                Ok(cs) => cs.into_raw(),
457                Err(_) => {
458                    if !out_error.is_null() {
459                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
460                    }
461                    ptr::null_mut()
462                }
463            }
464        }
465        Err(ref e) => {
466            if !out_error.is_null() {
467                unsafe { *out_error = FsErrorCode::from(e) as i32 };
468            }
469            ptr::null_mut()
470        }
471    }
472}
473
474/// List a directory at the given path. Returns a JSON string.
475///
476/// Pass an empty string or `"/"` to list the root directory.
477///
478/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
479/// On error, returns null and writes the error code to `*out_error`.
480///
481/// # Safety
482/// - `handle` must be a valid pointer.
483/// - `path` must be a valid null-terminated C string (may contain `/` separators).
484/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
485#[no_mangle]
486pub unsafe extern "C" fn fs_list_dir(
487    handle: *mut FsHandle,
488    path: *const c_char,
489    out_error: *mut i32,
490) -> *mut c_char {
491    let Some(h) = (unsafe { handle.as_mut() }) else {
492        if !out_error.is_null() {
493            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
494        }
495        return ptr::null_mut();
496    };
497    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
498        Some(s) => s,
499        None => {
500            if !out_error.is_null() {
501                unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
502            }
503            return ptr::null_mut();
504        }
505    };
506
507    match h.core.list_directory(path_str) {
508        Ok(entries) => {
509            let json = match serde_json::to_string(&entries) {
510                Ok(j) => j,
511                Err(_) => {
512                    if !out_error.is_null() {
513                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
514                    }
515                    return ptr::null_mut();
516                }
517            };
518            if !out_error.is_null() {
519                unsafe { *out_error = FsErrorCode::Ok as i32 };
520            }
521            match CString::new(json) {
522                Ok(cs) => cs.into_raw(),
523                Err(_) => {
524                    if !out_error.is_null() {
525                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
526                    }
527                    ptr::null_mut()
528                }
529            }
530        }
531        Err(ref e) => {
532            if !out_error.is_null() {
533                unsafe { *out_error = FsErrorCode::from(e) as i32 };
534            }
535            ptr::null_mut()
536        }
537    }
538}
539
540/// Create a directory at the given path.
541///
542/// Parent directories must already exist.  The `name` argument may be
543/// a `/`-separated path such as `"a/b/newdir"`.
544///
545/// # Safety
546/// `name` must be a valid null-terminated C string.
547#[no_mangle]
548pub unsafe extern "C" fn fs_create_dir(handle: *mut FsHandle, name: *const c_char) -> i32 {
549    let (h, name_str) = match validate_handle_and_name(handle, name) {
550        Ok(v) => v,
551        Err(code) => return code,
552    };
553    match h.core.create_directory(name_str) {
554        Ok(()) => FsErrorCode::Ok as i32,
555        Err(ref e) => FsErrorCode::from(e) as i32,
556    }
557}
558
559/// Remove a file (or empty directory) at the given path.
560///
561/// # Safety
562/// `name` must be a valid null-terminated C string (may contain `/` separators).
563#[no_mangle]
564pub unsafe extern "C" fn fs_remove_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
565    let (h, name_str) = match validate_handle_and_name(handle, name) {
566        Ok(v) => v,
567        Err(code) => return code,
568    };
569    match h.core.remove_file(name_str) {
570        Ok(()) => FsErrorCode::Ok as i32,
571        Err(ref e) => FsErrorCode::from(e) as i32,
572    }
573}
574
575/// Rename a file or directory.  Both paths must share the same parent directory.
576///
577/// # Safety
578/// `old_name` and `new_name` must be valid null-terminated C strings (may contain `/` separators).
579#[no_mangle]
580pub unsafe extern "C" fn fs_rename(
581    handle: *mut FsHandle,
582    old_name: *const c_char,
583    new_name: *const c_char,
584) -> i32 {
585    let Some(h) = (unsafe { handle.as_mut() }) else {
586        return FsErrorCode::InvalidArgument as i32;
587    };
588    let old_str = match unsafe_cstr_to_str(old_name) {
589        Some(s) => s,
590        None => return FsErrorCode::InvalidArgument as i32,
591    };
592    let new_str = match unsafe_cstr_to_str(new_name) {
593        Some(s) => s,
594        None => return FsErrorCode::InvalidArgument as i32,
595    };
596    match h.core.rename(old_str, new_str) {
597        Ok(()) => FsErrorCode::Ok as i32,
598        Err(ref e) => FsErrorCode::from(e) as i32,
599    }
600}
601
602/// Flush buffered writes to the block store **without** calling fsync.
603///
604/// Use this for FUSE `write`/`release` handlers.  Call [`fs_sync`] only
605/// for explicit fsync requests.
606///
607/// # Safety
608/// `handle` must be a valid pointer.
609#[no_mangle]
610pub unsafe extern "C" fn fs_flush(handle: *mut FsHandle) -> i32 {
611    let Some(h) = (unsafe { handle.as_mut() }) else {
612        return FsErrorCode::InvalidArgument as i32;
613    };
614    match h.core.flush() {
615        Ok(()) => FsErrorCode::Ok as i32,
616        Err(ref e) => FsErrorCode::from(e) as i32,
617    }
618}
619
620/// Sync / flush the filesystem (flush + fsync).
621///
622/// # Safety
623/// `handle` must be a valid pointer.
624#[no_mangle]
625pub unsafe extern "C" fn fs_sync(handle: *mut FsHandle) -> i32 {
626    let Some(h) = (unsafe { handle.as_mut() }) else {
627        return FsErrorCode::InvalidArgument as i32;
628    };
629    match h.core.sync() {
630        Ok(()) => FsErrorCode::Ok as i32,
631        Err(ref e) => FsErrorCode::from(e) as i32,
632    }
633}
634
635/// Stat metadata for a single file/directory.
636///
637/// Returns `0` on success and populates the out-parameters.  Much cheaper
638/// than `fs_list_dir` for FUSE `getattr` / `lookup`.
639///
640/// `out_size`:  file size in bytes (or 0 for directories).
641/// `out_kind`:  0 = file, 1 = directory.
642/// `out_inode_id`: the logical inode id.
643///
644/// # Safety
645/// `handle`, `name`, and all `out_*` pointers must be valid.
646#[no_mangle]
647pub unsafe extern "C" fn fs_stat(
648    handle: *mut FsHandle,
649    name: *const c_char,
650    out_size: *mut u64,
651    out_kind: *mut i32,
652    out_inode_id: *mut u64,
653) -> i32 {
654    let (h, name_str) = match validate_handle_and_name(handle, name) {
655        Ok(v) => v,
656        Err(code) => return code,
657    };
658    if out_size.is_null() || out_kind.is_null() || out_inode_id.is_null() {
659        return FsErrorCode::InvalidArgument as i32;
660    }
661    match h.core.stat(name_str) {
662        Ok(entry) => {
663            unsafe {
664                *out_size = entry.size;
665                *out_kind = match entry.kind {
666                    crate::model::InodeKind::File => 0,
667                    crate::model::InodeKind::Directory => 1,
668                };
669                *out_inode_id = entry.inode_id;
670            }
671            FsErrorCode::Ok as i32
672        }
673        Err(ref e) => FsErrorCode::from(e) as i32,
674    }
675}
676
677/// Fill all unused blocks with cryptographically random data.
678///
679/// # Safety
680/// `handle` must be a valid pointer.
681#[no_mangle]
682pub unsafe extern "C" fn fs_scrub_free_blocks(handle: *mut FsHandle) -> i32 {
683    let Some(h) = (unsafe { handle.as_mut() }) else {
684        return FsErrorCode::InvalidArgument as i32;
685    };
686    match h.core.scrub_free_blocks() {
687        Ok(()) => FsErrorCode::Ok as i32,
688        Err(ref e) => FsErrorCode::from(e) as i32,
689    }
690}
691
692/// Compute a BLAKE3 hash of the supplied data.
693///
694/// Writes exactly 32 bytes into `out`. Returns 0 on success,
695/// or `InvalidArgument` if any pointer is null.
696///
697/// # Safety
698/// `data` must point to `data_len` valid bytes.
699/// `out` must point to a buffer of at least 32 bytes.
700#[no_mangle]
701pub unsafe extern "C" fn fs_blake3(data: *const u8, data_len: usize, out: *mut u8) -> i32 {
702    if data.is_null() || out.is_null() {
703        return FsErrorCode::InvalidArgument as i32;
704    }
705    let input = unsafe { std::slice::from_raw_parts(data, data_len) };
706    let hash = blake3::hash(input);
707    unsafe { std::ptr::copy_nonoverlapping(hash.as_bytes().as_ptr(), out, 32) };
708    FsErrorCode::Ok as i32
709}
710
711/// Free a string previously returned by `fs_list_root`.
712///
713/// # Safety
714/// `s` must be a pointer previously returned by a `fs_*` function, or null.
715#[no_mangle]
716pub unsafe extern "C" fn fs_free_string(s: *mut c_char) {
717    if !s.is_null() {
718        unsafe {
719            drop(CString::from_raw(s));
720        }
721    }
722}
723
724// ── Internal helpers ──
725
726/// # Safety
727/// `ptr` must be a valid null-terminated C string or null.
728unsafe fn unsafe_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
729    if ptr.is_null() {
730        return None;
731    }
732    unsafe { CStr::from_ptr(ptr) }.to_str().ok()
733}
734
735unsafe fn validate_handle_and_name<'a>(
736    handle: *mut FsHandle,
737    name: *const c_char,
738) -> Result<(&'a mut FsHandle, &'a str), i32> {
739    let h = unsafe { handle.as_mut() }.ok_or(FsErrorCode::InvalidArgument as i32)?;
740    let name_str =
741        unsafe { unsafe_cstr_to_str(name) }.ok_or(FsErrorCode::InvalidArgument as i32)?;
742    Ok((h, name_str))
743}