use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, Ordering};
static REPORTED: AtomicBool = AtomicBool::new(false);
pub fn guard<T>(what: &'static str, fallback: T, body: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(body)) {
Ok(value) => value,
Err(payload) => {
report(what, &payload);
fallback
}
}
}
fn report(what: &'static str, payload: &Box<dyn std::any::Any + Send>) {
if REPORTED.swap(true, Ordering::Relaxed) {
return;
}
let msg = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string());
eprintln!(
"libasdf-rs: internal error: a panic escaped {what}: {msg}\n\
libasdf-rs: this is a bug; the call returned a failure value instead.\n\
libasdf-rs: please report it at https://github.com/cruzzil/asdf/issues"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_the_value_when_nothing_panics() {
assert_eq!(guard("test", -1, || 42), 42);
}
#[test]
fn returns_the_fallback_on_panic() {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let got = guard("test", -1, || panic!("boom"));
std::panic::set_hook(prev);
assert_eq!(got, -1);
}
#[test]
fn null_pointers_are_a_valid_fallback() {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let got: *mut u8 = guard("test", std::ptr::null_mut(), || panic!("boom"));
std::panic::set_hook(prev);
assert!(got.is_null());
}
}