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 in the root directory.
235///
236/// # Safety
237/// `name` must be a valid null-terminated C string.
238#[no_mangle]
239pub unsafe extern "C" fn fs_create_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
240    let (h, name_str) = match validate_handle_and_name(handle, name) {
241        Ok(v) => v,
242        Err(code) => return code,
243    };
244    match h.core.create_file(name_str) {
245        Ok(()) => FsErrorCode::Ok as i32,
246        Err(ref e) => FsErrorCode::from(e) as i32,
247    }
248}
249
250/// Write data to a file.
251///
252/// # Safety
253/// - `name` must be a valid null-terminated C string.
254/// - `data` must point to `data_len` valid bytes.
255#[no_mangle]
256pub unsafe extern "C" fn fs_write_file(
257    handle: *mut FsHandle,
258    name: *const c_char,
259    offset: u64,
260    data: *const u8,
261    data_len: usize,
262) -> i32 {
263    let (h, name_str) = match validate_handle_and_name(handle, name) {
264        Ok(v) => v,
265        Err(code) => return code,
266    };
267    if data.is_null() && data_len > 0 {
268        return FsErrorCode::InvalidArgument as i32;
269    }
270    let slice = if data_len > 0 {
271        unsafe { std::slice::from_raw_parts(data, data_len) }
272    } else {
273        &[]
274    };
275    match h.core.write_file(name_str, offset, slice) {
276        Ok(()) => FsErrorCode::Ok as i32,
277        Err(ref e) => FsErrorCode::from(e) as i32,
278    }
279}
280
281/// Read file data into a caller-provided buffer.
282///
283/// On success, writes the actual number of bytes read to `*out_len` and returns 0.
284/// If the buffer is too small, returns `BufferTooSmall` and sets `*out_len` to the required size.
285///
286/// # Safety
287/// - `name` must be a valid null-terminated C string.
288/// - `out_buf` must point to at least `buf_capacity` writable bytes.
289/// - `out_len` must be a valid pointer.
290#[no_mangle]
291pub unsafe extern "C" fn fs_read_file(
292    handle: *mut FsHandle,
293    name: *const c_char,
294    offset: u64,
295    len: usize,
296    out_buf: *mut u8,
297    out_len: *mut usize,
298) -> i32 {
299    let (h, name_str) = match validate_handle_and_name(handle, name) {
300        Ok(v) => v,
301        Err(code) => return code,
302    };
303    if out_buf.is_null() || out_len.is_null() {
304        return FsErrorCode::InvalidArgument as i32;
305    }
306
307    match h.core.read_file(name_str, offset, len) {
308        Ok(data) => {
309            let buf_capacity = len;
310            if data.len() > buf_capacity {
311                unsafe { *out_len = data.len() };
312                return FsErrorCode::BufferTooSmall as i32;
313            }
314            unsafe {
315                ptr::copy_nonoverlapping(data.as_ptr(), out_buf, data.len());
316                *out_len = data.len();
317            }
318            FsErrorCode::Ok as i32
319        }
320        Err(ref e) => FsErrorCode::from(e) as i32,
321    }
322}
323
324/// List the root directory. Returns a JSON string.
325///
326/// The returned string is allocated by Rust. The caller must free it with `fs_free_string`.
327/// On error, returns null and writes the error code to `*out_error`.
328///
329/// # Safety
330/// - `handle` must be a valid pointer.
331/// - `out_error` must be a valid pointer (or null if the caller doesn't need the error code).
332#[no_mangle]
333pub unsafe extern "C" fn fs_list_root(handle: *mut FsHandle, out_error: *mut i32) -> *mut c_char {
334    let Some(h) = (unsafe { handle.as_mut() }) else {
335        if !out_error.is_null() {
336            unsafe { *out_error = FsErrorCode::InvalidArgument as i32 };
337        }
338        return ptr::null_mut();
339    };
340
341    match h.core.list_directory() {
342        Ok(entries) => {
343            let json = match serde_json::to_string(&entries) {
344                Ok(j) => j,
345                Err(_) => {
346                    if !out_error.is_null() {
347                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
348                    }
349                    return ptr::null_mut();
350                }
351            };
352            if !out_error.is_null() {
353                unsafe { *out_error = FsErrorCode::Ok as i32 };
354            }
355            match CString::new(json) {
356                Ok(cs) => cs.into_raw(),
357                Err(_) => {
358                    if !out_error.is_null() {
359                        unsafe { *out_error = FsErrorCode::InternalError as i32 };
360                    }
361                    ptr::null_mut()
362                }
363            }
364        }
365        Err(ref e) => {
366            if !out_error.is_null() {
367                unsafe { *out_error = FsErrorCode::from(e) as i32 };
368            }
369            ptr::null_mut()
370        }
371    }
372}
373
374/// Create a directory in the root directory.
375///
376/// # Safety
377/// `name` must be a valid null-terminated C string.
378#[no_mangle]
379pub unsafe extern "C" fn fs_create_dir(handle: *mut FsHandle, name: *const c_char) -> i32 {
380    let (h, name_str) = match validate_handle_and_name(handle, name) {
381        Ok(v) => v,
382        Err(code) => return code,
383    };
384    match h.core.create_directory(name_str) {
385        Ok(()) => FsErrorCode::Ok as i32,
386        Err(ref e) => FsErrorCode::from(e) as i32,
387    }
388}
389
390/// Remove a file (or empty directory) from the root directory.
391///
392/// # Safety
393/// `name` must be a valid null-terminated C string.
394#[no_mangle]
395pub unsafe extern "C" fn fs_remove_file(handle: *mut FsHandle, name: *const c_char) -> i32 {
396    let (h, name_str) = match validate_handle_and_name(handle, name) {
397        Ok(v) => v,
398        Err(code) => return code,
399    };
400    match h.core.remove_file(name_str) {
401        Ok(()) => FsErrorCode::Ok as i32,
402        Err(ref e) => FsErrorCode::from(e) as i32,
403    }
404}
405
406/// Rename a file or directory within the root directory.
407///
408/// # Safety
409/// `old_name` and `new_name` must be valid null-terminated C strings.
410#[no_mangle]
411pub unsafe extern "C" fn fs_rename(
412    handle: *mut FsHandle,
413    old_name: *const c_char,
414    new_name: *const c_char,
415) -> i32 {
416    let Some(h) = (unsafe { handle.as_mut() }) else {
417        return FsErrorCode::InvalidArgument as i32;
418    };
419    let old_str = match unsafe_cstr_to_str(old_name) {
420        Some(s) => s,
421        None => return FsErrorCode::InvalidArgument as i32,
422    };
423    let new_str = match unsafe_cstr_to_str(new_name) {
424        Some(s) => s,
425        None => return FsErrorCode::InvalidArgument as i32,
426    };
427    match h.core.rename(old_str, new_str) {
428        Ok(()) => FsErrorCode::Ok as i32,
429        Err(ref e) => FsErrorCode::from(e) as i32,
430    }
431}
432
433/// Sync / flush the filesystem.
434///
435/// # Safety
436/// `handle` must be a valid pointer.
437#[no_mangle]
438pub unsafe extern "C" fn fs_sync(handle: *mut FsHandle) -> i32 {
439    let Some(h) = (unsafe { handle.as_mut() }) else {
440        return FsErrorCode::InvalidArgument as i32;
441    };
442    match h.core.sync() {
443        Ok(()) => FsErrorCode::Ok as i32,
444        Err(ref e) => FsErrorCode::from(e) as i32,
445    }
446}
447
448/// Free a string previously returned by `fs_list_root`.
449///
450/// # Safety
451/// `s` must be a pointer previously returned by a `fs_*` function, or null.
452#[no_mangle]
453pub unsafe extern "C" fn fs_free_string(s: *mut c_char) {
454    if !s.is_null() {
455        unsafe {
456            drop(CString::from_raw(s));
457        }
458    }
459}
460
461// ── Internal helpers ──
462
463/// # Safety
464/// `ptr` must be a valid null-terminated C string or null.
465unsafe fn unsafe_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
466    if ptr.is_null() {
467        return None;
468    }
469    unsafe { CStr::from_ptr(ptr) }.to_str().ok()
470}
471
472unsafe fn validate_handle_and_name<'a>(
473    handle: *mut FsHandle,
474    name: *const c_char,
475) -> Result<(&'a mut FsHandle, &'a str), i32> {
476    let h = unsafe { handle.as_mut() }.ok_or(FsErrorCode::InvalidArgument as i32)?;
477    let name_str =
478        unsafe { unsafe_cstr_to_str(name) }.ok_or(FsErrorCode::InvalidArgument as i32)?;
479    Ok((h, name_str))
480}