use core::fmt::{self, Display, Write as _};
pub const FFI_MSG_CAP: usize = 128;
pub type FfiMessage = heapless::String<FFI_MSG_CAP>;
fn build_message(msg: impl Display) -> FfiMessage {
let mut buf = FfiMessage::new();
let _ = write!(Truncating(&mut buf), "{msg}");
buf
}
struct Truncating<'a>(&'a mut FfiMessage);
impl fmt::Write for Truncating<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let remaining = FFI_MSG_CAP - self.0.len();
if remaining == 0 {
return Ok(());
}
let mut end = s.len().min(remaining);
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
let _ = self.0.push_str(&s[..end]);
Ok(())
}
}
#[cfg(feature = "std")]
std::thread_local! {
static PROVIDER_ERROR: core::cell::RefCell<Option<FfiMessage>> =
const { core::cell::RefCell::new(None) };
}
#[inline]
pub fn set_provider_error(msg: impl Display) {
#[cfg(feature = "std")]
PROVIDER_ERROR.with(|cell| *cell.borrow_mut() = Some(build_message(msg)));
#[cfg(not(feature = "std"))]
let _ = msg;
}
#[inline]
#[must_use]
pub fn take_provider_error() -> Option<FfiMessage> {
#[cfg(feature = "std")]
{
PROVIDER_ERROR.with(|cell| cell.borrow_mut().take())
}
#[cfg(not(feature = "std"))]
{
None
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("value was not finite")]
NotFinite,
#[error("parse error")]
Parse,
#[error("{0} is out of its valid range")]
OutOfRange(&'static str),
#[error("{message} (code {code})")]
Ffi {
code: i32,
message: FfiMessage,
},
#[error("unsupported coordinate system for this operation")]
UnsupportedSystem,
#[error("observer is not Earth-bound for this operation")]
UnsupportedObserver,
#[error("ephemeris error")]
Ephemeris,
}
impl Error {
#[inline]
pub(crate) fn ffi(code: impl Into<i32>) -> Self {
let code = code.into();
let message = take_provider_error()
.unwrap_or_else(|| build_message(format_args!("FFI call returned an error")));
Error::Ffi { code, message }
}
}
pub type Result<T> = core::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_and_take_provider_error() {
set_provider_error("something went wrong");
let msg = take_provider_error();
assert_eq!(msg.as_deref(), Some("something went wrong"));
assert!(take_provider_error().is_none());
}
#[test]
fn ffi_drains_captured_description() {
set_provider_error("bad ephemeris call");
let err = Error::ffi(3);
assert!(format!("{err}").contains("bad ephemeris call"));
let err2 = Error::ffi(7);
assert!(format!("{err2}").contains('7'));
}
#[test]
fn ffi_exposes_code_and_message() {
set_provider_error("boom");
let err = Error::ffi(42);
match err {
Error::Ffi { code, message } => {
assert_eq!(code, 42);
assert_eq!(message.as_str(), "boom");
}
other => panic!("expected Ffi, got {other:?}"),
}
}
#[test]
fn set_provider_error_accepts_format_args() {
let id = 499;
set_provider_error(format_args!("NAIF {id} not covered"));
assert_eq!(
take_provider_error().as_deref(),
Some("NAIF 499 not covered")
);
}
#[test]
fn long_message_truncates_without_panicking() {
let long = "x".repeat(FFI_MSG_CAP * 2);
set_provider_error(&long);
let msg = take_provider_error().unwrap();
assert_eq!(msg.len(), FFI_MSG_CAP, "should fill exactly to capacity");
assert!(msg.chars().all(|c| c == 'x'));
}
#[test]
fn truncation_respects_utf8_boundaries() {
let s: String = core::iter::once('a')
.chain(std::iter::repeat_n('é', FFI_MSG_CAP))
.collect();
set_provider_error(&s);
let msg = take_provider_error().unwrap();
assert_eq!(
msg.len(),
FFI_MSG_CAP - 1,
"should stop one byte short, not split 'é'"
);
assert!(msg.starts_with('a'));
assert!(msg.chars().skip(1).all(|c| c == 'é'));
}
#[test]
fn not_finite_display() {
let s = format!("{}", Error::NotFinite);
assert_eq!(s, "value was not finite");
}
#[test]
fn parse_display() {
let s = format!("{}", Error::Parse);
assert!(!s.is_empty());
}
#[test]
fn unsupported_system_display() {
let s = format!("{}", Error::UnsupportedSystem);
assert!(!s.is_empty());
}
#[test]
fn ephemeris_display() {
let s = format!("{}", Error::Ephemeris);
assert!(!s.is_empty());
}
#[test]
fn error_is_clone() {
let e = Error::NotFinite;
let _e2 = e.clone();
}
}