use crate::{Repo, RepoCheckoutFilterResult};
use glib::ffi::gpointer;
use glib::translate::*;
use libc::c_char;
use std::any::Any;
use std::panic::catch_unwind;
use std::path::{Path, PathBuf};
use std::process::abort;
#[allow(clippy::type_complexity)]
pub struct RepoCheckoutFilter(Box<dyn Fn(&Repo, &Path, &libc::stat) -> RepoCheckoutFilterResult>);
impl RepoCheckoutFilter {
pub fn new<F>(closure: F) -> Option<RepoCheckoutFilter>
where
F: (Fn(&Repo, &Path, &libc::stat) -> RepoCheckoutFilterResult) + 'static,
{
Some(RepoCheckoutFilter(Box::new(closure)))
}
fn call(&self, repo: &Repo, path: &Path, stat: &libc::stat) -> RepoCheckoutFilterResult {
self.0(repo, path, stat)
}
}
impl<'a> ToGlibPtr<'a, gpointer> for RepoCheckoutFilter {
type Storage = ();
fn to_glib_none(&'a self) -> Stash<gpointer, Self> {
Stash(self as *const RepoCheckoutFilter as gpointer, ())
}
}
impl FromGlibPtrNone<gpointer> for &RepoCheckoutFilter {
unsafe fn from_glib_none(ptr: gpointer) -> Self {
assert!(!ptr.is_null());
&*(ptr as *const RepoCheckoutFilter)
}
}
unsafe fn filter_trampoline(
repo: *mut ffi::OstreeRepo,
path: *const c_char,
stat: *mut libc::stat,
user_data: gpointer,
) -> ffi::OstreeRepoCheckoutFilterResult {
assert!(!stat.is_null());
let stat = &*stat;
let closure: &RepoCheckoutFilter = from_glib_none(user_data);
let repo = from_glib_borrow(repo);
let path: PathBuf = from_glib_none(path);
let result = closure.call(&repo, &path, stat);
result.into_glib()
}
pub(super) unsafe extern "C" fn filter_trampoline_unwindsafe(
repo: *mut ffi::OstreeRepo,
path: *const c_char,
stat: *mut libc::stat,
user_data: gpointer,
) -> ffi::OstreeRepoCheckoutFilterResult {
let result = catch_unwind(move || filter_trampoline(repo, path, stat, user_data));
result.unwrap_or_else(|panic| {
print_panic(panic);
abort()
})
}
fn print_panic(panic: Box<dyn Any>) {
use std::io::Write;
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
let _ = stderr.write_all(
r#"A Rust callback invoked by C code panicked.
Unwinding across FFI boundaries is Undefined Behavior so abort() will be called."#
.as_bytes(),
);
let msg = {
if let Some(s) = panic.as_ref().downcast_ref::<&str>() {
s
} else if let Some(s) = panic.as_ref().downcast_ref::<String>() {
s
} else {
"(non-string panic value)"
}
};
let _ = stderr.write_all(msg.as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::ptr;
#[test]
#[should_panic]
fn trampoline_should_panic_if_repo_is_nullptr() {
let path = CString::new("/a/b/c").unwrap();
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let filter = RepoCheckoutFilter(Box::new(|_, _, _| RepoCheckoutFilterResult::Allow));
unsafe {
filter_trampoline(
ptr::null_mut(),
path.as_ptr(),
&mut stat,
filter.to_glib_none().0,
);
}
}
#[test]
#[should_panic]
fn trampoline_should_panic_if_path_is_nullptr() {
let repo = Repo::new_default();
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let filter = RepoCheckoutFilter(Box::new(|_, _, _| RepoCheckoutFilterResult::Allow));
unsafe {
filter_trampoline(
repo.to_glib_none().0,
ptr::null(),
&mut stat,
filter.to_glib_none().0,
);
}
}
#[test]
#[should_panic]
fn trampoline_should_panic_if_stat_is_nullptr() {
let repo = Repo::new_default();
let path = CString::new("/a/b/c").unwrap();
let filter = RepoCheckoutFilter(Box::new(|_, _, _| RepoCheckoutFilterResult::Allow));
unsafe {
filter_trampoline(
repo.to_glib_none().0,
path.as_ptr(),
ptr::null_mut(),
filter.to_glib_none().0,
);
}
}
#[test]
#[should_panic]
fn trampoline_should_panic_if_user_data_is_nullptr() {
let repo = Repo::new_default();
let path = CString::new("/a/b/c").unwrap();
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
unsafe {
filter_trampoline(
repo.to_glib_none().0,
path.as_ptr(),
&mut stat,
ptr::null_mut(),
);
}
}
#[test]
fn trampoline_should_call_the_closure() {
let repo = Repo::new_default();
let path = CString::new("/a/b/c").unwrap();
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let filter = {
let repo = repo.clone();
let path = path.clone();
RepoCheckoutFilter(Box::new(move |arg_repo, arg_path, _| {
assert_eq!(arg_repo, &repo);
assert_eq!(&CString::new(arg_path.to_str().unwrap()).unwrap(), &path);
RepoCheckoutFilterResult::Skip
}))
};
let result = unsafe {
filter_trampoline(
repo.to_glib_none().0,
path.as_ptr(),
&mut stat,
filter.to_glib_none().0,
)
};
assert_eq!(result, ffi::OSTREE_REPO_CHECKOUT_FILTER_SKIP);
}
}