use std::{fmt::Display, slice};
use nautilus_model::types::fixed::FIXED_PRECISION;
use crate::{
NAUTILUS_PLUGIN_ABI_VERSION, PLUGIN_BUILD_ID_VERSION, boundary::BorrowedStr, host::HostVTable,
};
pub type PluginInitFn = unsafe extern "C" fn(host: *const HostVTable) -> *const PluginManifest;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PluginBuildId {
pub schema_version: u32,
pub nautilus_plugin_version: BorrowedStr<'static>,
pub rustc_version: BorrowedStr<'static>,
pub target_triple: BorrowedStr<'static>,
pub build_profile: BorrowedStr<'static>,
pub precision_mode: BorrowedStr<'static>,
pub fixed_precision: u8,
}
impl PluginBuildId {
#[must_use]
pub const fn current() -> Self {
Self {
schema_version: PLUGIN_BUILD_ID_VERSION,
nautilus_plugin_version: BorrowedStr::from_str(env!("CARGO_PKG_VERSION")),
rustc_version: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_RUSTC_VERSION")),
target_triple: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_TARGET")),
build_profile: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_PROFILE")),
precision_mode: BorrowedStr::from_str(compiled_precision_mode()),
fixed_precision: FIXED_PRECISION,
}
}
}
#[must_use]
pub const fn compiled_precision_mode() -> &'static str {
if FIXED_PRECISION > 9 {
"high-precision"
} else {
"standard"
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PluginManifestValidationErrors {
messages: Vec<String>,
}
impl PluginManifestValidationErrors {
#[must_use]
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
#[must_use]
pub fn messages(&self) -> &[String] {
&self.messages
}
fn push(&mut self, message: impl Into<String>) {
self.messages.push(message.into());
}
}
impl Display for PluginManifestValidationErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (index, message) in self.messages.iter().enumerate() {
if index > 0 {
write!(f, "; ")?;
}
write!(f, "{message}")?;
}
Ok(())
}
}
impl std::error::Error for PluginManifestValidationErrors {}
#[repr(C)]
#[derive(Debug)]
pub struct PluginManifest {
pub abi_version: u32,
pub plugin_name: BorrowedStr<'static>,
pub plugin_vendor: BorrowedStr<'static>,
pub plugin_version: BorrowedStr<'static>,
pub build_id: PluginBuildId,
}
impl PluginManifest {
#[must_use]
pub fn matches_compiled_abi(&self) -> bool {
self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
}
pub fn validate(&self) -> Result<(), PluginManifestValidationErrors> {
let mut errors = PluginManifestValidationErrors::default();
validate_required_str("plugin_name", self.plugin_name, &mut errors);
validate_optional_str("plugin_vendor", self.plugin_vendor, &mut errors);
validate_required_str("plugin_version", self.plugin_version, &mut errors);
validate_build_id(&self.build_id, &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn validate_build_id(build_id: &PluginBuildId, errors: &mut PluginManifestValidationErrors) {
if build_id.schema_version != PLUGIN_BUILD_ID_VERSION {
errors.push(format!(
"build_id.schema_version {} does not match supported schema {}",
build_id.schema_version, PLUGIN_BUILD_ID_VERSION
));
return;
}
validate_optional_str(
"build_id.nautilus_plugin_version",
build_id.nautilus_plugin_version,
errors,
);
validate_optional_str("build_id.rustc_version", build_id.rustc_version, errors);
validate_optional_str("build_id.target_triple", build_id.target_triple, errors);
validate_optional_str("build_id.build_profile", build_id.build_profile, errors);
if let Some(precision_mode) =
validate_required_str("build_id.precision_mode", build_id.precision_mode, errors)
{
let expected = compiled_precision_mode();
if precision_mode != expected {
errors.push(format!(
"build_id.precision_mode '{precision_mode}' does not match host precision mode '{expected}'"
));
}
}
if build_id.fixed_precision != FIXED_PRECISION {
errors.push(format!(
"build_id.fixed_precision {} does not match host fixed precision {}",
build_id.fixed_precision, FIXED_PRECISION
));
}
}
fn validate_required_str<'a>(
field: &str,
value: BorrowedStr<'a>,
errors: &mut PluginManifestValidationErrors,
) -> Option<&'a str> {
let text = validate_optional_str(field, value, errors)?;
if text.is_empty() {
errors.push(format!("{field} must not be empty"));
}
Some(text)
}
fn validate_optional_str<'a>(
field: &str,
value: BorrowedStr<'a>,
errors: &mut PluginManifestValidationErrors,
) -> Option<&'a str> {
if value.len == 0 {
return Some("");
}
if value.ptr.is_null() {
errors.push(format!(
"{field} has null pointer with non-zero length {}",
value.len
));
return None;
}
let bytes = unsafe { slice::from_raw_parts(value.ptr, value.len) };
match std::str::from_utf8(bytes) {
Ok(text) => Some(text),
Err(e) => {
errors.push(format!("{field} is not valid UTF-8: {e}"));
None
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn valid_manifest() -> PluginManifest {
PluginManifest {
abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
plugin_name: BorrowedStr::from_str("test-plugin"),
plugin_vendor: BorrowedStr::from_str("nautech"),
plugin_version: BorrowedStr::from_str("1.0.0"),
build_id: PluginBuildId::current(),
}
}
#[rstest]
fn matches_compiled_abi_accepts_compiled_version() {
assert!(valid_manifest().matches_compiled_abi());
}
#[rstest]
fn matches_compiled_abi_rejects_mismatch() {
let manifest = PluginManifest {
abi_version: NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1),
..valid_manifest()
};
assert!(!manifest.matches_compiled_abi());
}
#[rstest]
fn validate_accepts_valid_manifest() {
valid_manifest().validate().unwrap();
}
#[rstest]
fn validate_rejects_missing_name() {
let manifest = PluginManifest {
plugin_name: BorrowedStr::empty(),
..valid_manifest()
};
let errors = manifest.validate().unwrap_err();
assert_eq!(errors.messages(), &["plugin_name must not be empty"]);
}
#[rstest]
fn validate_rejects_mismatched_build_schema() {
let manifest = PluginManifest {
build_id: PluginBuildId {
schema_version: PLUGIN_BUILD_ID_VERSION.wrapping_add(1),
..PluginBuildId::current()
},
..valid_manifest()
};
let errors = manifest.validate().unwrap_err();
assert_eq!(
errors.messages(),
&[format!(
"build_id.schema_version {} does not match supported schema {}",
PLUGIN_BUILD_ID_VERSION.wrapping_add(1),
PLUGIN_BUILD_ID_VERSION
)]
);
}
}