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::crypto::ChaChaEngine;
23use crate::error::FsErrorCode;
24use crate::fs::FilesystemCore;
25use crate::model::DEFAULT_BLOCK_SIZE;
26
27/// Opaque handle to a FilesystemCore instance.
28pub struct FsHandle {
29    core: FilesystemCore,
30}
31
32// ── Lifecycle ──
33
34/// Create a new in-memory filesystem handle.
35///
36/// `total_blocks`: number of blocks in the virtual block device.
37/// `master_key`: pointer to the master encryption key bytes.
38/// `master_key_len`: length of master_key in bytes (should be 32).
39///
40/// Returns a pointer to an opaque handle, or null on failure.
41///
42/// # Safety
43/// `master_key` must point to `master_key_len` valid bytes.
44#[no_mangle]
45pub unsafe extern "C" fn fs_create(
46    total_blocks: u64,
47    master_key: *const u8,
48    master_key_len: usize,
49) -> *mut FsHandle {
50    if master_key.is_null() || master_key_len == 0 {
51        return ptr::null_mut();
52    }
53    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
54
55    let store = Arc::new(MemoryBlockStore::new(DEFAULT_BLOCK_SIZE, total_blocks));
56    let crypto = match ChaChaEngine::new(key_slice) {
57        Ok(c) => Arc::new(c),
58        Err(_) => return ptr::null_mut(),
59    };
60
61    let core = FilesystemCore::new(store, crypto);
62    let handle = Box::new(FsHandle { core });
63    Box::into_raw(handle)
64}
65
66/// Create a filesystem handle backed by a file on disk.
67///
68/// `path`: null-terminated path to the image file.
69/// `total_blocks`: number of blocks. Pass 0 to infer from file size.
70/// `block_size`: block size in bytes. Pass 0 to use the default (65536).
71/// `create_new`: if nonzero, create a new file (fails if it already exists).
72///               if zero, open an existing file.
73/// `master_key`: pointer to the master encryption key bytes.
74/// `master_key_len`: length of master_key in bytes (should be 32).
75///
76/// Returns a pointer to an opaque handle, or null on failure.
77///
78/// # Safety
79/// - `path` must be a valid null-terminated C string.
80/// - `master_key` must point to `master_key_len` valid bytes.
81#[no_mangle]
82pub unsafe extern "C" fn fs_create_disk(
83    path: *const c_char,
84    total_blocks: u64,
85    block_size: u32,
86    create_new: i32,
87    master_key: *const u8,
88    master_key_len: usize,
89) -> *mut FsHandle {
90    if master_key.is_null() || master_key_len == 0 {
91        return ptr::null_mut();
92    }
93    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
94        Some(s) => s,
95        None => return ptr::null_mut(),
96    };
97    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
98
99    let bs = if block_size == 0 {
100        DEFAULT_BLOCK_SIZE
101    } else {
102        block_size as usize
103    };
104
105    let store = if create_new != 0 {
106        match DiskBlockStore::create(path_str, bs, total_blocks) {
107            Ok(s) => Arc::new(s),
108            Err(_) => return ptr::null_mut(),
109        }
110    } else {
111        match DiskBlockStore::open(path_str, bs, total_blocks) {
112            Ok(s) => Arc::new(s),
113            Err(_) => return ptr::null_mut(),
114        }
115    };
116
117    let crypto = match ChaChaEngine::new(key_slice) {
118        Ok(c) => Arc::new(c),
119        Err(_) => return ptr::null_mut(),
120    };
121
122    let core = FilesystemCore::new(store, crypto);
123    let handle = Box::new(FsHandle { core });
124    Box::into_raw(handle)
125}
126
127/// Create a filesystem handle backed by a raw block device (e.g. `/dev/xvdf`).
128///
129/// `path`: null-terminated path to the block device.
130/// `total_blocks`: number of blocks. Pass 0 to infer from the device size.
131/// `block_size`: block size in bytes. Pass 0 to use the default (65536).
132/// `initialize`: if nonzero, fill the device with random data first (slow on
133///               large devices). If zero, open the device as-is.
134/// `master_key`: pointer to the master encryption key bytes.
135/// `master_key_len`: length of master_key in bytes (should be 32).
136///
137/// Returns a pointer to an opaque handle, or null on failure.
138///
139/// # Safety
140/// - `path` must be a valid null-terminated C string.
141/// - `master_key` must point to `master_key_len` valid bytes.
142#[no_mangle]
143pub unsafe extern "C" fn fs_create_device(
144    path: *const c_char,
145    total_blocks: u64,
146    block_size: u32,
147    initialize: i32,
148    master_key: *const u8,
149    master_key_len: usize,
150) -> *mut FsHandle {
151    if master_key.is_null() || master_key_len == 0 {
152        return ptr::null_mut();
153    }
154    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
155        Some(s) => s,
156        None => return ptr::null_mut(),
157    };
158    let key_slice = unsafe { std::slice::from_raw_parts(master_key, master_key_len) };
159
160    let bs = if block_size == 0 {
161        DEFAULT_BLOCK_SIZE
162    } else {
163        block_size as usize
164    };
165
166    let store = if initialize != 0 {
167        match DeviceBlockStore::initialize(path_str, bs, total_blocks) {
168            Ok(s) => Arc::new(s),
169            Err(_) => return ptr::null_mut(),
170        }
171    } else {
172        match DeviceBlockStore::open(path_str, bs, total_blocks) {
173            Ok(s) => Arc::new(s),
174            Err(_) => return ptr::null_mut(),
175        }
176    };
177
178    let crypto = match ChaChaEngine::new(key_slice) {
179        Ok(c) => Arc::new(c),
180        Err(_) => return ptr::null_mut(),
181    };
182
183    let core = FilesystemCore::new(store, crypto);
184    let handle = Box::new(FsHandle { core });
185    Box::into_raw(handle)
186}
187
188/// Destroy a filesystem handle and free all associated resources.
189///
190/// # Safety
191/// `handle` must be a valid pointer returned by `fs_create` or `fs_create_disk`,
192/// and must not be used after this call.
193#[no_mangle]
194pub unsafe extern "C" fn fs_destroy(handle: *mut FsHandle) {
195    if !handle.is_null() {
196        unsafe {
197            drop(Box::from_raw(handle));
198        }
199    }
200}
201
202// ── Filesystem operations ──
203
204/// Initialize a new filesystem on the block store.
205///
206/// # Safety
207/// `handle` must be a valid pointer returned by `fs_create`.
208#[no_mangle]
209pub unsafe extern "C" fn fs_init_filesystem(handle: *mut FsHandle) -> i32 {
210    let Some(h) = (unsafe { handle.as_mut() }) else {
211        return FsErrorCode::InvalidArgument as i32;
212    };
213    match h.core.init_filesystem() {
214        Ok(()) => FsErrorCode::Ok as i32,
215        Err(ref e) => FsErrorCode::from(e) as i32,
216    }
217}
218
219/// Open / mount an existing filesystem from the block store.
220///
221/// # Safety
222/// `handle` must be a valid pointer returned by `fs_create`.
223#[no_mangle]
224pub unsafe extern "C" fn fs_open(handle: *mut FsHandle) -> i32 {
225    let Some(h) = (unsafe { handle.as_mut() }) else {
226        return FsErrorCode::InvalidArgument as i32;
227    };
228    match h.core.open() {
229        Ok(()) => FsErrorCode::Ok as i32,
230        Err(ref e) => FsErrorCode::from(e) as i32,
231    }
232}
233
234/// Create a file at the given path.
235///
236/// Parent directories must already exist.  The `name` argument may be
237/// a `/`-separated path such as `"a/b/file.txt"`.
238///
239/// # Safety
240/// `name` must be a valid null-terminated C string.
241#[no_mangle]
242pub unsafe extern "C" fn fs_create_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
243    let (h, name_str) = match validate_handle_and_name(handle, name) {
244        Ok(v) => v,
245        Err(code) => return code,
246    };
247    match h.core.create_file(name_str) {
248        Ok(()) => FsErrorCode::Ok as i32,
249        Err(ref e) => FsErrorCode::from(e) as i32,
250    }
251}
252
253/// Write data to a file at the given path.
254///
255/// # Safety
256/// - `name` must be a valid null-terminated C string (may contain `/` separators).
257/// - `data` must point to `data_len` valid bytes.
258#[no_mangle]
259pub unsafe extern "C" fn fs_write_file(
260    handle: *mut FsHandle,
261    name: *const c_char,
262    offset: u64,
263    data: *const u8,
264    data_len: usize,
265) -> i32 {
266    let (h, name_str) = match validate_handle_and_name(handle, name) {
267        Ok(v) => v,
268        Err(code) => return code,
269    };
270    if data.is_null() && data_len > 0 {
271        return FsErrorCode::InvalidArgument as i32;
272    }
273    let slice = if data_len > 0 {
274        unsafe { std::slice::from_raw_parts(data, data_len) }
275    } else {
276        &[]
277    };
278    match h.core.write_file(name_str, offset, slice) {
279        Ok(()) => FsErrorCode::Ok as i32,
280        Err(ref e) => FsErrorCode::from(e) as i32,
281    }
282}
283
284/// Read file data into a caller-provided buffer.
285///
286/// On success, writes the actual number of bytes read to `*out_len` and returns 0.
287/// If the buffer is too small, returns `BufferTooSmall` and sets `*out_len` to the required size.
288///
289/// # Safety
290/// - `name` must be a valid null-terminated C string.
291/// - `out_buf` must point to at least `buf_capacity` writable bytes.
292/// - `out_len` must be a valid pointer.
293#[no_mangle]
294pub unsafe extern "C" fn fs_read_file(
295    handle: *mut FsHandle,
296    name: *const c_char,
297    offset: u64,
298    len: usize,
299    out_buf: *mut u8,
300    out_len: *mut usize,
301) -> i32 {
302    let (h, name_str) = match validate_handle_and_name(handle, name) {
303        Ok(v) => v,
304        Err(code) => return code,
305    };
306    if out_buf.is_null() || out_len.is_null() {
307        return FsErrorCode::InvalidArgument as i32;
308    }
309
310    match h.core.read_file(name_str, offset, len) {
311        Ok(data) => {
312            let buf_capacity = len;
313            if data.len() > buf_capacity {
314                unsafe { *out_len = data.len() };
315                return FsErrorCode::BufferTooSmall as i32;
316            }
317            unsafe {
318                ptr::copy_nonoverlapping(data.as_ptr(), out_buf, data.len());
319                *out_len = data.len();
320            }
321            FsErrorCode::Ok as i32
322        }
323        Err(ref e) => FsErrorCode::from(e) as i32,
324    }
325}
326
327/// List the root directory. Returns a JSON string.
328///
329/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
330/// On error, returns null and writes the error code to `*out_error`.
331///
332/// For listing a subdirectory, use `fs_list_dir` instead.
333///
334/// # Safety
335/// - `handle` must be a valid pointer.
336/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
337#[no_mangle]
338pub unsafe extern "C" fn fs_list_root(handle: *mut FsHandle, out_error: *mut i32) -> *mut c_char {
339    let Some(h) = (unsafe { handle.as_mut() }) else {
340        if !out_error.is_null() {
341            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
342        }
343        return ptr::null_mut();
344    };
345
346    match h.core.list_directory("") {
347        Ok(entries) => {
348            let json = match serde_json::to_string(&entries) {
349                Ok(j) => j,
350                Err(_) => {
351                    if !out_error.is_null() {
352                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
353                    }
354                    return ptr::null_mut();
355                }
356            };
357            if !out_error.is_null() {
358                unsafe { *out_error = FsErrorCode::Ok as i32 };
359            }
360            match CString::new(json) {
361                Ok(cs) => cs.into_raw(),
362                Err(_) => {
363                    if !out_error.is_null() {
364                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
365                    }
366                    ptr::null_mut()
367                }
368            }
369        }
370        Err(ref e) => {
371            if !out_error.is_null() {
372                unsafe { *out_error = FsErrorCode::from(e) as i32 };
373            }
374            ptr::null_mut()
375        }
376    }
377}
378
379/// List a directory at the given path. Returns a JSON string.
380///
381/// Pass an empty string or `"/"` to list the root directory.
382///
383/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
384/// On error, returns null and writes the error code to `*out_error`.
385///
386/// # Safety
387/// - `handle` must be a valid pointer.
388/// - `path` must be a valid null-terminated C string (may contain `/` separators).
389/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
390#[no_mangle]
391pub unsafe extern "C" fn fs_list_dir(
392    handle: *mut FsHandle,
393    path: *const c_char,
394    out_error: *mut i32,
395) -> *mut c_char {
396    let Some(h) = (unsafe { handle.as_mut() }) else {
397        if !out_error.is_null() {
398            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
399        }
400        return ptr::null_mut();
401    };
402    let path_str = match unsafe { unsafe_cstr_to_str(path) } {
403        Some(s) => s,
404        None => {
405            if !out_error.is_null() {
406                unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
407            }
408            return ptr::null_mut();
409        }
410    };
411
412    match h.core.list_directory(path_str) {
413        Ok(entries) => {
414            let json = match serde_json::to_string(&entries) {
415                Ok(j) => j,
416                Err(_) => {
417                    if !out_error.is_null() {
418                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
419                    }
420                    return ptr::null_mut();
421                }
422            };
423            if !out_error.is_null() {
424                unsafe { *out_error = FsErrorCode::Ok as i32 };
425            }
426            match CString::new(json) {
427                Ok(cs) => cs.into_raw(),
428                Err(_) => {
429                    if !out_error.is_null() {
430                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
431                    }
432                    ptr::null_mut()
433                }
434            }
435        }
436        Err(ref e) => {
437            if !out_error.is_null() {
438                unsafe { *out_error = FsErrorCode::from(e) as i32 };
439            }
440            ptr::null_mut()
441        }
442    }
443}
444
445/// Create a directory at the given path.
446///
447/// Parent directories must already exist.  The `name` argument may be
448/// a `/`-separated path such as `"a/b/newdir"`.
449///
450/// # Safety
451/// `name` must be a valid null-terminated C string.
452#[no_mangle]
453pub unsafe extern "C" fn fs_create_dir(handle: *mut FsHandle, name: *const c_char) -> i32 {
454    let (h, name_str) = match validate_handle_and_name(handle, name) {
455        Ok(v) => v,
456        Err(code) => return code,
457    };
458    match h.core.create_directory(name_str) {
459        Ok(()) => FsErrorCode::Ok as i32,
460        Err(ref e) => FsErrorCode::from(e) as i32,
461    }
462}
463
464/// Remove a file (or empty directory) at the given path.
465///
466/// # Safety
467/// `name` must be a valid null-terminated C string (may contain `/` separators).
468#[no_mangle]
469pub unsafe extern "C" fn fs_remove_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
470    let (h, name_str) = match validate_handle_and_name(handle, name) {
471        Ok(v) => v,
472        Err(code) => return code,
473    };
474    match h.core.remove_file(name_str) {
475        Ok(()) => FsErrorCode::Ok as i32,
476        Err(ref e) => FsErrorCode::from(e) as i32,
477    }
478}
479
480/// Rename a file or directory.  Both paths must share the same parent directory.
481///
482/// # Safety
483/// `old_name` and `new_name` must be valid null-terminated C strings (may contain `/` separators).
484#[no_mangle]
485pub unsafe extern "C" fn fs_rename(
486    handle: *mut FsHandle,
487    old_name: *const c_char,
488    new_name: *const c_char,
489) -> i32 {
490    let Some(h) = (unsafe { handle.as_mut() }) else {
491        return FsErrorCode::InvalidArgument as i32;
492    };
493    let old_str = match unsafe_cstr_to_str(old_name) {
494        Some(s) => s,
495        None => return FsErrorCode::InvalidArgument as i32,
496    };
497    let new_str = match unsafe_cstr_to_str(new_name) {
498        Some(s) => s,
499        None => return FsErrorCode::InvalidArgument as i32,
500    };
501    match h.core.rename(old_str, new_str) {
502        Ok(()) => FsErrorCode::Ok as i32,
503        Err(ref e) => FsErrorCode::from(e) as i32,
504    }
505}
506
507/// Sync / flush the filesystem.
508///
509/// # Safety
510/// `handle` must be a valid pointer.
511#[no_mangle]
512pub unsafe extern "C" fn fs_sync(handle: *mut FsHandle) -> i32 {
513    let Some(h) = (unsafe { handle.as_mut() }) else {
514        return FsErrorCode::InvalidArgument as i32;
515    };
516    match h.core.sync() {
517        Ok(()) => FsErrorCode::Ok as i32,
518        Err(ref e) => FsErrorCode::from(e) as i32,
519    }
520}
521
522/// Free a string previously returned by `fs_list_root`.
523///
524/// # Safety
525/// `s` must be a pointer previously returned by a `fs_*` function, or null.
526#[no_mangle]
527pub unsafe extern "C" fn fs_free_string(s: *mut c_char) {
528    if !s.is_null() {
529        unsafe {
530            drop(CString::from_raw(s));
531        }
532    }
533}
534
535// ── Internal helpers ──
536
537/// # Safety
538/// `ptr` must be a valid null-terminated C string or null.
539unsafe fn unsafe_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
540    if ptr.is_null() {
541        return None;
542    }
543    unsafe { CStr::from_ptr(ptr) }.to_str().ok()
544}
545
546unsafe fn validate_handle_and_name<'a>(
547    handle: *mut FsHandle,
548    name: *const c_char,
549) -> Result<(&'a mut FsHandle, &'a str), i32> {
550    let h = unsafe { handle.as_mut() }.ok_or(FsErrorCode::InvalidArgument as i32)?;
551    let name_str =
552        unsafe { unsafe_cstr_to_str(name) }.ok_or(FsErrorCode::InvalidArgument as i32)?;
553    Ok((h, name_str))
554}