use crate::error::{Error, Result, dedupe_io_prefix};
use std::ffi::{CStr, CString};
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
pub struct Context {
raw: NonNull<empyrean_sys::EmpyreanContext>,
}
unsafe impl Send for Context {}
unsafe impl Sync for Context {}
impl Context {
pub fn new_minimal(de440_path: impl AsRef<Path>, gm_path: impl AsRef<Path>) -> Result<Self> {
let de440_c = path_to_cstring(de440_path.as_ref())?;
let gm_c = path_to_cstring(gm_path.as_ref())?;
let raw =
unsafe { empyrean_sys::empyrean_context_new_minimal(de440_c.as_ptr(), gm_c.as_ptr()) };
NonNull::new(raw).map(|raw| Context { raw }).ok_or_else(|| {
let mut err = Error::from_null_ptr();
err.message = dedupe_io_prefix(&err.message);
err
})
}
pub fn from_data_dir(data_dir: Option<&Path>) -> Result<Self> {
let c_path = match data_dir {
Some(d) => Some(path_to_cstring(d)?),
None => None,
};
let raw_path = c_path
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null());
let raw = unsafe { empyrean_sys::empyrean_context_from_data_dir(raw_path) };
NonNull::new(raw)
.map(|raw| Context { raw })
.ok_or_else(|| construction_error(data_dir))
}
pub fn with_spk(&mut self, spk_path: impl AsRef<Path>) -> Result<()> {
let c_path = path_to_cstring(spk_path.as_ref())?;
let code =
unsafe { empyrean_sys::empyrean_context_with_spk(self.raw.as_ptr(), c_path.as_ptr()) };
if code != 0 {
let mut err = Error::capture(code);
err.message = dedupe_io_prefix(&err.message);
return Err(err);
}
Ok(())
}
pub(crate) fn as_raw(&self) -> *const empyrean_sys::EmpyreanContext {
self.raw.as_ptr()
}
}
const CORE_KERNELS: &[&str] = &[
"de440.bsp",
"gm_de440.tpc",
"sb441-n16.bsp",
"obscodes_extended.json",
"earth_latest_high_prec.bpc",
"bias.dat",
];
fn first_missing_core_kernel(dir: &Path) -> Option<&'static str> {
CORE_KERNELS.iter().copied().find(|f| !dir.join(f).exists())
}
fn augment_with_data_dir(base: &str, dir: &Path, missing: Option<&str>) -> String {
match missing {
Some(file) => format!(
"{base} — empyrean data directory '{}' may be incompletely provisioned \
(required kernel '{file}' is absent). Run `empyrean::download_data(None)` to \
provision it, or set EMPYREAN_DATA_DIR to a directory that already contains \
the kernels.",
dir.display(),
),
None => format!("{base} (data directory: '{}')", dir.display()),
}
}
fn construction_error(data_dir: Option<&Path>) -> Error {
let mut err = Error::from_null_ptr();
err.message = dedupe_io_prefix(&err.message);
let resolved = data_dir
.map(Path::to_path_buf)
.or_else(|| default_data_dir().ok());
if let Some(dir) = resolved {
let missing = first_missing_core_kernel(&dir);
if missing.is_some() {
err.code = -2;
}
err.message = augment_with_data_dir(&err.message, &dir, missing);
}
err
}
pub fn default_data_dir() -> Result<std::path::PathBuf> {
let raw = unsafe { empyrean_sys::empyrean_default_data_dir() };
if raw.is_null() {
return Err(Error::capture(-1));
}
let path = unsafe { CStr::from_ptr(raw) }
.to_str()
.map(std::path::PathBuf::from)
.map_err(|_| Error::invalid_input("default data dir is not valid UTF-8"));
unsafe { empyrean_sys::empyrean_string_free(raw) };
path
}
pub fn download_data(data_dir: Option<&Path>) -> Result<PathBuf> {
let _ctx = Context::from_data_dir(data_dir)?;
match data_dir {
Some(d) => Ok(d.to_path_buf()),
None => default_data_dir(),
}
}
impl Drop for Context {
fn drop(&mut self) {
unsafe { empyrean_sys::empyrean_context_free(self.raw.as_ptr()) }
}
}
fn path_to_cstring(path: &Path) -> Result<CString> {
let bytes = path
.to_str()
.ok_or_else(|| Error::invalid_input("path is not valid UTF-8"))?
.as_bytes();
CString::new(bytes).map_err(|_| Error::invalid_input("path contains a NUL byte"))
}
#[cfg(test)]
mod tests {
use super::{CORE_KERNELS, augment_with_data_dir, first_missing_core_kernel};
use std::path::Path;
#[test]
fn construction_message_assembly() {
let m = augment_with_data_dir("I/O error: nope", Path::new("/tmp/dd"), Some("bias.dat"));
assert!(
m.starts_with("I/O error: nope"),
"native cause kept up front: {m}"
);
assert!(m.contains("bias.dat"), "names the missing kernel: {m}");
assert!(m.contains("/tmp/dd"), "names the data directory: {m}");
assert!(
m.contains("download_data"),
"hints the download remedy: {m}"
);
assert!(
m.contains("EMPYREAN_DATA_DIR"),
"hints the env-var remedy: {m}"
);
let g = augment_with_data_dir("boom", Path::new("/tmp/dd"), None);
assert!(g.contains("/tmp/dd"));
assert!(!g.contains("download_data"));
}
#[test]
fn missing_core_kernel_probe() {
let tmp = std::env::temp_dir().join(format!("empyrean-probe-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).unwrap();
assert_eq!(first_missing_core_kernel(&tmp), Some(CORE_KERNELS[0]));
for k in CORE_KERNELS {
std::fs::write(tmp.join(k), b"x").unwrap();
}
assert_eq!(first_missing_core_kernel(&tmp), None);
std::fs::remove_file(tmp.join("sb441-n16.bsp")).unwrap();
assert_eq!(first_missing_core_kernel(&tmp), Some("sb441-n16.bsp"));
let _ = std::fs::remove_dir_all(&tmp);
}
}