use crate::{
DirEntry, Error as BashError, FileSystem, FileSystemExt, FileType, Metadata,
Result as BashResult, async_trait,
};
use std::ffi::c_void;
use std::future::Future;
use std::io::{self, Error as IoError, ErrorKind};
use std::path::{Path, PathBuf};
use std::ptr;
use std::slice;
use std::str;
use std::sync::Arc;
use std::sync::mpsc::sync_channel;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::runtime::{Builder, Runtime};
pub const BASHKIT_FS_ABI_VERSION_V1: u32 = 1;
pub type BashkitFsAbiStatus = u32;
pub const BASHKIT_FS_ABI_STATUS_OK: BashkitFsAbiStatus = 0;
pub const BASHKIT_FS_ABI_STATUS_ERR: BashkitFsAbiStatus = 1;
pub type ReadFileFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedBytes,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type WriteFileFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
content: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type MkdirFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
recursive: bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type RemoveFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
recursive: bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type StatFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiMetadata,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type ReadDirFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedDirEntries,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type ExistsFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type RenameFn = unsafe extern "C" fn(
instance: *const c_void,
from: BashkitFsAbiStrRef,
to: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type SymlinkFn = unsafe extern "C" fn(
instance: *const c_void,
target: BashkitFsAbiStrRef,
link: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type ReadLinkFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedBytes,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type ChmodFn = unsafe extern "C" fn(
instance: *const c_void,
path: BashkitFsAbiStrRef,
mode: u32,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus;
pub type FreeBytesFn = unsafe extern "C" fn(instance: *const c_void, bytes: BashkitFsAbiOwnedBytes);
pub type FreeDirEntriesFn =
unsafe extern "C" fn(instance: *const c_void, entries: BashkitFsAbiOwnedDirEntries);
pub type RetainFn = unsafe extern "C" fn(instance: *const c_void);
pub type ReleaseFn = unsafe extern "C" fn(instance: *const c_void);
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiStrRef {
pub ptr: *const u8,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiOwnedBytes {
pub ptr: *mut u8,
pub len: usize,
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BashkitFsAbiErrorKind {
#[default]
Other = 0,
NotFound = 1,
AlreadyExists = 2,
PermissionDenied = 3,
InvalidInput = 4,
IsADirectory = 5,
NotADirectory = 6,
DirectoryNotEmpty = 7,
Unsupported = 8,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiError {
pub kind: BashkitFsAbiErrorKind,
pub message: BashkitFsAbiOwnedBytes,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Default)]
pub enum BashkitFsAbiFileType {
#[default]
File = 0,
Directory = 1,
Symlink = 2,
Fifo = 3,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiMetadata {
pub file_type: BashkitFsAbiFileType,
pub _reserved: [u8; 7],
pub size: u64,
pub mode: u32,
pub modified_secs: i64,
pub modified_nanos: u32,
pub created_secs: i64,
pub created_nanos: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiDirEntry {
pub name: BashkitFsAbiOwnedBytes,
pub metadata: BashkitFsAbiMetadata,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct BashkitFsAbiOwnedDirEntries {
pub ptr: *mut BashkitFsAbiDirEntry,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct BashkitFsAbiVTableV1 {
pub read_file: Option<ReadFileFn>,
pub write_file: Option<WriteFileFn>,
pub append_file: Option<WriteFileFn>,
pub mkdir: Option<MkdirFn>,
pub remove: Option<RemoveFn>,
pub stat: Option<StatFn>,
pub read_dir: Option<ReadDirFn>,
pub exists: Option<ExistsFn>,
pub rename: Option<RenameFn>,
pub copy: Option<RenameFn>,
pub symlink: Option<SymlinkFn>,
pub read_link: Option<ReadLinkFn>,
pub chmod: Option<ChmodFn>,
pub free_bytes: Option<FreeBytesFn>,
pub free_dir_entries: Option<FreeDirEntriesFn>,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct BashkitFsAbiHandleV1 {
pub abi_version: u32,
pub _reserved: u32,
pub instance: *const c_void,
pub retain: Option<RetainFn>,
pub release: Option<ReleaseFn>,
pub vtable: *const BashkitFsAbiVTableV1,
}
#[repr(C)]
pub struct BashkitFsAbiOwnedHandleV1 {
pub handle: BashkitFsAbiHandleV1,
}
impl BashkitFsAbiOwnedHandleV1 {
pub fn as_handle(&self) -> &BashkitFsAbiHandleV1 {
&self.handle
}
}
impl Drop for BashkitFsAbiOwnedHandleV1 {
fn drop(&mut self) {
if let Some(release) = self.handle.release {
unsafe {
release(self.handle.instance);
}
}
}
}
unsafe impl Send for BashkitFsAbiHandleV1 {}
unsafe impl Sync for BashkitFsAbiHandleV1 {}
unsafe impl Send for BashkitFsAbiOwnedHandleV1 {}
unsafe impl Sync for BashkitFsAbiOwnedHandleV1 {}
struct ExportState {
fs: Arc<dyn FileSystem>,
rt: Runtime,
}
impl ExportState {
fn run<T, Fut>(&self, fut: Fut) -> io::Result<T>
where
T: Send + 'static,
Fut: Future<Output = BashResult<T>> + Send + 'static,
{
let (tx, rx) = sync_channel(1);
self.rt.handle().spawn(async move {
let _ = tx.send(fut.await.map_err(bash_error_to_io));
});
rx.recv().map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"filesystem interop runtime stopped before operation completed",
)
})?
}
}
pub fn export_filesystem(fs: Arc<dyn FileSystem>) -> io::Result<BashkitFsAbiOwnedHandleV1> {
let rt = Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()?;
let state = Arc::new(ExportState { fs, rt });
let instance = Arc::into_raw(state).cast::<c_void>();
Ok(BashkitFsAbiOwnedHandleV1 {
handle: BashkitFsAbiHandleV1 {
abi_version: BASHKIT_FS_ABI_VERSION_V1,
_reserved: 0,
instance,
retain: Some(retain_export_state),
release: Some(release_export_state),
vtable: &EXPORT_VTABLE,
},
})
}
pub fn import_filesystem(handle: &BashkitFsAbiHandleV1) -> io::Result<Arc<dyn FileSystem>> {
Ok(Arc::new(ImportedFileSystem::from_handle(handle)?) as Arc<dyn FileSystem>)
}
pub fn import_owned_filesystem(
handle: &BashkitFsAbiOwnedHandleV1,
) -> io::Result<Arc<dyn FileSystem>> {
import_filesystem(handle.as_handle())
}
fn missing_abi_field(name: &str) -> IoError {
IoError::new(
ErrorKind::InvalidData,
format!("filesystem ABI field '{name}' must not be null"),
)
}
fn require_abi_fn<T>(value: Option<T>, name: &str) -> io::Result<T> {
value.ok_or_else(|| missing_abi_field(name))
}
fn validate_vtable(vtable: &BashkitFsAbiVTableV1) -> io::Result<()> {
require_abi_fn(vtable.read_file, "vtable.read_file")?;
require_abi_fn(vtable.write_file, "vtable.write_file")?;
require_abi_fn(vtable.append_file, "vtable.append_file")?;
require_abi_fn(vtable.mkdir, "vtable.mkdir")?;
require_abi_fn(vtable.remove, "vtable.remove")?;
require_abi_fn(vtable.stat, "vtable.stat")?;
require_abi_fn(vtable.read_dir, "vtable.read_dir")?;
require_abi_fn(vtable.exists, "vtable.exists")?;
require_abi_fn(vtable.rename, "vtable.rename")?;
require_abi_fn(vtable.copy, "vtable.copy")?;
require_abi_fn(vtable.symlink, "vtable.symlink")?;
require_abi_fn(vtable.read_link, "vtable.read_link")?;
require_abi_fn(vtable.chmod, "vtable.chmod")?;
require_abi_fn(vtable.free_bytes, "vtable.free_bytes")?;
require_abi_fn(vtable.free_dir_entries, "vtable.free_dir_entries")?;
Ok(())
}
fn validate_handle(handle: &BashkitFsAbiHandleV1) -> io::Result<&BashkitFsAbiVTableV1> {
if handle.abi_version != BASHKIT_FS_ABI_VERSION_V1 {
return Err(IoError::new(
ErrorKind::InvalidData,
format!("unsupported filesystem ABI version: {}", handle.abi_version),
));
}
if handle.instance.is_null() {
return Err(IoError::new(
ErrorKind::InvalidData,
"filesystem handle instance must not be null",
));
}
require_abi_fn(handle.retain, "retain")?;
require_abi_fn(handle.release, "release")?;
let vtable = unsafe {
handle
.vtable
.as_ref()
.ok_or_else(|| missing_abi_field("vtable"))?
};
validate_vtable(vtable)?;
Ok(vtable)
}
pub struct ImportedFileSystem {
handle: BashkitFsAbiHandleV1,
}
impl ImportedFileSystem {
pub fn from_handle(handle: &BashkitFsAbiHandleV1) -> io::Result<Self> {
validate_handle(handle)?;
let retain = handle.retain.expect("filesystem ABI handle was validated");
unsafe {
retain(handle.instance);
}
Ok(Self { handle: *handle })
}
fn vtable(&self) -> &BashkitFsAbiVTableV1 {
unsafe { &*self.handle.vtable }
}
fn call(
&self,
f: impl FnOnce(&BashkitFsAbiVTableV1, *mut BashkitFsAbiError) -> BashkitFsAbiStatus,
) -> BashResult<()> {
let mut err = BashkitFsAbiError::default();
let status = f(self.vtable(), &mut err);
if status == BASHKIT_FS_ABI_STATUS_OK {
return Ok(());
}
Err(abi_error_to_io(self.vtable(), self.handle.instance, err).into())
}
fn take_bytes(&self, bytes: BashkitFsAbiOwnedBytes) -> io::Result<Vec<u8>> {
let result = owned_bytes_to_vec(bytes);
let free_bytes = self
.vtable()
.free_bytes
.expect("filesystem ABI vtable was validated");
unsafe {
free_bytes(self.handle.instance, bytes);
}
result
}
fn take_dir_entries(&self, entries: BashkitFsAbiOwnedDirEntries) -> BashResult<Vec<DirEntry>> {
let result = owned_dir_entries_to_vec(entries);
let free_dir_entries = self
.vtable()
.free_dir_entries
.expect("filesystem ABI vtable was validated");
unsafe {
free_dir_entries(self.handle.instance, entries);
}
result.map_err(Into::into)
}
}
unsafe impl Send for ImportedFileSystem {}
unsafe impl Sync for ImportedFileSystem {}
impl Drop for ImportedFileSystem {
fn drop(&mut self) {
if let Some(release) = self.handle.release {
unsafe {
release(self.handle.instance);
}
}
}
}
#[async_trait]
impl FileSystemExt for ImportedFileSystem {}
#[async_trait]
impl FileSystem for ImportedFileSystem {
async fn read_file(&self, path: &Path) -> BashResult<Vec<u8>> {
let path = str_ref_from_path(path)?;
let mut out = BashkitFsAbiOwnedBytes::default();
self.call(|vtable, err| unsafe {
(vtable
.read_file
.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
&mut out,
err,
)
})?;
self.take_bytes(out).map_err(Into::into)
}
async fn write_file(&self, path: &Path, content: &[u8]) -> BashResult<()> {
let path = str_ref_from_path(path)?;
let content = BashkitFsAbiStrRef {
ptr: content.as_ptr(),
len: content.len(),
};
self.call(|vtable, err| unsafe {
(vtable
.write_file
.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
content,
err,
)
})
}
async fn append_file(&self, path: &Path, content: &[u8]) -> BashResult<()> {
let path = str_ref_from_path(path)?;
let content = BashkitFsAbiStrRef {
ptr: content.as_ptr(),
len: content.len(),
};
self.call(|vtable, err| unsafe {
(vtable
.append_file
.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
content,
err,
)
})
}
async fn mkdir(&self, path: &Path, recursive: bool) -> BashResult<()> {
let path = str_ref_from_path(path)?;
self.call(|vtable, err| unsafe {
(vtable.mkdir.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
recursive,
err,
)
})
}
async fn remove(&self, path: &Path, recursive: bool) -> BashResult<()> {
let path = str_ref_from_path(path)?;
self.call(|vtable, err| unsafe {
(vtable.remove.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
recursive,
err,
)
})
}
async fn stat(&self, path: &Path) -> BashResult<Metadata> {
let path = str_ref_from_path(path)?;
let mut out = BashkitFsAbiMetadata::default();
self.call(|vtable, err| unsafe {
(vtable.stat.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
&mut out,
err,
)
})?;
abi_metadata_to_metadata(out).map_err(Into::into)
}
async fn read_dir(&self, path: &Path) -> BashResult<Vec<DirEntry>> {
let path = str_ref_from_path(path)?;
let mut out = BashkitFsAbiOwnedDirEntries::default();
self.call(|vtable, err| unsafe {
(vtable
.read_dir
.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
&mut out,
err,
)
})?;
self.take_dir_entries(out)
}
async fn exists(&self, path: &Path) -> BashResult<bool> {
let path = str_ref_from_path(path)?;
let mut out = false;
self.call(|vtable, err| unsafe {
(vtable.exists.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
&mut out,
err,
)
})?;
Ok(out)
}
async fn rename(&self, from: &Path, to: &Path) -> BashResult<()> {
let from = str_ref_from_path(from)?;
let to = str_ref_from_path(to)?;
self.call(|vtable, err| unsafe {
(vtable.rename.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
from,
to,
err,
)
})
}
async fn copy(&self, from: &Path, to: &Path) -> BashResult<()> {
let from = str_ref_from_path(from)?;
let to = str_ref_from_path(to)?;
self.call(|vtable, err| unsafe {
(vtable.copy.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
from,
to,
err,
)
})
}
async fn symlink(&self, target: &Path, link: &Path) -> BashResult<()> {
let target = str_ref_from_path(target)?;
let link = str_ref_from_path(link)?;
self.call(|vtable, err| unsafe {
(vtable.symlink.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
target,
link,
err,
)
})
}
async fn read_link(&self, path: &Path) -> BashResult<PathBuf> {
let path = str_ref_from_path(path)?;
let mut out = BashkitFsAbiOwnedBytes::default();
self.call(|vtable, err| unsafe {
(vtable
.read_link
.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
&mut out,
err,
)
})?;
let bytes = self.take_bytes(out)?;
let text = String::from_utf8(bytes)
.map_err(|e| IoError::new(ErrorKind::InvalidData, e.to_string()))?;
Ok(PathBuf::from(text))
}
async fn chmod(&self, path: &Path, mode: u32) -> BashResult<()> {
let path = str_ref_from_path(path)?;
self.call(|vtable, err| unsafe {
(vtable.chmod.expect("filesystem ABI vtable was validated"))(
self.handle.instance,
path,
mode,
err,
)
})
}
}
fn str_ref_from_path(path: &Path) -> io::Result<BashkitFsAbiStrRef> {
let text = path
.to_str()
.ok_or_else(|| IoError::new(ErrorKind::InvalidInput, "path must be valid UTF-8"))?;
Ok(BashkitFsAbiStrRef {
ptr: text.as_ptr(),
len: text.len(),
})
}
fn str_ref_to_bytes(bytes: BashkitFsAbiStrRef) -> io::Result<&'static [u8]> {
if bytes.len == 0 {
return Ok(&[]);
}
if bytes.ptr.is_null() {
return Err(IoError::new(
ErrorKind::InvalidInput,
"byte pointer must not be null when len > 0",
));
}
unsafe { Ok(slice::from_raw_parts(bytes.ptr, bytes.len)) }
}
fn owned_bytes_to_vec(bytes: BashkitFsAbiOwnedBytes) -> io::Result<Vec<u8>> {
if bytes.len == 0 {
return Ok(Vec::new());
}
if bytes.ptr.is_null() {
return Err(IoError::new(
ErrorKind::InvalidData,
"owned byte pointer must not be null when len > 0",
));
}
unsafe { Ok(slice::from_raw_parts(bytes.ptr.cast_const(), bytes.len).to_vec()) }
}
fn path_buf_from_abi(path: BashkitFsAbiStrRef) -> io::Result<PathBuf> {
let text = str::from_utf8(str_ref_to_bytes(path)?)
.map_err(|e| IoError::new(ErrorKind::InvalidInput, e.to_string()))?;
Ok(PathBuf::from(text))
}
fn abi_metadata_to_metadata(meta: BashkitFsAbiMetadata) -> io::Result<Metadata> {
Ok(Metadata {
file_type: match meta.file_type {
BashkitFsAbiFileType::File => FileType::File,
BashkitFsAbiFileType::Directory => FileType::Directory,
BashkitFsAbiFileType::Symlink => FileType::Symlink,
BashkitFsAbiFileType::Fifo => FileType::Fifo,
},
size: meta.size,
mode: meta.mode,
modified: time_from_parts(meta.modified_secs, meta.modified_nanos)?,
created: time_from_parts(meta.created_secs, meta.created_nanos)?,
})
}
fn metadata_to_abi(metadata: Metadata) -> io::Result<BashkitFsAbiMetadata> {
let (modified_secs, modified_nanos) = time_to_parts(metadata.modified)?;
let (created_secs, created_nanos) = time_to_parts(metadata.created)?;
Ok(BashkitFsAbiMetadata {
file_type: match metadata.file_type {
FileType::File => BashkitFsAbiFileType::File,
FileType::Directory => BashkitFsAbiFileType::Directory,
FileType::Symlink => BashkitFsAbiFileType::Symlink,
FileType::Fifo => BashkitFsAbiFileType::Fifo,
},
_reserved: [0; 7],
size: metadata.size,
mode: metadata.mode,
modified_secs,
modified_nanos,
created_secs,
created_nanos,
})
}
fn time_to_parts(time: SystemTime) -> io::Result<(i64, u32)> {
match time.duration_since(UNIX_EPOCH) {
Ok(duration) => Ok((
i64::try_from(duration.as_secs())
.map_err(|_| IoError::new(ErrorKind::InvalidData, "timestamp out of range"))?,
duration.subsec_nanos(),
)),
Err(err) => {
let duration = err.duration();
let secs = i64::try_from(duration.as_secs())
.map_err(|_| IoError::new(ErrorKind::InvalidData, "timestamp out of range"))?;
let nanos = duration.subsec_nanos();
if nanos == 0 {
Ok((
secs.checked_neg().ok_or_else(|| {
IoError::new(ErrorKind::InvalidData, "timestamp out of range")
})?,
0,
))
} else {
Ok((
secs.checked_add(1)
.and_then(|value| value.checked_neg())
.ok_or_else(|| {
IoError::new(ErrorKind::InvalidData, "timestamp out of range")
})?,
1_000_000_000 - nanos,
))
}
}
}
}
fn time_from_parts(secs: i64, nanos: u32) -> io::Result<SystemTime> {
if nanos >= 1_000_000_000 {
return Err(IoError::new(
ErrorKind::InvalidData,
"timestamp nanoseconds out of range",
));
}
if secs >= 0 {
return UNIX_EPOCH
.checked_add(Duration::new(secs as u64, nanos))
.ok_or_else(|| IoError::new(ErrorKind::InvalidData, "timestamp out of range"));
}
let duration = if nanos == 0 {
Duration::new(secs.unsigned_abs(), 0)
} else {
Duration::new(
secs.unsigned_abs()
.checked_sub(1)
.ok_or_else(|| IoError::new(ErrorKind::InvalidData, "timestamp out of range"))?,
1_000_000_000 - nanos,
)
};
UNIX_EPOCH
.checked_sub(duration)
.ok_or_else(|| IoError::new(ErrorKind::InvalidData, "timestamp out of range"))
}
fn bytes_from_vec(bytes: Vec<u8>) -> BashkitFsAbiOwnedBytes {
let boxed = bytes.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *mut u8;
BashkitFsAbiOwnedBytes { ptr, len }
}
unsafe fn free_owned_bytes(bytes: BashkitFsAbiOwnedBytes) {
if bytes.ptr.is_null() {
return;
}
let raw = ptr::slice_from_raw_parts_mut(bytes.ptr, bytes.len);
unsafe {
drop(Box::from_raw(raw));
}
}
fn dir_entries_from_vec(entries: Vec<DirEntry>) -> io::Result<BashkitFsAbiOwnedDirEntries> {
let mut raw_entries = Vec::with_capacity(entries.len());
for entry in entries {
raw_entries.push(BashkitFsAbiDirEntry {
name: bytes_from_vec(entry.name.into_bytes()),
metadata: metadata_to_abi(entry.metadata)?,
});
}
let boxed = raw_entries.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *mut BashkitFsAbiDirEntry;
Ok(BashkitFsAbiOwnedDirEntries { ptr, len })
}
unsafe fn free_owned_dir_entries(entries: BashkitFsAbiOwnedDirEntries) {
if entries.ptr.is_null() {
return;
}
let raw = ptr::slice_from_raw_parts_mut(entries.ptr, entries.len);
let boxed = unsafe { Box::from_raw(raw) };
for entry in boxed.iter() {
unsafe {
free_owned_bytes(entry.name);
}
}
}
fn owned_dir_entries_to_vec(entries: BashkitFsAbiOwnedDirEntries) -> io::Result<Vec<DirEntry>> {
if entries.len == 0 {
return Ok(Vec::new());
}
if entries.ptr.is_null() {
return Err(IoError::new(
ErrorKind::InvalidData,
"directory entries pointer must not be null when len > 0",
));
}
let slice = unsafe { slice::from_raw_parts(entries.ptr.cast_const(), entries.len) };
let mut out = Vec::with_capacity(slice.len());
for entry in slice {
let name = String::from_utf8(owned_bytes_to_vec(entry.name)?)
.map_err(|e| IoError::new(ErrorKind::InvalidData, e.to_string()))?;
out.push(DirEntry {
name,
metadata: abi_metadata_to_metadata(entry.metadata)?,
});
}
Ok(out)
}
fn io_kind_to_abi(kind: ErrorKind) -> BashkitFsAbiErrorKind {
match kind {
ErrorKind::NotFound => BashkitFsAbiErrorKind::NotFound,
ErrorKind::AlreadyExists => BashkitFsAbiErrorKind::AlreadyExists,
ErrorKind::PermissionDenied => BashkitFsAbiErrorKind::PermissionDenied,
ErrorKind::InvalidInput | ErrorKind::InvalidData => BashkitFsAbiErrorKind::InvalidInput,
ErrorKind::IsADirectory => BashkitFsAbiErrorKind::IsADirectory,
ErrorKind::NotADirectory => BashkitFsAbiErrorKind::NotADirectory,
ErrorKind::DirectoryNotEmpty => BashkitFsAbiErrorKind::DirectoryNotEmpty,
ErrorKind::Unsupported => BashkitFsAbiErrorKind::Unsupported,
_ => BashkitFsAbiErrorKind::Other,
}
}
fn abi_kind_to_io(kind: BashkitFsAbiErrorKind) -> ErrorKind {
match kind {
BashkitFsAbiErrorKind::NotFound => ErrorKind::NotFound,
BashkitFsAbiErrorKind::AlreadyExists => ErrorKind::AlreadyExists,
BashkitFsAbiErrorKind::PermissionDenied => ErrorKind::PermissionDenied,
BashkitFsAbiErrorKind::InvalidInput => ErrorKind::InvalidInput,
BashkitFsAbiErrorKind::IsADirectory => ErrorKind::IsADirectory,
BashkitFsAbiErrorKind::NotADirectory => ErrorKind::NotADirectory,
BashkitFsAbiErrorKind::DirectoryNotEmpty => ErrorKind::DirectoryNotEmpty,
BashkitFsAbiErrorKind::Unsupported => ErrorKind::Unsupported,
BashkitFsAbiErrorKind::Other => ErrorKind::Other,
}
}
fn fill_abi_error(dst: *mut BashkitFsAbiError, err: &IoError) {
if dst.is_null() {
return;
}
let message = err.to_string();
unsafe {
ptr::write(
dst,
BashkitFsAbiError {
kind: io_kind_to_abi(err.kind()),
message: bytes_from_vec(message.into_bytes()),
},
);
}
}
fn abi_error_to_io(
vtable: &BashkitFsAbiVTableV1,
instance: *const c_void,
err: BashkitFsAbiError,
) -> IoError {
let message = if err.message.len == 0 || err.message.ptr.is_null() {
String::new()
} else {
unsafe { String::from_utf8_lossy(slice::from_raw_parts(err.message.ptr, err.message.len)) }
.into_owned()
};
if let Some(free_bytes) = vtable.free_bytes {
unsafe {
free_bytes(instance, err.message);
}
}
IoError::new(abi_kind_to_io(err.kind), message)
}
fn bash_error_to_io(err: BashError) -> IoError {
match err {
BashError::Io(io) => io,
BashError::Cancelled => IoError::new(ErrorKind::Interrupted, "execution cancelled"),
other => IoError::other(other.to_string()),
}
}
fn export_state<'a>(instance: *const c_void) -> io::Result<&'a ExportState> {
if instance.is_null() {
return Err(IoError::new(
ErrorKind::InvalidInput,
"filesystem ABI instance must not be null",
));
}
Ok(unsafe { &*instance.cast::<ExportState>() })
}
unsafe extern "C" fn retain_export_state(instance: *const c_void) {
if !instance.is_null() {
unsafe {
Arc::increment_strong_count(instance.cast::<ExportState>());
}
}
}
unsafe extern "C" fn release_export_state(instance: *const c_void) {
if !instance.is_null() {
unsafe {
Arc::decrement_strong_count(instance.cast::<ExportState>());
}
}
}
fn check_out_ptr<T>(ptr: *mut T) -> io::Result<()> {
if ptr.is_null() {
return Err(IoError::new(
ErrorKind::InvalidInput,
"output pointer must not be null",
));
}
Ok(())
}
fn call0(
instance: *const c_void,
err: *mut BashkitFsAbiError,
f: impl FnOnce(&ExportState) -> io::Result<()>,
) -> BashkitFsAbiStatus {
match export_state(instance).and_then(f) {
Ok(()) => BASHKIT_FS_ABI_STATUS_OK,
Err(io) => {
fill_abi_error(err, &io);
BASHKIT_FS_ABI_STATUS_ERR
}
}
}
fn call<T>(
instance: *const c_void,
err: *mut BashkitFsAbiError,
f: impl FnOnce(&ExportState) -> io::Result<T>,
out: impl FnOnce(T),
) -> BashkitFsAbiStatus {
match export_state(instance).and_then(f) {
Ok(value) => {
out(value);
BASHKIT_FS_ABI_STATUS_OK
}
Err(io) => {
fill_abi_error(err, &io);
BASHKIT_FS_ABI_STATUS_ERR
}
}
}
unsafe extern "C" fn export_read_file(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedBytes,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call(
instance,
err,
|state| {
check_out_ptr(out)?;
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
let bytes = state.run(async move { fs.read_file(&path).await })?;
Ok(bytes_from_vec(bytes))
},
|value| unsafe {
ptr::write(out, value);
},
)
}
unsafe extern "C" fn export_write_file(
instance: *const c_void,
path: BashkitFsAbiStrRef,
content: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let path = path_buf_from_abi(path)?;
let content = str_ref_to_bytes(content)?.to_vec();
let fs = Arc::clone(&state.fs);
state.run(async move { fs.write_file(&path, &content).await })
})
}
unsafe extern "C" fn export_append_file(
instance: *const c_void,
path: BashkitFsAbiStrRef,
content: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let path = path_buf_from_abi(path)?;
let content = str_ref_to_bytes(content)?.to_vec();
let fs = Arc::clone(&state.fs);
state.run(async move { fs.append_file(&path, &content).await })
})
}
unsafe extern "C" fn export_mkdir(
instance: *const c_void,
path: BashkitFsAbiStrRef,
recursive: bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.mkdir(&path, recursive).await })
})
}
unsafe extern "C" fn export_remove(
instance: *const c_void,
path: BashkitFsAbiStrRef,
recursive: bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.remove(&path, recursive).await })
})
}
unsafe extern "C" fn export_stat(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiMetadata,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call(
instance,
err,
|state| {
check_out_ptr(out)?;
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
let metadata = state.run(async move { fs.stat(&path).await })?;
metadata_to_abi(metadata)
},
|value| unsafe {
ptr::write(out, value);
},
)
}
unsafe extern "C" fn export_read_dir(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedDirEntries,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call(
instance,
err,
|state| {
check_out_ptr(out)?;
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
let entries = state.run(async move { fs.read_dir(&path).await })?;
dir_entries_from_vec(entries)
},
|value| unsafe {
ptr::write(out, value);
},
)
}
unsafe extern "C" fn export_exists(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut bool,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call(
instance,
err,
|state| {
check_out_ptr(out)?;
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.exists(&path).await })
},
|value| unsafe {
ptr::write(out, value);
},
)
}
unsafe extern "C" fn export_rename(
instance: *const c_void,
from: BashkitFsAbiStrRef,
to: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let from = path_buf_from_abi(from)?;
let to = path_buf_from_abi(to)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.rename(&from, &to).await })
})
}
unsafe extern "C" fn export_copy(
instance: *const c_void,
from: BashkitFsAbiStrRef,
to: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let from = path_buf_from_abi(from)?;
let to = path_buf_from_abi(to)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.copy(&from, &to).await })
})
}
unsafe extern "C" fn export_symlink(
instance: *const c_void,
target: BashkitFsAbiStrRef,
link: BashkitFsAbiStrRef,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let target = path_buf_from_abi(target)?;
let link = path_buf_from_abi(link)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.symlink(&target, &link).await })
})
}
unsafe extern "C" fn export_read_link(
instance: *const c_void,
path: BashkitFsAbiStrRef,
out: *mut BashkitFsAbiOwnedBytes,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call(
instance,
err,
|state| {
check_out_ptr(out)?;
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
let target = state.run(async move { fs.read_link(&path).await })?;
Ok(bytes_from_vec(
target.to_string_lossy().into_owned().into_bytes(),
))
},
|value| unsafe {
ptr::write(out, value);
},
)
}
unsafe extern "C" fn export_chmod(
instance: *const c_void,
path: BashkitFsAbiStrRef,
mode: u32,
err: *mut BashkitFsAbiError,
) -> BashkitFsAbiStatus {
call0(instance, err, |state| {
let path = path_buf_from_abi(path)?;
let fs = Arc::clone(&state.fs);
state.run(async move { fs.chmod(&path, mode).await })
})
}
unsafe extern "C" fn export_free_bytes(_instance: *const c_void, bytes: BashkitFsAbiOwnedBytes) {
unsafe {
free_owned_bytes(bytes);
}
}
unsafe extern "C" fn export_free_dir_entries(
_instance: *const c_void,
entries: BashkitFsAbiOwnedDirEntries,
) {
unsafe {
free_owned_dir_entries(entries);
}
}
static EXPORT_VTABLE: BashkitFsAbiVTableV1 = BashkitFsAbiVTableV1 {
read_file: Some(export_read_file),
write_file: Some(export_write_file),
append_file: Some(export_append_file),
mkdir: Some(export_mkdir),
remove: Some(export_remove),
stat: Some(export_stat),
read_dir: Some(export_read_dir),
exists: Some(export_exists),
rename: Some(export_rename),
copy: Some(export_copy),
symlink: Some(export_symlink),
read_link: Some(export_read_link),
chmod: Some(export_chmod),
free_bytes: Some(export_free_bytes),
free_dir_entries: Some(export_free_dir_entries),
};
#[cfg(test)]
mod tests {
use super::*;
use crate::InMemoryFs;
#[test]
fn export_import_roundtrip() {
let rt = Builder::new_current_thread().enable_all().build().unwrap();
let source: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
rt.block_on(async {
source.mkdir(Path::new("/org/repo"), true).await.unwrap();
source
.write_file(Path::new("/org/repo/README.md"), b"interop\n")
.await
.unwrap();
});
let exported = export_filesystem(source).unwrap();
let imported = import_owned_filesystem(&exported).unwrap();
let bytes = rt
.block_on(async { imported.read_file(Path::new("/org/repo/README.md")).await })
.unwrap();
assert_eq!(bytes, b"interop\n");
}
#[test]
fn rejects_unknown_abi_version() {
let handle = BashkitFsAbiHandleV1 {
abi_version: 999,
_reserved: 0,
instance: ptr::null(),
retain: Some(retain_export_state),
release: Some(release_export_state),
vtable: ptr::null(),
};
let err = match ImportedFileSystem::from_handle(&handle) {
Ok(_) => panic!("expected ABI version check to fail"),
Err(err) => err,
};
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn rejects_null_abi_callbacks_before_retain() {
let source: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
let exported = export_filesystem(source).unwrap();
let mut handle = *exported.as_handle();
handle.retain = None;
let err = match ImportedFileSystem::from_handle(&handle) {
Ok(_) => panic!("expected null retain check to fail"),
Err(err) => err,
};
assert_eq!(err.kind(), ErrorKind::InvalidData);
let mut vtable = EXPORT_VTABLE;
vtable.read_file = None;
let mut handle = *exported.as_handle();
handle.vtable = &vtable;
let err = match ImportedFileSystem::from_handle(&handle) {
Ok(_) => panic!("expected null vtable entry check to fail"),
Err(err) => err,
};
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn export_callbacks_reject_null_instance() {
let path = BashkitFsAbiStrRef {
ptr: b"/missing".as_ptr(),
len: b"/missing".len(),
};
let mut out = BashkitFsAbiOwnedBytes::default();
let mut err = BashkitFsAbiError::default();
let status = unsafe { export_read_file(ptr::null(), path, &mut out, &mut err) };
assert_eq!(status, BASHKIT_FS_ABI_STATUS_ERR);
assert_eq!(err.kind, BashkitFsAbiErrorKind::InvalidInput);
unsafe {
free_owned_bytes(err.message);
}
}
}