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, BlockRead};
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 for a **write** past the end of an RW
62 /// slice, and for a write past the end of a file-backed device --
63 /// both refused up front, nothing transferred. It reaches **reads**
64 /// from sister crates whose container
65 /// declares a virtual size (the `img-*` readers), and from this
66 /// crate's caching / read-only / slice wrappers when they forward
67 /// such a parent's error. A C consumer that only wants to know "the
68 /// read overran the device", and does not control which crate opened
69 /// the handle, should accept this and `FS_CORE_SHORT_READ` alike —
70 /// and should not treat the pair as exhaustive, since a
71 /// callback-backed handle reports its host's refusal as
72 /// `FS_CORE_IO`.
73 OutOfBounds = 4,
74 /// Driver-specific error — message in the thread-local last-error.
75 Custom = 5,
76 /// One of the input pointers was null.
77 NullArg = 6,
78 /// `catch_unwind` caught a panic crossing the FFI boundary.
79 Panic = 7,
80 /// Reserved. Never returned.
81 ///
82 /// It was meant for a path that is not valid UTF-8, but the one
83 /// function that meets that case — `fs_core_file_open` — returns a
84 /// POINTER, not a code, so it reports the failure as NULL plus a
85 /// message and cannot return this. No other entry point takes a
86 /// path.
87 ///
88 /// Kept rather than removed because the numbering is published in
89 /// `include/fs_core.h` and a consumer may already switch on 8;
90 /// renumbering the codes after it would be an ABI break for a
91 /// tidiness gain. A future path-taking function that returns a code
92 /// should use this rather than invent another.
93 BadString = 8,
94}
95
96impl FsCoreErrorCode {
97 fn from_error(e: &Error) -> Self {
98 match e {
99 Error::Io(_) => FsCoreErrorCode::Io,
100 Error::ShortRead { .. } => FsCoreErrorCode::ShortRead,
101 Error::ReadOnly => FsCoreErrorCode::ReadOnly,
102 Error::OutOfBounds { .. } => FsCoreErrorCode::OutOfBounds,
103 Error::Custom(_) => FsCoreErrorCode::Custom,
104 }
105 }
106}
107
108// ---------------------------------------------------------------------------
109// Thread-local last-error — errno-style detail companion.
110// ---------------------------------------------------------------------------
111
112thread_local! {
113 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
114}
115
116/// Stash a message in the thread-local, replacing any previous one. Public
117/// to sister crates so they can populate it for their own error paths.
118pub fn set_last_error(message: impl Into<String>) {
119 let s = message.into();
120 let cs = CString::new(s.replace('\0', "?")).expect("contains no NUL after replace");
121 LAST_ERROR.with(|slot| {
122 *slot.borrow_mut() = Some(cs);
123 });
124}
125
126fn clear_last_error() {
127 LAST_ERROR.with(|slot| {
128 *slot.borrow_mut() = None;
129 });
130}
131
132/// Return a pointer to the calling thread's most recent error message, or
133/// NULL if there is none. The pointer is owned by the framework and remains
134/// valid until the next FFI call on this thread.
135#[unsafe(no_mangle)]
136pub extern "C" fn fs_core_last_error_message() -> *const c_char {
137 LAST_ERROR.with(|slot| {
138 slot.borrow()
139 .as_ref()
140 .map(|cs| cs.as_ptr())
141 .unwrap_or(ptr::null())
142 })
143}
144
145/// Helper for sister crates: run `body`, catch panics, map errors to codes,
146/// stash the message in the thread-local. Returns the error code.
147pub fn ffi_guard<F>(body: F) -> FsCoreErrorCode
148where
149 F: FnOnce() -> Result<(), Error>,
150{
151 clear_last_error();
152 match std::panic::catch_unwind(AssertUnwindSafe(body)) {
153 Ok(Ok(())) => FsCoreErrorCode::Ok,
154 Ok(Err(e)) => {
155 let code = FsCoreErrorCode::from_error(&e);
156 set_last_error(e.to_string());
157 code
158 }
159 Err(panic) => {
160 set_last_error(panic_message(&panic));
161 FsCoreErrorCode::Panic
162 }
163 }
164}
165
166/// Run `body`, catching a panic and returning `fail` instead — and
167/// recording the panic's message where a caller can read it.
168///
169/// # Why the message matters more than the fallback
170///
171/// Every fallback value here is also a legitimate answer. Zero is what
172/// an empty device reports for its size; `false` is what a read-only
173/// device reports for writability; a null pointer is what a failed open
174/// returns. So a caller that only sees the fallback cannot tell an
175/// ordinary answer from a driver that exploded computing it.
176///
177/// [`fs_core_last_error_message`] is what separates them, and a guard
178/// that returns the fallback without setting it throws away the only
179/// evidence there was.
180///
181/// # Why this is separate from [`ffi_guard`]
182///
183/// `ffi_guard` returns an [`FsCoreErrorCode`] and takes a body that
184/// returns `Result<(), Error>`. That fits an entry point whose whole
185/// answer is a status code, and fits nothing else — which is why the
186/// eight entry points in this file that return a size, a flag or a
187/// pointer each wrote `catch_unwind(AssertUnwindSafe(…)).unwrap_or(…)`
188/// by hand instead, sixty lines below the helper.
189///
190/// Sister crates did the same: eleven of them re-roll one of these two
191/// shapes rather than share either.
192///
193/// The error slot is cleared on entry, like [`ffi_guard`]: a call that
194/// succeeds must not leave the previous call's message in place for a
195/// caller to read and attribute to this one.
196///
197/// `AssertUnwindSafe` is used deliberately. The bodies here touch a
198/// handle the caller owns and a thread-local error slot; a panic can
199/// leave neither in a state another call can observe as inconsistent,
200/// because the handle is not read again on this path and the slot is
201/// overwritten whole.
202pub fn ffi_guard_or<T, F>(fail: T, body: F) -> T
203where
204 F: FnOnce() -> T,
205{
206 clear_last_error();
207 match std::panic::catch_unwind(AssertUnwindSafe(body)) {
208 Ok(value) => value,
209 Err(panic) => {
210 set_last_error(panic_message(&panic));
211 fail
212 }
213 }
214}
215
216/// Run a cleanup body — a `close`, a `free` — catching a panic so it
217/// cannot unwind into C, and **leaving the error slot untouched**.
218///
219/// # Why a third guard rather than one of the two above
220///
221/// The other two own the slot, because they have something to say
222/// through it: a status code to explain, or a fallback value that needs
223/// separating from a legitimate answer. A cleanup function that returns
224/// `void` has neither. Running it through [`ffi_guard_or`] therefore
225/// cleared a slot it could never fill, and destroyed the diagnostic in
226/// the ordinary C shape where the free comes before the log — see
227/// [`fs_core_device_close`].
228///
229/// So the rule this restores is that the slot's lifecycle belongs to the
230/// call that can report through it, rather than to whichever helper
231/// happened to wrap the body.
232///
233/// # A panic here is caught and NOT reported, deliberately
234///
235/// There is nowhere to report it. The return type is `()`, so a caller
236/// learns nothing from the call itself, and the slot is the one thing it
237/// is about to read for the *earlier* failure that sent it down the
238/// cleanup path. Overwriting that with a message about the free would
239/// destroy the very diagnostic this exists to preserve, and it is the
240/// earlier error a caller is looking for. Swallowing the panic is the
241/// lesser loss of the two, and it is a choice rather than an oversight.
242///
243/// `AssertUnwindSafe` for the same reason as [`ffi_guard_or`]: the body
244/// touches a handle the caller owns and is not read again on this path.
245pub fn ffi_guard_cleanup<F>(body: F)
246where
247 F: FnOnce(),
248{
249 let _ = std::panic::catch_unwind(AssertUnwindSafe(body));
250}
251
252/// What a caught panic actually said.
253///
254/// PUBLIC BECAUSE THE OTHER ELEVEN CRATES NEED IT. Each of them guards
255/// its own C entry points with `catch_unwind` and, having no way to
256/// reach this, reports the panic as `"panic in <function>"` -- the name
257/// of the function that was running, which the caller already knew, in
258/// place of the message, which is the only part it did not. An index
259/// out of bounds, a slice out of range, an `expect` with a sentence in
260/// it: all of it was thrown away at the boundary.
261///
262/// The guards themselves are NOT shareable, and that is why this is
263/// what moved rather than [`ffi_guard`]. Each crate's guard records the
264/// message into that crate's own thread-local, which is what its own C
265/// callers read; a guard from here would record into this crate's, and
266/// every panic message would land in a slot nobody reads.
267pub fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
268 if let Some(s) = panic.downcast_ref::<&'static str>() {
269 return (*s).to_string();
270 }
271 if let Some(s) = panic.downcast_ref::<String>() {
272 return s.clone();
273 }
274 "panic in FFI".to_string()
275}
276
277// ---------------------------------------------------------------------------
278// Device handle — opaque to C callers, shared across crates.
279// ---------------------------------------------------------------------------
280
281/// Opaque handle wrapping an `Arc<dyn BlockDevice>`. Allocated by sister
282/// crates' constructors and freed via [`fs_core_device_close`].
283pub struct FsCoreDevice {
284 inner: Arc<dyn BlockDevice>,
285}
286
287impl FsCoreDevice {
288 /// Internal constructor — sister crates use this to wrap their own
289 /// device types (Qcow2Reader, FileDevice, OwnedSlice, etc.) into the
290 /// shared handle type. Returns a `Box::into_raw` pointer ready to hand
291 /// across the FFI boundary.
292 pub fn into_handle(inner: Arc<dyn BlockDevice>) -> *mut FsCoreDevice {
293 Box::into_raw(Box::new(FsCoreDevice { inner }))
294 }
295
296 /// Borrow the inner device. `Arc::clone` it if you want shared
297 /// ownership — e.g. when handing the device to a slice adapter while
298 /// keeping the original handle alive.
299 pub fn inner(&self) -> &Arc<dyn BlockDevice> {
300 &self.inner
301 }
302}
303
304/// Free a device handle. Safe to call with NULL (no-op).
305///
306/// # THIS PRESERVES THE LAST ERROR MESSAGE
307///
308/// It returns `void`, so it can never report anything through the error
309/// slot — and it used to clear the slot anyway, because it went through
310/// [`ffi_guard_or`]. That destroyed the diagnostic in the ordinary C
311/// cleanup shape, where the close comes before the log:
312///
313/// ```c
314/// if (fs_core_device_read_at(h, off, buf, len) != FS_CORE_OK) goto fail;
315/// ...
316/// fail:
317/// fs_core_device_close(h);
318/// log("%s", fs_core_last_error_message()); /* was NULL */
319/// ```
320///
321/// A caller may now close before reading the message. Nothing on this
322/// path reads or writes the slot.
323#[unsafe(no_mangle)]
324pub unsafe extern "C" fn fs_core_device_close(handle: *mut FsCoreDevice) {
325 if handle.is_null() {
326 return;
327 }
328 ffi_guard_cleanup(|| unsafe {
329 drop(Box::from_raw(handle));
330 });
331}
332
333/// Total device size in bytes. Returns 0 if `handle` is NULL.
334#[unsafe(no_mangle)]
335pub unsafe extern "C" fn fs_core_device_size_bytes(handle: *const FsCoreDevice) -> u64 {
336 if handle.is_null() {
337 // 0 is also what an empty device reports, so the message is the
338 // only thing that separates the two -- and leaving the previous
339 // call's message here explained this answer with something that
340 // happened somewhere else.
341 set_last_error("fs_core_device_size_bytes: handle is null");
342 return 0;
343 }
344 ffi_guard_or(0, || unsafe { (*handle).inner.size_bytes() })
345}
346
347/// True if `write_at` is likely to succeed. Returns false on NULL.
348#[unsafe(no_mangle)]
349pub unsafe extern "C" fn fs_core_device_is_writable(handle: *const FsCoreDevice) -> bool {
350 if handle.is_null() {
351 // `false` is also what a perfectly good read-only device reports.
352 set_last_error("fs_core_device_is_writable: handle is null");
353 return false;
354 }
355 ffi_guard_or(false, || unsafe { (*handle).inner.is_writable() })
356}
357
358/// Read exactly `len` bytes from `offset` into `buf`. `buf` must be at
359/// least `len` bytes. Returns an `FsCoreErrorCode`.
360#[unsafe(no_mangle)]
361pub unsafe extern "C" fn fs_core_device_read_at(
362 handle: *const FsCoreDevice,
363 offset: u64,
364 buf: *mut u8,
365 len: usize,
366) -> FsCoreErrorCode {
367 // A null buffer is refused whatever the length. `from_raw_parts_mut`
368 // requires a non-null, aligned pointer even for a zero-length slice,
369 // so `(NULL, 0)` was undefined behaviour rather than the no-op it
370 // looks like -- in a crate that otherwise denies
371 // `unsafe_op_in_unsafe_fn`.
372 if handle.is_null() {
373 set_last_error("fs_core_device_read_at: handle is null");
374 return FsCoreErrorCode::NullArg;
375 }
376 if buf.is_null() {
377 set_last_error("fs_core_device_read_at: buf is null");
378 return FsCoreErrorCode::NullArg;
379 }
380 ffi_guard(|| {
381 let slice_buf = unsafe { slice::from_raw_parts_mut(buf, len) };
382 unsafe { (*handle).inner.read_at(offset, slice_buf) }
383 })
384}
385
386/// Write exactly `len` bytes from `buf` to `offset`. Returns `ReadOnly`
387/// for read-only devices.
388#[unsafe(no_mangle)]
389pub unsafe extern "C" fn fs_core_device_write_at(
390 handle: *const FsCoreDevice,
391 offset: u64,
392 buf: *const u8,
393 len: usize,
394) -> FsCoreErrorCode {
395 // Null is refused whatever the length; see `fs_core_device_read_at`.
396 if handle.is_null() {
397 set_last_error("fs_core_device_write_at: handle is null");
398 return FsCoreErrorCode::NullArg;
399 }
400 if buf.is_null() {
401 set_last_error("fs_core_device_write_at: buf is null");
402 return FsCoreErrorCode::NullArg;
403 }
404 ffi_guard(|| {
405 let slice_buf = unsafe { slice::from_raw_parts(buf, len) };
406 unsafe { (*handle).inner.write_at(offset, slice_buf) }
407 })
408}
409
410/// Flush pending writes to stable storage.
411#[unsafe(no_mangle)]
412pub unsafe extern "C" fn fs_core_device_flush(handle: *const FsCoreDevice) -> FsCoreErrorCode {
413 if handle.is_null() {
414 set_last_error("fs_core_device_flush: handle is null");
415 return FsCoreErrorCode::NullArg;
416 }
417 ffi_guard(|| unsafe { (*handle).inner.flush() })
418}
419
420// ---------------------------------------------------------------------------
421// Convenience: open a regular file as a device. Saves callers the trouble
422// of building a Rust crate just to wrap `FileDevice`.
423// ---------------------------------------------------------------------------
424
425/// Open `path` (NUL-terminated UTF-8) as a `FileDevice` and return a
426/// handle. Pass `writable=true` for RW. On failure returns NULL and the
427/// thread-local last-error has detail.
428#[cfg(any(unix, windows))]
429#[unsafe(no_mangle)]
430pub unsafe extern "C" fn fs_core_file_open(
431 path: *const c_char,
432 writable: bool,
433) -> *mut FsCoreDevice {
434 if path.is_null() {
435 set_last_error("path is null");
436 return ptr::null_mut();
437 }
438 ffi_guard_or(ptr::null_mut(), || {
439 let cstr = unsafe { std::ffi::CStr::from_ptr(path) };
440 let s = match cstr.to_str() {
441 Ok(s) => s,
442 Err(_) => {
443 set_last_error("path is not valid UTF-8");
444 return ptr::null_mut();
445 }
446 };
447 let dev = if writable {
448 crate::file_device::FileDevice::open_rw(s)
449 } else {
450 crate::file_device::FileDevice::open(s)
451 };
452 match dev {
453 Ok(d) => FsCoreDevice::into_handle(Arc::new(d)),
454 Err(e) => {
455 set_last_error(e.to_string());
456 ptr::null_mut()
457 }
458 }
459 })
460}
461
462// ---------------------------------------------------------------------------
463// Callback-backed device. Used when the caller already owns the underlying
464// resource (FSKit FSBlockDeviceResource, Go file handle, C-side fd) and
465// wants to expose it as an `FsCoreDevice` so it can be stacked under a
466// container reader (qcow2, vhd, ...) before reaching a filesystem driver.
467// ---------------------------------------------------------------------------
468
469/// Read callback. Returns 0 on success, non-zero (errno-like) on failure.
470/// Must fully fill `len` bytes — short reads are treated as I/O errors.
471pub type FsCoreReadCb =
472 Option<unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int>;
473
474/// Write callback. NULL → device is read-only.
475pub type FsCoreWriteCb = Option<
476 unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *const u8, len: usize) -> c_int,
477>;
478
479/// Flush/fsync callback. NULL → flush is a no-op.
480pub type FsCoreFlushCb = Option<unsafe extern "C" fn(ctx: *mut c_void) -> c_int>;
481
482/// Configuration passed to [`fs_core_device_from_callbacks`].
483#[repr(C)]
484pub struct FsCoreCallbackCfg {
485 pub read: FsCoreReadCb,
486 pub write: FsCoreWriteCb,
487 pub flush: FsCoreFlushCb,
488 pub ctx: *mut c_void,
489 pub size: u64,
490}
491
492/// Turn a callback's non-zero return into an `io::Error`.
493fn cb_io_err(rc: c_int, op: &str) -> io::Error {
494 io::Error::other(format!("callback {op} returned {rc}"))
495}
496
497/// The host callback contract, in one place: **zero is success**.
498///
499/// All three adapters below wrapped a call in the same four lines —
500/// invoke, compare against zero, `Ok(())` or `cb_io_err`. Three copies
501/// of a convention is three chances to write `rc != 0` where the others
502/// write `rc == 0`, and a caller would see reads succeed while writes
503/// reported failure on the very same device.
504///
505/// `op` names the operation in the error, which is the only thing the
506/// three genuinely differ in.
507fn cb_result(rc: c_int, op: &'static str) -> io::Result<()> {
508 if rc == 0 {
509 Ok(())
510 } else {
511 Err(cb_io_err(rc, op))
512 }
513}
514
515/// Build an [`FsCoreDevice`] backed by host-provided callbacks. Returns NULL
516/// on failure (config null, read callback null, etc.) and stashes detail in
517/// the thread-local last-error.
518///
519/// `cfg.ctx` is opaque to fs-core; it is passed back verbatim to every
520/// callback invocation. The caller is responsible for ensuring it remains
521/// valid until [`fs_core_device_close`] is called on the returned handle.
522#[unsafe(no_mangle)]
523pub unsafe extern "C" fn fs_core_device_from_callbacks(
524 cfg: *const FsCoreCallbackCfg,
525) -> *mut FsCoreDevice {
526 if cfg.is_null() {
527 set_last_error("cfg is null");
528 return ptr::null_mut();
529 }
530 ffi_guard_or(ptr::null_mut(), || unsafe {
531 let cfg = &*cfg;
532 let read_fn = match cfg.read {
533 Some(f) => f,
534 None => {
535 set_last_error("cfg.read is null");
536 return ptr::null_mut();
537 }
538 };
539 let write_fn = cfg.write;
540 let flush_fn = cfg.flush;
541 // `*mut c_void` is `!Send + !Sync` by default, and `unsafe impl
542 // Send` on a newtype does not propagate cleanly through closure
543 // auto-traits. Round-tripping the pointer through `usize` gives
544 // something that is `Copy + Send + Sync`, and the callback
545 // contract already puts the host on the hook for using `ctx`
546 // safely across threads.
547 let ctx_addr = cfg.ctx as usize;
548 let size = cfg.size;
549
550 let read_cb: crate::callback_device::ReadCb = Box::new(move |off, buf| {
551 let ctx = ctx_addr as *mut c_void;
552 cb_result(read_fn(ctx, off, buf.as_mut_ptr(), buf.len()), "read")
553 });
554 let write_cb: Option<crate::callback_device::WriteCb> = write_fn.map(|f| {
555 Box::new(move |off, buf: &[u8]| {
556 let ctx = ctx_addr as *mut c_void;
557 cb_result(f(ctx, off, buf.as_ptr(), buf.len()), "write")
558 }) as crate::callback_device::WriteCb
559 });
560 let flush_cb: Option<crate::callback_device::FlushCb> = flush_fn.map(|f| {
561 Box::new(move || {
562 let ctx = ctx_addr as *mut c_void;
563 cb_result(f(ctx), "flush")
564 }) as crate::callback_device::FlushCb
565 });
566
567 let dev = CallbackDevice {
568 size,
569 read: read_cb,
570 write: write_cb,
571 flush: flush_cb,
572 };
573 FsCoreDevice::into_handle(Arc::new(dev))
574 })
575}
576
577// ---------------------------------------------------------------------------
578// Slice constructor. Returns a child `FsCoreDevice` whose byte 0 maps to
579// `start` of the parent and whose addressable range is `length` bytes.
580// Useful for partition-table walkers that want to hand one partition to
581// a filesystem driver without copying. The slice keeps an `Arc` to the
582// parent, so closing the parent before the slice is fine.
583// ---------------------------------------------------------------------------
584
585/// Read-only slice. Writes via the returned handle return
586/// `FS_CORE_READ_ONLY` regardless of the parent's writability.
587///
588/// `length` is clamped to what the parent can back, and a `start` at or
589/// past the parent's end returns NULL with a message — see
590/// [`crate::slice::window_on_parent`]. Read `fs_core_device_size_bytes`
591/// on the returned handle rather than assuming it is `length`.
592#[unsafe(no_mangle)]
593pub unsafe extern "C" fn fs_core_device_slice_ro(
594 parent: *const FsCoreDevice,
595 start: u64,
596 length: u64,
597) -> *mut FsCoreDevice {
598 if parent.is_null() {
599 set_last_error("parent is null");
600 return ptr::null_mut();
601 }
602 ffi_guard_or(ptr::null_mut(), || unsafe {
603 let parent_arc = (*parent).inner().clone();
604 let Some(length) = slice_window(parent_arc.size_bytes(), start, length, "slice_ro") else {
605 return ptr::null_mut();
606 };
607 // OwnedSlice takes Arc<dyn BlockRead>; trait upcast from
608 // BlockDevice -> BlockRead is supported in the pinned toolchain.
609 let parent_read: Arc<dyn crate::block::BlockRead> = parent_arc;
610 let slice = crate::slice::OwnedSlice::new(parent_read, start, length);
611 FsCoreDevice::into_handle(Arc::new(slice))
612 })
613}
614
615/// Read-write slice. Writes are forwarded to the parent at `start +
616/// offset`; writes outside `[0, length)` return `FS_CORE_OUT_OF_BOUNDS`.
617/// If the parent reports `is_writable() == false`, write attempts return
618/// `FS_CORE_READ_ONLY`.
619///
620/// `length` is clamped to what the parent can back, and a `start` at or
621/// past the parent's end returns NULL with a message — see
622/// [`crate::slice::window_on_parent`]. Read `fs_core_device_size_bytes`
623/// on the returned handle rather than assuming it is `length`.
624#[unsafe(no_mangle)]
625pub unsafe extern "C" fn fs_core_device_slice_rw(
626 parent: *const FsCoreDevice,
627 start: u64,
628 length: u64,
629) -> *mut FsCoreDevice {
630 if parent.is_null() {
631 set_last_error("parent is null");
632 return ptr::null_mut();
633 }
634 ffi_guard_or(ptr::null_mut(), || unsafe {
635 let parent_arc = (*parent).inner().clone();
636 let Some(length) = slice_window(parent_arc.size_bytes(), start, length, "slice_rw") else {
637 return ptr::null_mut();
638 };
639 let slice = crate::slice::OwnedRwSlice::new(parent_arc, start, length);
640 FsCoreDevice::into_handle(Arc::new(slice))
641 })
642}
643
644/// The slice window both C constructors take, or `None` with the error
645/// slot already set.
646///
647/// The clamp itself lives in [`crate::slice::window_on_parent`] and is
648/// applied again inside the slice constructors, so calling it here is
649/// not the check — it is how the C ABI learns that there was nothing to
650/// slice, which is the one outcome a `*mut` return can express and an
651/// infallible Rust constructor cannot. A zero-byte handle would be
652/// technically honest and useless to debug: the mount that follows fails
653/// on its superblock read with no hint that the window was the problem.
654fn slice_window(parent_size: u64, start: u64, length: u64, what: &str) -> Option<u64> {
655 match crate::slice::window_on_parent(parent_size, start, length) {
656 Some(clamped) => Some(clamped),
657 None => {
658 set_last_error(format!(
659 "fs_core_device_{what}: start {start} is at or past the end of the \
660 parent device ({parent_size} bytes), so there is nothing to slice"
661 ));
662 None
663 }
664 }
665}
666
667// ---------------------------------------------------------------------------
668// Tests — exercise the FFI surface from Rust. The C side is verified by
669// the consumer crates that use these functions through their own headers.
670// ---------------------------------------------------------------------------
671
672#[cfg(test)]
673mod tests {
674 /// THE MESSAGE, not the fact that something panicked.
675 ///
676 /// Both shapes a panic payload takes: `panic!("literal")` gives a
677 /// `&'static str`, and `panic!("{x}")` or an out-of-bounds index
678 /// gives a `String`. A guard that reports neither tells its caller
679 /// only what it already knew.
680 #[test]
681 fn a_caught_panic_reports_what_it_said() {
682 let literal =
683 std::panic::catch_unwind(|| panic!("a literal message")).expect_err("it panicked");
684 assert_eq!(panic_message(&literal), "a literal message");
685
686 let owned = std::panic::catch_unwind(|| {
687 let v: Vec<u8> = Vec::new();
688 let _ = v[3];
689 })
690 .expect_err("it panicked");
691 assert!(
692 panic_message(&owned).contains("index out of bounds"),
693 "the index panic's own words should survive: {}",
694 panic_message(&owned)
695 );
696
697 // Anything else says so rather than pretending to a message.
698 let odd =
699 std::panic::catch_unwind(|| std::panic::panic_any(42u8)).expect_err("it panicked");
700 assert_eq!(panic_message(&odd), "panic in FFI");
701 }
702
703 use super::*;
704 use std::fs::File;
705 use std::io::Write;
706
707 fn tmp_image(bytes: &[u8]) -> String {
708 use std::sync::atomic::{AtomicU32, Ordering};
709 static C: AtomicU32 = AtomicU32::new(0);
710 let n = C.fetch_add(1, Ordering::Relaxed);
711 let p = std::env::temp_dir()
712 .join(format!("fs_core_ffi_{}_{n}.img", std::process::id()))
713 .to_string_lossy()
714 .into_owned();
715 File::create(&p).unwrap().write_all(bytes).unwrap();
716 p
717 }
718
719 #[test]
720 fn open_read_close_round_trip() {
721 let path = tmp_image(b"hello, fs-core ffi");
722 let cpath = CString::new(path.as_str()).unwrap();
723 let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
724 assert!(!h.is_null(), "open failed");
725
726 unsafe {
727 assert_eq!(fs_core_device_size_bytes(h), 18);
728 assert!(!fs_core_device_is_writable(h));
729
730 let mut buf = [0u8; 5];
731 let rc = fs_core_device_read_at(h, 0, buf.as_mut_ptr(), buf.len());
732 assert_eq!(rc, FsCoreErrorCode::Ok);
733 assert_eq!(&buf, b"hello");
734
735 // Write should fail with ReadOnly.
736 let rc = fs_core_device_write_at(h, 0, b"x".as_ptr(), 1);
737 assert_eq!(rc, FsCoreErrorCode::ReadOnly);
738
739 fs_core_device_close(h);
740 }
741 let _ = std::fs::remove_file(&path);
742 }
743
744 #[test]
745 fn null_args_return_null_arg() {
746 let mut buf = [0u8; 4];
747 let rc = unsafe { fs_core_device_read_at(ptr::null(), 0, buf.as_mut_ptr(), buf.len()) };
748 assert_eq!(rc, FsCoreErrorCode::NullArg);
749 let rc = unsafe { fs_core_device_flush(ptr::null()) };
750 assert_eq!(rc, FsCoreErrorCode::NullArg);
751 }
752
753 #[test]
754 fn last_error_populated_on_open_failure() {
755 let cpath = CString::new("/path/that/does/not/exist/we/hope").unwrap();
756 let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
757 assert!(h.is_null());
758 let msg = fs_core_last_error_message();
759 assert!(!msg.is_null());
760 let s = unsafe { std::ffi::CStr::from_ptr(msg).to_string_lossy().into_owned() };
761 assert!(!s.is_empty(), "expected an error message");
762 }
763
764 // ---- callback-backed device tests --------------------------------
765
766 use std::sync::{Arc as StdArc, Mutex as StdMutex};
767
768 struct CbState {
769 data: Vec<u8>,
770 flushed: u32,
771 }
772
773 /// Trampoline that pulls a `*mut CbState` out of the opaque ctx.
774 unsafe extern "C" fn t_read(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int {
775 let st = unsafe { &mut *(ctx as *mut CbState) };
776 // `off + len` was computed BEFORE the bounds check that exists
777 // to refuse a past-end range, so a wild offset panicked here
778 // instead of returning 5 -- the same pair of lines, and the
779 // same defect, as the doubles in `test_device`.
780 let Ok((off, _)) = crate::test_device::range_within(st.data.len(), offset, len) else {
781 return 5; // out of bounds
782 };
783 unsafe {
784 std::ptr::copy_nonoverlapping(st.data.as_ptr().add(off), buf, len);
785 }
786 0
787 }
788 unsafe extern "C" fn t_write(
789 ctx: *mut c_void,
790 offset: u64,
791 buf: *const u8,
792 len: usize,
793 ) -> c_int {
794 let st = unsafe { &mut *(ctx as *mut CbState) };
795 let Ok((off, _)) = crate::test_device::range_within(st.data.len(), offset, len) else {
796 return 5;
797 };
798 unsafe {
799 std::ptr::copy_nonoverlapping(buf, st.data.as_mut_ptr().add(off), len);
800 }
801 0
802 }
803 unsafe extern "C" fn t_flush(ctx: *mut c_void) -> c_int {
804 let st = unsafe { &mut *(ctx as *mut CbState) };
805 st.flushed += 1;
806 0
807 }
808
809 #[test]
810 fn callback_device_round_trip_rw() {
811 let mut st = Box::new(CbState {
812 data: vec![0u8; 32],
813 flushed: 0,
814 });
815 for (i, b) in st.data.iter_mut().enumerate() {
816 *b = i as u8;
817 }
818 let ctx = &mut *st as *mut CbState as *mut c_void;
819
820 let cfg = FsCoreCallbackCfg {
821 read: Some(t_read),
822 write: Some(t_write),
823 flush: Some(t_flush),
824 ctx,
825 size: 32,
826 };
827 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
828 assert!(!h.is_null(), "device_from_callbacks returned NULL");
829
830 unsafe {
831 assert_eq!(fs_core_device_size_bytes(h), 32);
832 assert!(fs_core_device_is_writable(h));
833
834 let mut buf = [0u8; 4];
835 let rc = fs_core_device_read_at(h, 4, buf.as_mut_ptr(), buf.len());
836 assert_eq!(rc, FsCoreErrorCode::Ok);
837 assert_eq!(buf, [4, 5, 6, 7]);
838
839 let payload = [0xDE, 0xAD, 0xBE, 0xEF];
840 let rc = fs_core_device_write_at(h, 8, payload.as_ptr(), payload.len());
841 assert_eq!(rc, FsCoreErrorCode::Ok);
842
843 let rc = fs_core_device_flush(h);
844 assert_eq!(rc, FsCoreErrorCode::Ok);
845
846 let mut readback = [0u8; 4];
847 let rc = fs_core_device_read_at(h, 8, readback.as_mut_ptr(), readback.len());
848 assert_eq!(rc, FsCoreErrorCode::Ok);
849 assert_eq!(readback, payload);
850
851 fs_core_device_close(h);
852 }
853 assert_eq!(st.flushed, 1);
854 assert_eq!(&st.data[8..12], &[0xDE, 0xAD, 0xBE, 0xEF]);
855 }
856
857 #[test]
858 fn callback_device_readonly_when_write_null() {
859 let mut st = Box::new(CbState {
860 data: vec![0xAAu8; 16],
861 flushed: 0,
862 });
863 let ctx = &mut *st as *mut CbState as *mut c_void;
864 let cfg = FsCoreCallbackCfg {
865 read: Some(t_read),
866 write: None,
867 flush: None,
868 ctx,
869 size: 16,
870 };
871 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
872 assert!(!h.is_null());
873 unsafe {
874 assert!(!fs_core_device_is_writable(h));
875 let rc = fs_core_device_write_at(h, 0, [1u8].as_ptr(), 1);
876 assert_eq!(rc, FsCoreErrorCode::ReadOnly);
877 // Flush is a no-op when callback is NULL.
878 assert_eq!(fs_core_device_flush(h), FsCoreErrorCode::Ok);
879 fs_core_device_close(h);
880 }
881 // suppress unused warning
882 let _ = StdArc::new(StdMutex::new(0u8));
883 }
884
885 /// The last error as text, or `None`. Read directly rather than
886 /// through `panic_message_tests::last_error`, which is a different
887 /// module.
888 fn cb_last_error() -> Option<String> {
889 let p = fs_core_last_error_message();
890 if p.is_null() {
891 return None;
892 }
893 Some(
894 unsafe { std::ffi::CStr::from_ptr(p) }
895 .to_string_lossy()
896 .into_owned(),
897 )
898 }
899
900 /// THE THIRD COPY OF THE BOUNDS RULE, AND THE ONE NOTHING HELD.
901 ///
902 /// `tests/common/mod.rs` states the standard this crate works to:
903 /// the rule is written twice, so both copies carry a test that pins
904 /// it. #84 fixed a THIRD copy -- these two trampolines -- and gave
905 /// it none. Reverting both to the pre-fix arithmetic left
906 /// `cargo test --locked --lib` at 92 passed, 0 failed, and `--lib`
907 /// is the complete check: they are `#[cfg(test)]` items in the lib
908 /// target, so no integration test can reach them.
909 ///
910 /// # What the reverted arithmetic actually does, measured
911 ///
912 /// Not what I first wrote here. `ffi_guard` wraps the call in
913 /// `catch_unwind`, so the obvious expectation is that an overflow
914 /// panic returns `FsCoreErrorCode::Panic`. It does not: a
915 /// trampoline is `extern "C"`, panicking out of one is
916 /// non-unwinding, and the process aborts before any code is
917 /// returned. With `t_read` reverted, `cargo test --locked --lib`
918 /// gives
919 ///
920 /// ```text
921 /// thread caused non-unwinding panic. aborting.
922 /// process didn't exit successfully: ... (signal: 6, SIGABRT)
923 /// EXIT=101, and NO `... FAILED` line for any test
924 /// ```
925 ///
926 /// So the control produces a crash rather than a failure, which is
927 /// why the exit status is the thing to read: counting `test
928 /// result:` lines cannot see an aborted binary.
929 ///
930 /// # Why the assertion is still about the MESSAGE
931 ///
932 /// The abort makes the revert impossible to miss, but it is not
933 /// what these assertions are for. `ffi_guard` turns any refusal
934 /// into a non-`Ok` code, so "not Ok" alone would also be satisfied
935 /// by the wrapper refusing before the trampoline was ever called --
936 /// a test that passes without exercising the copy it exists to
937 /// pin. `callback read returned 5` is the trampoline's own
938 /// out-of-bounds path and nothing else produces it. `Panic` is
939 /// excluded too, for the case where a future edit makes the
940 /// unwinding reachable.
941 #[test]
942 fn a_callback_read_at_a_wild_offset_is_refused_rather_than_panicking() {
943 let mut st = Box::new(CbState {
944 data: vec![0x5Au8; 32],
945 flushed: 0,
946 });
947 let ctx = &mut *st as *mut CbState as *mut c_void;
948 let cfg = FsCoreCallbackCfg {
949 read: Some(t_read),
950 write: Some(t_write),
951 flush: Some(t_flush),
952 ctx,
953 size: 32,
954 };
955 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
956 assert!(!h.is_null());
957
958 // Three shapes, and the first two are the ones the arithmetic
959 // used to get wrong: an offset that is itself past every
960 // addressable byte, and an offset whose sum with the length
961 // wraps. The third is the ordinary past-end read, which the
962 // pre-fix code also handled -- it is here so a guard that
963 // refused everything would not look like a pass.
964 for (what, offset, want) in [
965 ("the very top of the address space", u64::MAX, 8usize),
966 ("an offset whose sum with len wraps", u64::MAX - 2, 8usize),
967 ("an ordinary past-end read", 64u64, 8usize),
968 ] {
969 let mut buf = [0u8; 8];
970 let rc = unsafe { fs_core_device_read_at(h, offset, buf.as_mut_ptr(), want) };
971 assert_ne!(
972 rc,
973 FsCoreErrorCode::Panic,
974 "{what}: the trampoline panicked instead of refusing; \
975 last error was {:?}",
976 cb_last_error()
977 );
978 assert_ne!(rc, FsCoreErrorCode::Ok, "{what}: must not succeed");
979 let msg = cb_last_error().unwrap_or_default();
980 assert!(
981 msg.contains("callback read returned 5"),
982 "{what}: the refusal must come from the trampoline's own \
983 out-of-bounds path, not from a panic caught by ffi_guard. \
984 last error was {msg:?}"
985 );
986 }
987 unsafe { fs_core_device_close(h) };
988 }
989
990 /// The write half. `t_write` is the copy most easily forgotten, and
991 /// the one that would corrupt rather than merely panic.
992 #[test]
993 fn a_callback_write_at_a_wild_offset_is_refused_rather_than_panicking() {
994 let mut st = Box::new(CbState {
995 data: vec![0x5Au8; 32],
996 flushed: 0,
997 });
998 let ctx = &mut *st as *mut CbState as *mut c_void;
999 let cfg = FsCoreCallbackCfg {
1000 read: Some(t_read),
1001 write: Some(t_write),
1002 flush: Some(t_flush),
1003 ctx,
1004 size: 32,
1005 };
1006 let h = unsafe { fs_core_device_from_callbacks(&cfg) };
1007 assert!(!h.is_null());
1008
1009 let payload = [0xEEu8; 8];
1010 for (what, offset) in [
1011 ("the very top of the address space", u64::MAX),
1012 ("an offset whose sum with len wraps", u64::MAX - 2),
1013 ("an ordinary past-end write", 64u64),
1014 ] {
1015 let rc = unsafe { fs_core_device_write_at(h, offset, payload.as_ptr(), payload.len()) };
1016 assert_ne!(
1017 rc,
1018 FsCoreErrorCode::Panic,
1019 "{what}: the trampoline panicked instead of refusing; \
1020 last error was {:?}",
1021 cb_last_error()
1022 );
1023 assert_ne!(rc, FsCoreErrorCode::Ok, "{what}: must not succeed");
1024 let msg = cb_last_error().unwrap_or_default();
1025 assert!(
1026 msg.contains("callback write returned 5"),
1027 "{what}: the refusal must come from the trampoline's own \
1028 out-of-bounds path, not from a panic caught by ffi_guard. \
1029 last error was {msg:?}"
1030 );
1031 }
1032 unsafe { fs_core_device_close(h) };
1033 // Nothing was written anywhere: a refused write must not have
1034 // narrowed a wild offset into a plausible one on the way out.
1035 assert!(
1036 st.data.iter().all(|b| *b == 0x5A),
1037 "a refused write modified the backing buffer"
1038 );
1039 }
1040
1041 #[test]
1042 fn callback_device_null_cfg_returns_null() {
1043 let h = unsafe { fs_core_device_from_callbacks(ptr::null()) };
1044 assert!(h.is_null());
1045 let msg = fs_core_last_error_message();
1046 assert!(!msg.is_null());
1047 }
1048}
1049
1050#[cfg(test)]
1051mod panic_message_tests {
1052 use super::*;
1053 use crate::block::{BlockDevice, BlockRead};
1054
1055 /// A device whose every method panics.
1056 ///
1057 /// Not a hypothetical: a driver's `size_bytes` computes a geometry
1058 /// from on-disk fields, and an arithmetic overflow there panics.
1059 /// The FFI boundary is where that has to stop being a panic and
1060 /// start being a reportable error.
1061 struct Panicking;
1062
1063 impl BlockRead for Panicking {
1064 fn read_at(&self, _offset: u64, _buf: &mut [u8]) -> Result<(), Error> {
1065 panic!("read_at exploded")
1066 }
1067 fn size_bytes(&self) -> u64 {
1068 panic!("size_bytes exploded")
1069 }
1070 }
1071 impl BlockDevice for Panicking {
1072 fn is_writable(&self) -> bool {
1073 panic!("is_writable exploded")
1074 }
1075 }
1076
1077 fn handle() -> *mut FsCoreDevice {
1078 FsCoreDevice::into_handle(std::sync::Arc::new(Panicking))
1079 }
1080
1081 fn last_error() -> Option<String> {
1082 let p = fs_core_last_error_message();
1083 if p.is_null() {
1084 return None;
1085 }
1086 Some(
1087 unsafe { std::ffi::CStr::from_ptr(p) }
1088 .to_string_lossy()
1089 .into_owned(),
1090 )
1091 }
1092
1093 /// A panic caught at the boundary must leave a message behind.
1094 ///
1095 /// `fs_core_device_size_bytes` returns 0 on panic — and 0 is also
1096 /// what a legitimately empty device returns. Without a message the
1097 /// caller cannot tell "this device is empty" from "the driver
1098 /// exploded computing its size", which is the whole reason the
1099 /// thread-local error slot exists.
1100 #[test]
1101 fn a_panic_computing_the_size_is_reported_not_just_swallowed() {
1102 clear_last_error();
1103 let h = handle();
1104 let size = unsafe { fs_core_device_size_bytes(h) };
1105 assert_eq!(size, 0, "the fallback value is still returned");
1106 let msg = last_error().expect("a caught panic must leave a message");
1107 assert!(
1108 msg.contains("size_bytes exploded"),
1109 "the message should carry the panic's own text, got: {msg}"
1110 );
1111 unsafe { fs_core_device_close(h) };
1112 }
1113
1114 /// Same for the writability probe, whose fallback is `false` — the
1115 /// answer a perfectly good read-only device gives.
1116 #[test]
1117 fn a_panic_probing_writability_is_reported() {
1118 clear_last_error();
1119 let h = handle();
1120 let writable = unsafe { fs_core_device_is_writable(h) };
1121 assert!(!writable, "the fallback value is still returned");
1122 assert!(
1123 last_error().is_some(),
1124 "a caught panic must leave a message"
1125 );
1126 unsafe { fs_core_device_close(h) };
1127 }
1128
1129 /// A call that succeeds must not leave a stale message behind for
1130 /// the next one to pick up.
1131 #[test]
1132 fn a_successful_call_clears_the_previous_error() {
1133 let h = handle();
1134 let _ = unsafe { fs_core_device_size_bytes(h) };
1135 assert!(last_error().is_some(), "setup: an error is recorded");
1136 unsafe { fs_core_device_close(h) };
1137
1138 struct Sixteen;
1139 impl BlockRead for Sixteen {
1140 fn read_at(&self, _offset: u64, _buf: &mut [u8]) -> Result<(), Error> {
1141 Ok(())
1142 }
1143 fn size_bytes(&self) -> u64 {
1144 16
1145 }
1146 }
1147 impl BlockDevice for Sixteen {}
1148 let h2 = FsCoreDevice::into_handle(std::sync::Arc::new(Sixteen));
1149 assert_eq!(unsafe { fs_core_device_size_bytes(h2) }, 16);
1150 assert!(
1151 last_error().is_none(),
1152 "a call that worked must not leave the previous panic's message in place"
1153 );
1154 unsafe { fs_core_device_close(h2) };
1155 }
1156
1157 // ---------------------------------------------------------------------
1158 // A null argument explains itself, rather than inheriting whatever the
1159 // previous call left behind.
1160 //
1161 // Every null check returned ABOVE the guard, and the guard is the only
1162 // thing that touches the error slot -- so a null-argument call left the
1163 // previous call's message readable and a caller attributed something
1164 // that happened elsewhere to this call.
1165 //
1166 // Worst for `size_bytes` and `is_writable`, whose fallbacks are both
1167 // legitimate answers: the caller got `0` or `false` AND a confident
1168 // explanation of it belonging to a different operation. That is not
1169 // "the evidence was thrown away", which this file already argues
1170 // against -- it is evidence about something else, substituted.
1171 //
1172 // The slot is seeded with `set_last_error` rather than by provoking a
1173 // real failure. That is exactly what a failed call does to it --
1174 // `ffi_guard` sets it the same way -- and it makes "not the earlier
1175 // message" an exact comparison rather than a fuzzy one.
1176 // ---------------------------------------------------------------------
1177
1178 /// Distinctive enough that finding it in a message is unambiguous.
1179 const SEEDED: &str = "SEEDED-earlier-failure-belonging-to-another-call";
1180
1181 /// Assert the slot names a null argument and has lost the seed.
1182 fn assert_named_null(msg: Option<String>, expect: &str) {
1183 let msg = msg.expect("a null argument must leave a message of its own");
1184 assert!(
1185 msg.contains(expect),
1186 "the message must name the null argument ({expect}), got: {msg}"
1187 );
1188 assert!(
1189 !msg.contains(SEEDED),
1190 "the previous call's message must not survive to explain this one, got: {msg}"
1191 );
1192 }
1193
1194 #[test]
1195 fn a_null_handle_to_size_bytes_names_the_argument() {
1196 set_last_error(SEEDED);
1197 let size = unsafe { fs_core_device_size_bytes(std::ptr::null()) };
1198 assert_eq!(size, 0, "the fallback value is still returned");
1199 assert_named_null(last_error(), "fs_core_device_size_bytes: handle is null");
1200 }
1201
1202 #[test]
1203 fn a_null_handle_to_is_writable_names_the_argument() {
1204 set_last_error(SEEDED);
1205 let writable = unsafe { fs_core_device_is_writable(std::ptr::null()) };
1206 assert!(!writable, "the fallback value is still returned");
1207 assert_named_null(last_error(), "fs_core_device_is_writable: handle is null");
1208 }
1209
1210 #[test]
1211 fn a_null_handle_to_flush_names_the_argument() {
1212 set_last_error(SEEDED);
1213 let rc = unsafe { fs_core_device_flush(std::ptr::null()) };
1214 assert_eq!(rc, FsCoreErrorCode::NullArg, "the code is still NullArg");
1215 assert_named_null(last_error(), "fs_core_device_flush: handle is null");
1216 }
1217
1218 /// `read_at` has two null arguments, and the message says which.
1219 ///
1220 /// A code of `NullArg` is honest but says nothing about *what* was
1221 /// null, and `fs_core.h` promises a human-readable message for every
1222 /// fallible call. Two arms, so neither can pass on the other's back.
1223 #[test]
1224 fn a_null_argument_to_read_at_names_which_one() {
1225 let mut buf = [0u8; 8];
1226
1227 set_last_error(SEEDED);
1228 let rc = unsafe { fs_core_device_read_at(std::ptr::null(), 0, buf.as_mut_ptr(), 8) };
1229 assert_eq!(rc, FsCoreErrorCode::NullArg);
1230 assert_named_null(last_error(), "fs_core_device_read_at: handle is null");
1231
1232 // A real handle, a null buffer: the other arm.
1233 set_last_error(SEEDED);
1234 let h = handle();
1235 let rc = unsafe { fs_core_device_read_at(h, 0, std::ptr::null_mut(), 8) };
1236 assert_eq!(rc, FsCoreErrorCode::NullArg);
1237 assert_named_null(last_error(), "fs_core_device_read_at: buf is null");
1238 unsafe { fs_core_device_close(h) };
1239 }
1240
1241 /// Same for `write_at`.
1242 #[test]
1243 fn a_null_argument_to_write_at_names_which_one() {
1244 let buf = [0u8; 8];
1245
1246 set_last_error(SEEDED);
1247 let rc = unsafe { fs_core_device_write_at(std::ptr::null(), 0, buf.as_ptr(), 8) };
1248 assert_eq!(rc, FsCoreErrorCode::NullArg);
1249 assert_named_null(last_error(), "fs_core_device_write_at: handle is null");
1250
1251 set_last_error(SEEDED);
1252 let h = handle();
1253 let rc = unsafe { fs_core_device_write_at(h, 0, std::ptr::null(), 8) };
1254 assert_eq!(rc, FsCoreErrorCode::NullArg);
1255 assert_named_null(last_error(), "fs_core_device_write_at: buf is null");
1256 unsafe { fs_core_device_close(h) };
1257 }
1258
1259 /// CLOSE MUST NOT DESTROY THE MESSAGE A CALLER IS ABOUT TO READ.
1260 ///
1261 /// `close` returns `void`, so it can never fill the error slot — and it
1262 /// used to clear it anyway, by going through the guard that owns the
1263 /// slot for calls that *can* report. That breaks the ordinary C cleanup
1264 /// shape, where the free comes before the log:
1265 ///
1266 /// ```c
1267 /// fail:
1268 /// fs_core_device_close(h);
1269 /// log("%s", fs_core_last_error_message()); /* was NULL */
1270 /// ```
1271 #[test]
1272 fn close_preserves_the_message_a_caller_is_about_to_read() {
1273 set_last_error(SEEDED);
1274 let h = handle();
1275 unsafe { fs_core_device_close(h) };
1276 let msg = last_error().expect("close must not destroy the last error");
1277 assert!(
1278 msg.contains(SEEDED),
1279 "the diagnostic a caller closes before reading must survive, got: {msg}"
1280 );
1281 }
1282
1283 /// The control for the one above: closing NULL is a no-op and always
1284 /// preserved the slot, because it returns before any guard. It passes
1285 /// before and after the fix, and it is here so that
1286 /// `close_preserves_...` failing points at the guard rather than at
1287 /// something about handles.
1288 #[test]
1289 fn closing_null_also_preserves_the_message() {
1290 set_last_error(SEEDED);
1291 unsafe { fs_core_device_close(std::ptr::null_mut()) };
1292 let msg = last_error().expect("a no-op must not clear the slot");
1293 assert!(msg.contains(SEEDED));
1294 }
1295}
1296
1297// ---------------------------------------------------------------------------
1298// The published error numbering, pinned on both sides.
1299//
1300// `FsCoreErrorCode` and the `FsCoreErrorCode` enum in
1301// `include/fs_core.h` are two hand-written copies of one ABI, of which
1302// the header says "Stable: do not renumber." Nothing compiles the
1303// header, and every other test in this file compares codes symbolically
1304// — `assert_eq!(rc, FsCoreErrorCode::ShortRead)` — which is invariant
1305// under precisely the change that breaks the ABI: a variant inserted
1306// into the middle of one copy, or added to one copy and not the other.
1307//
1308// Two checks, because neither sees what the other does. Comparing the
1309// two files as text catches a variant that reached only one of them.
1310// Pinning the discriminants against literals catches a renumbering
1311// applied tidily to both, which is the change the header forbids and
1312// the one the text comparison would call agreement.
1313// ---------------------------------------------------------------------------
1314
1315#[cfg(test)]
1316mod error_code_abi_tests {
1317 use super::FsCoreErrorCode;
1318
1319 /// The C spelling of a Rust variant name: `Ok` is `FS_CORE_OK`,
1320 /// `OutOfBounds` is `FS_CORE_OUT_OF_BOUNDS`.
1321 fn c_name(rust_name: &str) -> String {
1322 let mut out = String::from("FS_CORE_");
1323 for (i, ch) in rust_name.chars().enumerate() {
1324 if i != 0 && ch.is_ascii_uppercase() {
1325 out.push('_');
1326 }
1327 out.extend(ch.to_uppercase());
1328 }
1329 out
1330 }
1331
1332 /// The body of an enum block: everything between an opening marker
1333 /// that must occur exactly once — so a second enum added to either
1334 /// file cannot quietly redirect the parse — and the first brace at
1335 /// column 0 after it, which must be followed by `closes_with`.
1336 fn enum_body<'a>(src: &'a str, opens_with: &str, closes_with: &str) -> &'a str {
1337 assert_eq!(
1338 src.matches(opens_with).count(),
1339 1,
1340 "{opens_with:?} must appear exactly once or this parses the wrong enum"
1341 );
1342 let start = src.find(opens_with).unwrap() + opens_with.len();
1343 let end = start
1344 + src[start..]
1345 .find("\n}")
1346 .expect("the enum block is closed by a brace at column 0");
1347 let after = &src[end + 2..];
1348 assert!(
1349 after.starts_with(closes_with),
1350 "the closing brace should be followed by {closes_with:?}, not {:?} — \
1351 the parse stopped somewhere other than the end of the enum",
1352 &after[..closes_with.len().min(after.len())]
1353 );
1354 &src[start..end]
1355 }
1356
1357 /// The error codes as `src/ffi.rs` declares them, in declaration
1358 /// order, spelled the way C spells them.
1359 ///
1360 /// Reads this very file rather than listing the variants, so a
1361 /// variant added to the enum cannot be absent from what is compared
1362 /// against the header — that absence being the drift under guard,
1363 /// and a hand-maintained list here would share it.
1364 ///
1365 /// It refuses to guess. A line in the enum body that is neither
1366 /// blank, a comment, an attribute nor `Name = <integer>,` fails the
1367 /// test, because a parser that silently matches nothing agrees with
1368 /// every header.
1369 fn codes_declared_in_rust() -> Vec<(String, i32)> {
1370 // NORMALISED FIRST. A Windows checkout has CRLF, so every marker
1371 // below would carry a `\r` the source does not, and the
1372 // uniqueness assertion fails rather than parsing the wrong enum
1373 // -- which is the guard working, but it fails a correct file.
1374 // The caller owns the normalised copy because `enum_body`
1375 // borrows from it.
1376 let src = include_str!("ffi.rs").replace("\r\n", "\n");
1377 let body = enum_body(
1378 &src,
1379 "pub enum FsCoreErrorCode {\n",
1380 "\n\nimpl FsCoreErrorCode {",
1381 );
1382 assert!(
1383 !body.contains('{'),
1384 "the extracted Rust enum body should hold no nested braces: {body:?}"
1385 );
1386 body.lines()
1387 .map(str::trim)
1388 .filter(|l| !(l.is_empty() || l.starts_with("//") || l.starts_with("#[")))
1389 .map(|l| {
1390 let (name, value) = l
1391 .strip_suffix(',')
1392 .and_then(|entry| entry.split_once('='))
1393 .unwrap_or_else(|| panic!("unparsed line in the Rust enum body: {l:?}"));
1394 let value = value.trim().parse().unwrap_or_else(|e| {
1395 panic!("{name:?} has no literal discriminant ({e}): {l:?}")
1396 });
1397 (c_name(name.trim()), value)
1398 })
1399 .collect()
1400 }
1401
1402 /// The same list as `include/fs_core.h` declares it, with the same
1403 /// refusal to guess: an entry it cannot read fails the test.
1404 fn codes_declared_in_c() -> Vec<(String, i32)> {
1405 let src = include_str!("../include/fs_core.h").replace("\r\n", "\n");
1406 let body = enum_body(&src, "typedef enum {\n", " FsCoreErrorCode;");
1407
1408 // A C comment spans lines and sits between entries, so it goes
1409 // before the split on commas rather than after it.
1410 let mut stripped = String::new();
1411 let mut rest = body;
1412 while let Some(open) = rest.find("/*") {
1413 stripped.push_str(&rest[..open]);
1414 let tail = &rest[open + 2..];
1415 let close = tail
1416 .find("*/")
1417 .expect("an unterminated comment in the header's enum");
1418 rest = &tail[close + 2..];
1419 }
1420 stripped.push_str(rest);
1421
1422 stripped
1423 .split(',')
1424 .map(str::trim)
1425 .filter(|entry| !entry.is_empty())
1426 .map(|entry| {
1427 let (name, value) = entry
1428 .split_once('=')
1429 .unwrap_or_else(|| panic!("unparsed entry in the C enum body: {entry:?}"));
1430 let value = value
1431 .trim()
1432 .parse()
1433 .unwrap_or_else(|e| panic!("{name:?} has no literal value ({e}): {entry:?}"));
1434 (name.trim().to_owned(), value)
1435 })
1436 .collect()
1437 }
1438
1439 /// Name for name and number for number, in the same order.
1440 ///
1441 /// This is the check a ninth code added to one file and not the
1442 /// other fails. The crate has no other defence against it: the
1443 /// build succeeds and every symbolic comparison still passes.
1444 #[test]
1445 fn the_header_and_the_rust_enum_publish_the_same_error_codes() {
1446 let rust = codes_declared_in_rust();
1447 let c = codes_declared_in_c();
1448
1449 // Nine codes are published and a published code is never
1450 // withdrawn, so a parse returning fewer read less than the
1451 // enum — whatever it then agreed with.
1452 assert!(
1453 rust.len() >= 9 && c.len() >= 9,
1454 "both parses should reach every published code, got {} from Rust and {} from C",
1455 rust.len(),
1456 c.len()
1457 );
1458 assert_eq!(rust, c, "the two copies of the error ABI have drifted");
1459 }
1460
1461 /// What the compiler assigns, against the numbers the header
1462 /// publishes as unchangeable.
1463 ///
1464 /// The text comparison above cannot see a renumbering applied to
1465 /// both files, and `BadString` is carried as a deliberately dead
1466 /// variant precisely so that 8 keeps its meaning for a consumer
1467 /// already switching on it.
1468 #[test]
1469 fn the_error_codes_still_have_the_numbers_they_were_published_with() {
1470 assert_eq!(FsCoreErrorCode::Ok as i32, 0);
1471 assert_eq!(FsCoreErrorCode::Io as i32, 1);
1472 assert_eq!(FsCoreErrorCode::ShortRead as i32, 2);
1473 assert_eq!(FsCoreErrorCode::ReadOnly as i32, 3);
1474 assert_eq!(FsCoreErrorCode::OutOfBounds as i32, 4);
1475 assert_eq!(FsCoreErrorCode::Custom as i32, 5);
1476 assert_eq!(FsCoreErrorCode::NullArg as i32, 6);
1477 assert_eq!(FsCoreErrorCode::Panic as i32, 7);
1478 assert_eq!(FsCoreErrorCode::BadString as i32, 8);
1479 }
1480}