fs_core/ffi.rs
1//! C ABI for the block-device framework.
2//!
3//! Every sister crate (qcow2 reader, partition probe, fs-* drivers) speaks
4//! through the [`FsCoreDevice`] handle defined here, so consumers (Swift
5//! FSKit modules, Go callers, C programs) only learn one device-handle
6//! type and one error convention.
7//!
8//! ## Conventions
9//!
10//! - Handles are opaque `*mut FsCoreDevice`. Allocate via a constructor in
11//! one of the sister crates (e.g. `qcow2_open` from rust-img-qcow2),
12//! free via [`fs_core_device_close`] regardless of which crate created
13//! it.
14//! - Error reporting is errno-style: every fallible function returns an
15//! [`FsCoreErrorCode`] (0 = OK, non-zero = failure) and stashes a human
16//! message in a thread-local. Read it via
17//! [`fs_core_last_error_message`].
18//! - Every entry point catches Rust panics with `catch_unwind` and maps
19//! them to [`FsCoreErrorCode::Panic`]. Crossing an FFI boundary while
20//! unwinding is UB; the catch-net is non-negotiable.
21//! - Thread safety: handles wrap `Arc<dyn BlockDevice>`, which is
22//! `Send + Sync` by trait bound. Multiple threads can call read/write
23//! concurrently as long as the underlying device's locking permits it.
24
25#![allow(clippy::missing_safety_doc)]
26
27use crate::block::BlockDevice;
28use crate::callback_device::CallbackDevice;
29use crate::error::Error;
30use std::cell::RefCell;
31use std::ffi::{c_char, c_int, c_void, CString};
32use std::io;
33use std::panic::AssertUnwindSafe;
34use std::ptr;
35use std::slice;
36use std::sync::Arc;
37
38// ---------------------------------------------------------------------------
39// Error codes — kept dense and stable so consumers can hard-code them.
40// ---------------------------------------------------------------------------
41
42/// Numeric error codes mirrored across every sister crate's C ABI.
43///
44/// `#[repr(i32)]` so the layout is identical to the matching C `enum`.
45#[repr(i32)]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum FsCoreErrorCode {
48 /// Success.
49 Ok = 0,
50 /// Underlying I/O failed.
51 Io = 1,
52 /// A read the source could not satisfy in full — it ran out of data.
53 /// What a file-backed handle returns for a read off the end of the
54 /// file, and what a slice returns for a read past its own end.
55 ShortRead = 2,
56 /// Write attempted on a read-only device.
57 ReadOnly = 3,
58 /// A request refused up front because its range lies outside the
59 /// device's declared size; nothing was transferred.
60 ///
61 /// This crate returns it only for a **write** past the end of an RW
62 /// slice. It reaches **reads** from sister crates whose container
63 /// declares a virtual size (the `img-*` readers), and from this
64 /// crate's caching / read-only / slice wrappers when they forward
65 /// such a parent's error. A C consumer that only wants to know "the
66 /// read overran the device", and does not control which crate opened
67 /// the handle, should accept this and `FS_CORE_SHORT_READ` alike —
68 /// and should not treat the pair as exhaustive, since a
69 /// callback-backed handle reports its host's refusal as
70 /// `FS_CORE_IO`.
71 OutOfBounds = 4,
72 /// Driver-specific error — message in the thread-local last-error.
73 Custom = 5,
74 /// One of the input pointers was null.
75 NullArg = 6,
76 /// `catch_unwind` caught a panic crossing the FFI boundary.
77 Panic = 7,
78 /// Reserved. Never returned.
79 ///
80 /// It was meant for a path that is not valid UTF-8, but the one
81 /// function that meets that case — `fs_core_open_file` — returns a
82 /// POINTER, not a code, so it reports the failure as NULL plus a
83 /// message and cannot return this. No other entry point takes a
84 /// path.
85 ///
86 /// Kept rather than removed because the numbering is published in
87 /// `include/fs_core.h` and a consumer may already switch on 8;
88 /// renumbering the codes after it would be an ABI break for a
89 /// tidiness gain. A future path-taking function that returns a code
90 /// should use this rather than invent another.
91 BadString = 8,
92}
93
94impl FsCoreErrorCode {
95 fn from_error(e: &Error) -> Self {
96 match e {
97 Error::Io(_) => FsCoreErrorCode::Io,
98 Error::ShortRead { .. } => FsCoreErrorCode::ShortRead,
99 Error::ReadOnly => FsCoreErrorCode::ReadOnly,
100 Error::OutOfBounds { .. } => FsCoreErrorCode::OutOfBounds,
101 Error::Custom(_) => FsCoreErrorCode::Custom,
102 }
103 }
104}
105
106// ---------------------------------------------------------------------------
107// Thread-local last-error — errno-style detail companion.
108// ---------------------------------------------------------------------------
109
110thread_local! {
111 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
112}
113
114/// Stash a message in the thread-local, replacing any previous one. Public
115/// to sister crates so they can populate it for their own error paths.
116pub fn set_last_error(message: impl Into<String>) {
117 let s = message.into();
118 let cs = CString::new(s.replace('\0', "?")).expect("contains no NUL after replace");
119 LAST_ERROR.with(|slot| {
120 *slot.borrow_mut() = Some(cs);
121 });
122}
123
124fn clear_last_error() {
125 LAST_ERROR.with(|slot| {
126 *slot.borrow_mut() = None;
127 });
128}
129
130/// Return a pointer to the calling thread's most recent error message, or
131/// NULL if there is none. The pointer is owned by the framework and remains
132/// valid until the next FFI call on this thread.
133#[unsafe(no_mangle)]
134pub extern "C" fn fs_core_last_error_message() -> *const c_char {
135 LAST_ERROR.with(|slot| {
136 slot.borrow()
137 .as_ref()
138 .map(|cs| cs.as_ptr())
139 .unwrap_or(ptr::null())
140 })
141}
142
143/// Helper for sister crates: run `body`, catch panics, map errors to codes,
144/// stash the message in the thread-local. Returns the error code.
145pub fn ffi_guard<F>(body: F) -> FsCoreErrorCode
146where
147 F: FnOnce() -> Result<(), Error>,
148{
149 clear_last_error();
150 match std::panic::catch_unwind(AssertUnwindSafe(body)) {
151 Ok(Ok(())) => FsCoreErrorCode::Ok,
152 Ok(Err(e)) => {
153 let code = FsCoreErrorCode::from_error(&e);
154 set_last_error(e.to_string());
155 code
156 }
157 Err(panic) => {
158 set_last_error(panic_message(&panic));
159 FsCoreErrorCode::Panic
160 }
161 }
162}
163
164/// Run `body`, catching a panic and returning `fail` instead — and
165/// recording the panic's message where a caller can read it.
166///
167/// # Why the message matters more than the fallback
168///
169/// Every fallback value here is also a legitimate answer. Zero is what
170/// an empty device reports for its size; `false` is what a read-only
171/// device reports for writability; a null pointer is what a failed open
172/// returns. So a caller that only sees the fallback cannot tell an
173/// ordinary answer from a driver that exploded computing it.
174///
175/// [`fs_core_last_error_message`] is what separates them, and a guard
176/// that returns the fallback without setting it throws away the only
177/// evidence there was.
178///
179/// # Why this is separate from [`ffi_guard`]
180///
181/// `ffi_guard` returns an [`FsCoreErrorCode`] and takes a body that
182/// returns `Result<(), Error>`. That fits an entry point whose whole
183/// answer is a status code, and fits nothing else — which is why the
184/// eight entry points in this file that return a size, a flag or a
185/// pointer each wrote `catch_unwind(AssertUnwindSafe(…)).unwrap_or(…)`
186/// by hand instead, sixty lines below the helper.
187///
188/// Sister crates did the same: eleven of them re-roll one of these two
189/// shapes rather than share either.
190///
191/// The error slot is cleared on entry, like [`ffi_guard`]: a call that
192/// succeeds must not leave the previous call's message in place for a
193/// caller to read and attribute to this one.
194///
195/// `AssertUnwindSafe` is used deliberately. The bodies here touch a
196/// handle the caller owns and a thread-local error slot; a panic can
197/// leave neither in a state another call can observe as inconsistent,
198/// because the handle is not read again on this path and the slot is
199/// overwritten whole.
200pub fn ffi_guard_or<T, F>(fail: T, body: F) -> T
201where
202 F: FnOnce() -> T,
203{
204 clear_last_error();
205 match std::panic::catch_unwind(AssertUnwindSafe(body)) {
206 Ok(value) => value,
207 Err(panic) => {
208 set_last_error(panic_message(&panic));
209 fail
210 }
211 }
212}
213
214/// What a caught panic actually said.
215///
216/// PUBLIC BECAUSE THE OTHER ELEVEN CRATES NEED IT. Each of them guards
217/// its own C entry points with `catch_unwind` and, having no way to
218/// reach this, reports the panic as `"panic in <function>"` -- the name
219/// of the function that was running, which the caller already knew, in
220/// place of the message, which is the only part it did not. An index
221/// out of bounds, a slice out of range, an `expect` with a sentence in
222/// it: all of it was thrown away at the boundary.
223///
224/// The guards themselves are NOT shareable, and that is why this is
225/// what moved rather than [`ffi_guard`]. Each crate's guard records the
226/// message into that crate's own thread-local, which is what its own C
227/// callers read; a guard from here would record into this crate's, and
228/// every panic message would land in a slot nobody reads.
229pub fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
230 if let Some(s) = panic.downcast_ref::<&'static str>() {
231 return (*s).to_string();
232 }
233 if let Some(s) = panic.downcast_ref::<String>() {
234 return s.clone();
235 }
236 "panic in FFI".to_string()
237}
238
239// ---------------------------------------------------------------------------
240// Device handle — opaque to C callers, shared across crates.
241// ---------------------------------------------------------------------------
242
243/// Opaque handle wrapping an `Arc<dyn BlockDevice>`. Allocated by sister
244/// crates' constructors and freed via [`fs_core_device_close`].
245pub struct FsCoreDevice {
246 inner: Arc<dyn BlockDevice>,
247}
248
249impl FsCoreDevice {
250 /// Internal constructor — sister crates use this to wrap their own
251 /// device types (Qcow2Reader, FileDevice, OwnedSlice, etc.) into the
252 /// shared handle type. Returns a `Box::into_raw` pointer ready to hand
253 /// across the FFI boundary.
254 pub fn into_handle(inner: Arc<dyn BlockDevice>) -> *mut FsCoreDevice {
255 Box::into_raw(Box::new(FsCoreDevice { inner }))
256 }
257
258 /// Borrow the inner device. `Arc::clone` it if you want shared
259 /// ownership — e.g. when handing the device to a slice adapter while
260 /// keeping the original handle alive.
261 pub fn inner(&self) -> &Arc<dyn BlockDevice> {
262 &self.inner
263 }
264}
265
266/// Free a device handle. Safe to call with NULL (no-op).
267#[unsafe(no_mangle)]
268pub unsafe extern "C" fn fs_core_device_close(handle: *mut FsCoreDevice) {
269 if handle.is_null() {
270 return;
271 }
272 ffi_guard_or((), || unsafe {
273 drop(Box::from_raw(handle));
274 });
275}
276
277/// Total device size in bytes. Returns 0 if `handle` is NULL.
278#[unsafe(no_mangle)]
279pub unsafe extern "C" fn fs_core_device_size_bytes(handle: *const FsCoreDevice) -> u64 {
280 if handle.is_null() {
281 return 0;
282 }
283 ffi_guard_or(0, || unsafe { (*handle).inner.size_bytes() })
284}
285
286/// True if `write_at` is likely to succeed. Returns false on NULL.
287#[unsafe(no_mangle)]
288pub unsafe extern "C" fn fs_core_device_is_writable(handle: *const FsCoreDevice) -> bool {
289 if handle.is_null() {
290 return false;
291 }
292 ffi_guard_or(false, || unsafe { (*handle).inner.is_writable() })
293}
294
295/// Read exactly `len` bytes from `offset` into `buf`. `buf` must be at
296/// least `len` bytes. Returns an `FsCoreErrorCode`.
297#[unsafe(no_mangle)]
298pub unsafe extern "C" fn fs_core_device_read_at(
299 handle: *const FsCoreDevice,
300 offset: u64,
301 buf: *mut u8,
302 len: usize,
303) -> FsCoreErrorCode {
304 // A null buffer is refused whatever the length. `from_raw_parts_mut`
305 // requires a non-null, aligned pointer even for a zero-length slice,
306 // so `(NULL, 0)` was undefined behaviour rather than the no-op it
307 // looks like -- in a crate that otherwise denies
308 // `unsafe_op_in_unsafe_fn`.
309 if handle.is_null() || buf.is_null() {
310 return FsCoreErrorCode::NullArg;
311 }
312 ffi_guard(|| {
313 let slice_buf = unsafe { slice::from_raw_parts_mut(buf, len) };
314 unsafe { (*handle).inner.read_at(offset, slice_buf) }
315 })
316}
317
318/// Write exactly `len` bytes from `buf` to `offset`. Returns `ReadOnly`
319/// for read-only devices.
320#[unsafe(no_mangle)]
321pub unsafe extern "C" fn fs_core_device_write_at(
322 handle: *const FsCoreDevice,
323 offset: u64,
324 buf: *const u8,
325 len: usize,
326) -> FsCoreErrorCode {
327 // Null is refused whatever the length; see `fs_core_device_read_at`.
328 if handle.is_null() || buf.is_null() {
329 return FsCoreErrorCode::NullArg;
330 }
331 ffi_guard(|| {
332 let slice_buf = unsafe { slice::from_raw_parts(buf, len) };
333 unsafe { (*handle).inner.write_at(offset, slice_buf) }
334 })
335}
336
337/// Flush pending writes to stable storage.
338#[unsafe(no_mangle)]
339pub unsafe extern "C" fn fs_core_device_flush(handle: *const FsCoreDevice) -> FsCoreErrorCode {
340 if handle.is_null() {
341 return FsCoreErrorCode::NullArg;
342 }
343 ffi_guard(|| unsafe { (*handle).inner.flush() })
344}
345
346// ---------------------------------------------------------------------------
347// Convenience: open a regular file as a device. Saves callers the trouble
348// of building a Rust crate just to wrap `FileDevice`.
349// ---------------------------------------------------------------------------
350
351/// Open `path` (NUL-terminated UTF-8) as a `FileDevice` and return a
352/// handle. Pass `writable=true` for RW. On failure returns NULL and the
353/// thread-local last-error has detail.
354#[unsafe(no_mangle)]
355pub unsafe extern "C" fn fs_core_file_open(
356 path: *const c_char,
357 writable: bool,
358) -> *mut FsCoreDevice {
359 if path.is_null() {
360 set_last_error("path is null");
361 return ptr::null_mut();
362 }
363 ffi_guard_or(ptr::null_mut(), || {
364 let cstr = unsafe { std::ffi::CStr::from_ptr(path) };
365 let s = match cstr.to_str() {
366 Ok(s) => s,
367 Err(_) => {
368 set_last_error("path is not valid UTF-8");
369 return ptr::null_mut();
370 }
371 };
372 let dev = if writable {
373 crate::file_device::FileDevice::open_rw(s)
374 } else {
375 crate::file_device::FileDevice::open(s)
376 };
377 match dev {
378 Ok(d) => FsCoreDevice::into_handle(Arc::new(d)),
379 Err(e) => {
380 set_last_error(e.to_string());
381 ptr::null_mut()
382 }
383 }
384 })
385}
386
387// ---------------------------------------------------------------------------
388// Callback-backed device. Used when the caller already owns the underlying
389// resource (FSKit FSBlockDeviceResource, Go file handle, C-side fd) and
390// wants to expose it as an `FsCoreDevice` so it can be stacked under a
391// container reader (qcow2, vhd, ...) before reaching a filesystem driver.
392// ---------------------------------------------------------------------------
393
394/// Read callback. Returns 0 on success, non-zero (errno-like) on failure.
395/// Must fully fill `len` bytes — short reads are treated as I/O errors.
396pub type FsCoreReadCb =
397 Option<unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int>;
398
399/// Write callback. NULL → device is read-only.
400pub type FsCoreWriteCb = Option<
401 unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *const u8, len: usize) -> c_int,
402>;
403
404/// Flush/fsync callback. NULL → flush is a no-op.
405pub type FsCoreFlushCb = Option<unsafe extern "C" fn(ctx: *mut c_void) -> c_int>;
406
407/// Configuration passed to [`fs_core_device_from_callbacks`].
408#[repr(C)]
409pub struct FsCoreCallbackCfg {
410 pub read: FsCoreReadCb,
411 pub write: FsCoreWriteCb,
412 pub flush: FsCoreFlushCb,
413 pub ctx: *mut c_void,
414 pub size: u64,
415}
416
417/// Turn a callback's non-zero return into an `io::Error`.
418fn cb_io_err(rc: c_int, op: &str) -> io::Error {
419 io::Error::other(format!("callback {op} returned {rc}"))
420}
421
422/// The host callback contract, in one place: **zero is success**.
423///
424/// All three adapters below wrapped a call in the same four lines —
425/// invoke, compare against zero, `Ok(())` or `cb_io_err`. Three copies
426/// of a convention is three chances to write `rc != 0` where the others
427/// write `rc == 0`, and a caller would see reads succeed while writes
428/// reported failure on the very same device.
429///
430/// `op` names the operation in the error, which is the only thing the
431/// three genuinely differ in.
432fn cb_result(rc: c_int, op: &'static str) -> io::Result<()> {
433 if rc == 0 {
434 Ok(())
435 } else {
436 Err(cb_io_err(rc, op))
437 }
438}
439
440/// Build an [`FsCoreDevice`] backed by host-provided callbacks. Returns NULL
441/// on failure (config null, read callback null, etc.) and stashes detail in
442/// the thread-local last-error.
443///
444/// `cfg.ctx` is opaque to fs-core; it is passed back verbatim to every
445/// callback invocation. The caller is responsible for ensuring it remains
446/// valid until [`fs_core_device_close`] is called on the returned handle.
447#[unsafe(no_mangle)]
448pub unsafe extern "C" fn fs_core_device_from_callbacks(
449 cfg: *const FsCoreCallbackCfg,
450) -> *mut FsCoreDevice {
451 if cfg.is_null() {
452 set_last_error("cfg is null");
453 return ptr::null_mut();
454 }
455 ffi_guard_or(ptr::null_mut(), || unsafe {
456 let cfg = &*cfg;
457 let read_fn = match cfg.read {
458 Some(f) => f,
459 None => {
460 set_last_error("cfg.read is null");
461 return ptr::null_mut();
462 }
463 };
464 let write_fn = cfg.write;
465 let flush_fn = cfg.flush;
466 // `*mut c_void` is `!Send + !Sync` by default, and `unsafe impl
467 // Send` on a newtype does not propagate cleanly through closure
468 // auto-traits. Round-tripping the pointer through `usize` gives
469 // something that is `Copy + Send + Sync`, and the callback
470 // contract already puts the host on the hook for using `ctx`
471 // safely across threads.
472 let ctx_addr = cfg.ctx as usize;
473 let size = cfg.size;
474
475 let read_cb: crate::callback_device::ReadCb = Box::new(move |off, buf| {
476 let ctx = ctx_addr as *mut c_void;
477 cb_result(read_fn(ctx, off, buf.as_mut_ptr(), buf.len()), "read")
478 });
479 let write_cb: Option<crate::callback_device::WriteCb> = write_fn.map(|f| {
480 Box::new(move |off, buf: &[u8]| {
481 let ctx = ctx_addr as *mut c_void;
482 cb_result(f(ctx, off, buf.as_ptr(), buf.len()), "write")
483 }) as crate::callback_device::WriteCb
484 });
485 let flush_cb: Option<crate::callback_device::FlushCb> = flush_fn.map(|f| {
486 Box::new(move || {
487 let ctx = ctx_addr as *mut c_void;
488 cb_result(f(ctx), "flush")
489 }) as crate::callback_device::FlushCb
490 });
491
492 let dev = CallbackDevice {
493 size,
494 read: read_cb,
495 write: write_cb,
496 flush: flush_cb,
497 };
498 FsCoreDevice::into_handle(Arc::new(dev))
499 })
500}
501
502// ---------------------------------------------------------------------------
503// Slice constructor. Returns a child `FsCoreDevice` whose byte 0 maps to
504// `start` of the parent and whose addressable range is `length` bytes.
505// Useful for partition-table walkers that want to hand one partition to
506// a filesystem driver without copying. The slice keeps an `Arc` to the
507// parent, so closing the parent before the slice is fine.
508// ---------------------------------------------------------------------------
509
510/// Read-only slice. Writes via the returned handle return
511/// `FS_CORE_READ_ONLY` regardless of the parent's writability.
512#[unsafe(no_mangle)]
513pub unsafe extern "C" fn fs_core_device_slice_ro(
514 parent: *const FsCoreDevice,
515 start: u64,
516 length: u64,
517) -> *mut FsCoreDevice {
518 if parent.is_null() {
519 set_last_error("parent is null");
520 return ptr::null_mut();
521 }
522 ffi_guard_or(ptr::null_mut(), || unsafe {
523 let parent_arc = (*parent).inner().clone();
524 // OwnedSlice takes Arc<dyn BlockRead>; trait upcast from
525 // BlockDevice -> BlockRead is supported in the pinned toolchain.
526 let parent_read: Arc<dyn crate::block::BlockRead> = parent_arc;
527 let slice = crate::slice::OwnedSlice::new(parent_read, start, length);
528 FsCoreDevice::into_handle(Arc::new(slice))
529 })
530}
531
532/// Read-write slice. Writes are forwarded to the parent at `start +
533/// offset`; writes outside `[0, length)` return `FS_CORE_OUT_OF_BOUNDS`.
534/// If the parent reports `is_writable() == false`, write attempts return
535/// `FS_CORE_READ_ONLY`.
536#[unsafe(no_mangle)]
537pub unsafe extern "C" fn fs_core_device_slice_rw(
538 parent: *const FsCoreDevice,
539 start: u64,
540 length: u64,
541) -> *mut FsCoreDevice {
542 if parent.is_null() {
543 set_last_error("parent is null");
544 return ptr::null_mut();
545 }
546 ffi_guard_or(ptr::null_mut(), || unsafe {
547 let parent_arc = (*parent).inner().clone();
548 let slice = crate::slice::OwnedRwSlice::new(parent_arc, start, length);
549 FsCoreDevice::into_handle(Arc::new(slice))
550 })
551}
552
553// ---------------------------------------------------------------------------
554// Tests — exercise the FFI surface from Rust. The C side is verified by
555// the consumer crates that use these functions through their own headers.
556// ---------------------------------------------------------------------------
557
558#[cfg(test)]
559mod tests {
560 /// THE MESSAGE, not the fact that something panicked.
561 ///
562 /// Both shapes a panic payload takes: `panic!("literal")` gives a
563 /// `&'static str`, and `panic!("{x}")` or an out-of-bounds index
564 /// gives a `String`. A guard that reports neither tells its caller
565 /// only what it already knew.
566 #[test]
567 fn a_caught_panic_reports_what_it_said() {
568 let literal =
569 std::panic::catch_unwind(|| panic!("a literal message")).expect_err("it panicked");
570 assert_eq!(panic_message(&literal), "a literal message");
571
572 let owned = std::panic::catch_unwind(|| {
573 let v: Vec<u8> = Vec::new();
574 let _ = v[3];
575 })
576 .expect_err("it panicked");
577 assert!(
578 panic_message(&owned).contains("index out of bounds"),
579 "the index panic's own words should survive: {}",
580 panic_message(&owned)
581 );
582
583 // Anything else says so rather than pretending to a message.
584 let odd =
585 std::panic::catch_unwind(|| std::panic::panic_any(42u8)).expect_err("it panicked");
586 assert_eq!(panic_message(&odd), "panic in FFI");
587 }
588
589 use super::*;
590 use std::fs::File;
591 use std::io::Write;
592
593 fn tmp_image(bytes: &[u8]) -> String {
594 use std::sync::atomic::{AtomicU32, Ordering};
595 static C: AtomicU32 = AtomicU32::new(0);
596 let n = C.fetch_add(1, Ordering::Relaxed);
597 let p = std::env::temp_dir()
598 .join(format!("fs_core_ffi_{}_{n}.img", std::process::id()))
599 .to_string_lossy()
600 .into_owned();
601 File::create(&p).unwrap().write_all(bytes).unwrap();
602 p
603 }
604
605 #[test]
606 fn open_read_close_round_trip() {
607 let path = tmp_image(b"hello, fs-core ffi");
608 let cpath = CString::new(path.as_str()).unwrap();
609 let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
610 assert!(!h.is_null(), "open failed");
611
612 unsafe {
613 assert_eq!(fs_core_device_size_bytes(h), 18);
614 assert!(!fs_core_device_is_writable(h));
615
616 let mut buf = [0u8; 5];
617 let rc = fs_core_device_read_at(h, 0, buf.as_mut_ptr(), buf.len());
618 assert_eq!(rc, FsCoreErrorCode::Ok);
619 assert_eq!(&buf, b"hello");
620
621 // Write should fail with ReadOnly.
622 let rc = fs_core_device_write_at(h, 0, b"x".as_ptr(), 1);
623 assert_eq!(rc, FsCoreErrorCode::ReadOnly);
624
625 fs_core_device_close(h);
626 }
627 let _ = std::fs::remove_file(&path);
628 }
629
630 #[test]
631 fn null_args_return_null_arg() {
632 let mut buf = [0u8; 4];
633 let rc = unsafe { fs_core_device_read_at(ptr::null(), 0, buf.as_mut_ptr(), buf.len()) };
634 assert_eq!(rc, FsCoreErrorCode::NullArg);
635 let rc = unsafe { fs_core_device_flush(ptr::null()) };
636 assert_eq!(rc, FsCoreErrorCode::NullArg);
637 }
638
639 #[test]
640 fn last_error_populated_on_open_failure() {
641 let cpath = CString::new("/path/that/does/not/exist/we/hope").unwrap();
642 let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
643 assert!(h.is_null());
644 let msg = fs_core_last_error_message();
645 assert!(!msg.is_null());
646 let s = unsafe { std::ffi::CStr::from_ptr(msg).to_string_lossy().into_owned() };
647 assert!(!s.is_empty(), "expected an error message");
648 }
649
650 // ---- callback-backed device tests --------------------------------
651
652 use std::sync::{Arc as StdArc, Mutex as StdMutex};
653
654 struct CbState {
655 data: Vec<u8>,
656 flushed: u32,
657 }
658
659 /// Trampoline that pulls a `*mut CbState` out of the opaque ctx.
660 unsafe extern "C" fn t_read(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int {
661 let st = unsafe { &mut *(ctx as *mut CbState) };
662 let off = offset as usize;
663 if off + len > st.data.len() {
664 return 5; // out of bounds
665 }
666 unsafe {
667 std::ptr::copy_nonoverlapping(st.data.as_ptr().add(off), buf, len);
668 }
669 0
670 }
671 unsafe extern "C" fn t_write(
672 ctx: *mut c_void,
673 offset: u64,
674 buf: *const u8,
675 len: usize,
676 ) -> c_int {
677 let st = unsafe { &mut *(ctx as *mut CbState) };
678 let off = offset as usize;
679 if off + len > st.data.len() {
680 return 5;
681 }
682 unsafe {
683 std::ptr::copy_nonoverlapping(buf, st.data.as_mut_ptr().add(off), len);
684 }
685 0
686 }
687 unsafe extern "C" fn t_flush(ctx: *mut c_void) -> c_int {
688 let st = unsafe { &mut *(ctx as *mut CbState) };
689 st.flushed += 1;
690 0
691 }
692
693 #[test]
694 fn callback_device_round_trip_rw() {
695 let mut st = Box::new(CbState {
696 data: vec![0u8; 32],
697 flushed: 0,
698 });
699 for (i, b) in st.data.iter_mut().enumerate() {
700 *b = i as u8;
701 }
702 let ctx = &mut *st as *mut CbState as *mut c_void;
703
704 let cfg = FsCoreCallbackCfg {
705 read: Some(t_read),
706 write: Some(t_write),
707 flush: Some(t_flush),
708 ctx,
709 size: 32,
710 };
711 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
712 assert!(!h.is_null(), "device_from_callbacks returned NULL");
713
714 unsafe {
715 assert_eq!(fs_core_device_size_bytes(h), 32);
716 assert!(fs_core_device_is_writable(h));
717
718 let mut buf = [0u8; 4];
719 let rc = fs_core_device_read_at(h, 4, buf.as_mut_ptr(), buf.len());
720 assert_eq!(rc, FsCoreErrorCode::Ok);
721 assert_eq!(buf, [4, 5, 6, 7]);
722
723 let payload = [0xDE, 0xAD, 0xBE, 0xEF];
724 let rc = fs_core_device_write_at(h, 8, payload.as_ptr(), payload.len());
725 assert_eq!(rc, FsCoreErrorCode::Ok);
726
727 let rc = fs_core_device_flush(h);
728 assert_eq!(rc, FsCoreErrorCode::Ok);
729
730 let mut readback = [0u8; 4];
731 let rc = fs_core_device_read_at(h, 8, readback.as_mut_ptr(), readback.len());
732 assert_eq!(rc, FsCoreErrorCode::Ok);
733 assert_eq!(readback, payload);
734
735 fs_core_device_close(h);
736 }
737 assert_eq!(st.flushed, 1);
738 assert_eq!(&st.data[8..12], &[0xDE, 0xAD, 0xBE, 0xEF]);
739 }
740
741 #[test]
742 fn callback_device_readonly_when_write_null() {
743 let mut st = Box::new(CbState {
744 data: vec![0xAAu8; 16],
745 flushed: 0,
746 });
747 let ctx = &mut *st as *mut CbState as *mut c_void;
748 let cfg = FsCoreCallbackCfg {
749 read: Some(t_read),
750 write: None,
751 flush: None,
752 ctx,
753 size: 16,
754 };
755 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
756 assert!(!h.is_null());
757 unsafe {
758 assert!(!fs_core_device_is_writable(h));
759 let rc = fs_core_device_write_at(h, 0, [1u8].as_ptr(), 1);
760 assert_eq!(rc, FsCoreErrorCode::ReadOnly);
761 // Flush is a no-op when callback is NULL.
762 assert_eq!(fs_core_device_flush(h), FsCoreErrorCode::Ok);
763 fs_core_device_close(h);
764 }
765 // suppress unused warning
766 let _ = StdArc::new(StdMutex::new(0u8));
767 }
768
769 #[test]
770 fn callback_device_null_cfg_returns_null() {
771 let h = unsafe { fs_core_device_from_callbacks(ptr::null()) };
772 assert!(h.is_null());
773 let msg = fs_core_last_error_message();
774 assert!(!msg.is_null());
775 }
776}
777
778#[cfg(test)]
779mod panic_message_tests {
780 use super::*;
781 use crate::block::{BlockDevice, BlockRead};
782
783 /// A device whose every method panics.
784 ///
785 /// Not a hypothetical: a driver's `size_bytes` computes a geometry
786 /// from on-disk fields, and an arithmetic overflow there panics.
787 /// The FFI boundary is where that has to stop being a panic and
788 /// start being a reportable error.
789 struct Panicking;
790
791 impl BlockRead for Panicking {
792 fn read_at(&self, _offset: u64, _buf: &mut [u8]) -> Result<(), Error> {
793 panic!("read_at exploded")
794 }
795 fn size_bytes(&self) -> u64 {
796 panic!("size_bytes exploded")
797 }
798 }
799 impl BlockDevice for Panicking {
800 fn is_writable(&self) -> bool {
801 panic!("is_writable exploded")
802 }
803 }
804
805 fn handle() -> *mut FsCoreDevice {
806 FsCoreDevice::into_handle(std::sync::Arc::new(Panicking))
807 }
808
809 fn last_error() -> Option<String> {
810 let p = fs_core_last_error_message();
811 if p.is_null() {
812 return None;
813 }
814 Some(
815 unsafe { std::ffi::CStr::from_ptr(p) }
816 .to_string_lossy()
817 .into_owned(),
818 )
819 }
820
821 /// A panic caught at the boundary must leave a message behind.
822 ///
823 /// `fs_core_device_size_bytes` returns 0 on panic — and 0 is also
824 /// what a legitimately empty device returns. Without a message the
825 /// caller cannot tell "this device is empty" from "the driver
826 /// exploded computing its size", which is the whole reason the
827 /// thread-local error slot exists.
828 #[test]
829 fn a_panic_computing_the_size_is_reported_not_just_swallowed() {
830 clear_last_error();
831 let h = handle();
832 let size = unsafe { fs_core_device_size_bytes(h) };
833 assert_eq!(size, 0, "the fallback value is still returned");
834 let msg = last_error().expect("a caught panic must leave a message");
835 assert!(
836 msg.contains("size_bytes exploded"),
837 "the message should carry the panic's own text, got: {msg}"
838 );
839 unsafe { fs_core_device_close(h) };
840 }
841
842 /// Same for the writability probe, whose fallback is `false` — the
843 /// answer a perfectly good read-only device gives.
844 #[test]
845 fn a_panic_probing_writability_is_reported() {
846 clear_last_error();
847 let h = handle();
848 let writable = unsafe { fs_core_device_is_writable(h) };
849 assert!(!writable, "the fallback value is still returned");
850 assert!(
851 last_error().is_some(),
852 "a caught panic must leave a message"
853 );
854 unsafe { fs_core_device_close(h) };
855 }
856
857 /// A call that succeeds must not leave a stale message behind for
858 /// the next one to pick up.
859 #[test]
860 fn a_successful_call_clears_the_previous_error() {
861 let h = handle();
862 let _ = unsafe { fs_core_device_size_bytes(h) };
863 assert!(last_error().is_some(), "setup: an error is recorded");
864 unsafe { fs_core_device_close(h) };
865
866 struct Sixteen;
867 impl BlockRead for Sixteen {
868 fn read_at(&self, _offset: u64, _buf: &mut [u8]) -> Result<(), Error> {
869 Ok(())
870 }
871 fn size_bytes(&self) -> u64 {
872 16
873 }
874 }
875 impl BlockDevice for Sixteen {}
876 let h2 = FsCoreDevice::into_handle(std::sync::Arc::new(Sixteen));
877 assert_eq!(unsafe { fs_core_device_size_bytes(h2) }, 16);
878 assert!(
879 last_error().is_none(),
880 "a call that worked must not leave the previous panic's message in place"
881 );
882 unsafe { fs_core_device_close(h2) };
883 }
884}