use std::{
collections::BTreeMap,
fmt::{Debug, Display},
slice,
};
use nautilus_model::types::fixed::FIXED_PRECISION;
use crate::{
NAUTILUS_PLUGIN_ABI_VERSION, PLUGIN_BUILD_ID_VERSION,
boundary::{BorrowedStr, Slice},
host::HostVTable,
surfaces::{
actor::ActorVTable, controller::ControllerVTable, custom_data::CustomDataVTable,
strategy::StrategyVTable,
},
};
pub type PluginInitFn = unsafe extern "C" fn(host: *const HostVTable) -> *const PluginManifest;
#[repr(C)]
#[derive(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)]
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,
pub custom_data: Slice<'static, CustomDataRegistration>,
pub actors: Slice<'static, ActorRegistration>,
pub strategies: Slice<'static, StrategyRegistration>,
pub controllers: Slice<'static, ControllerRegistration>,
}
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);
validate_registrations(self, &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
));
}
}
macro_rules! validate_vtable_slots {
($location:expr, $type_name:expr, $vtable:expr, $errors:expr, [$($slot:ident),+ $(,)?]) => {
$(
validate_vtable_slot(
$location,
$type_name,
stringify!($slot),
$vtable.$slot.is_some(),
$errors,
);
)+
};
}
fn validate_registrations(manifest: &PluginManifest, errors: &mut PluginManifestValidationErrors) {
let mut seen_type_names = BTreeMap::<String, String>::new();
if let Some(entries) = validate_slice("custom_data", &manifest.custom_data, errors) {
for (index, entry) in entries.iter().enumerate() {
let location = format!("custom_data[{index}]");
let type_name = validate_type_name(&location, entry.type_name, errors);
validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
if entry.vtable.is_null() {
errors.push(format!("{location}.vtable must not be null"));
} else {
validate_custom_data_vtable(&location, type_name, entry.vtable, errors);
}
}
}
if let Some(entries) = validate_slice("actors", &manifest.actors, errors) {
for (index, entry) in entries.iter().enumerate() {
let location = format!("actors[{index}]");
let type_name = validate_type_name(&location, entry.type_name, errors);
validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
if entry.vtable.is_null() {
errors.push(format!("{location}.vtable must not be null"));
} else {
validate_actor_vtable(&location, type_name, entry.vtable, errors);
}
}
}
if let Some(entries) = validate_slice("strategies", &manifest.strategies, errors) {
for (index, entry) in entries.iter().enumerate() {
let location = format!("strategies[{index}]");
let type_name = validate_type_name(&location, entry.type_name, errors);
validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
if entry.vtable.is_null() {
errors.push(format!("{location}.vtable must not be null"));
} else {
validate_strategy_vtable(&location, type_name, entry.vtable, errors);
}
}
}
if let Some(entries) = validate_slice("controllers", &manifest.controllers, errors) {
for (index, entry) in entries.iter().enumerate() {
let location = format!("controllers[{index}]");
let type_name = validate_type_name(&location, entry.type_name, errors);
validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
if entry.vtable.is_null() {
errors.push(format!("{location}.vtable must not be null"));
} else {
validate_controller_vtable(&location, type_name, entry.vtable, errors);
}
}
}
}
fn validate_custom_data_vtable(
location: &str,
type_name: Option<&str>,
vtable: *const CustomDataVTable,
errors: &mut PluginManifestValidationErrors,
) {
let vtable = unsafe { &*vtable };
validate_vtable_slots!(
location,
type_name,
vtable,
errors,
[
type_name,
schema_ipc,
from_json,
encode_batch,
decode_batch,
ts_event,
ts_init,
to_json,
clone_handle,
drop_handle,
eq_handles,
]
);
}
fn validate_actor_vtable(
location: &str,
type_name: Option<&str>,
vtable: *const ActorVTable,
errors: &mut PluginManifestValidationErrors,
) {
let vtable = unsafe { &*vtable };
validate_vtable_slots!(
location,
type_name,
vtable,
errors,
[
create,
drop_handle,
type_name,
on_start,
on_stop,
on_resume,
on_reset,
on_dispose,
on_degrade,
on_fault,
on_time_event,
on_data,
on_instrument,
on_book_deltas,
on_book,
on_quote,
on_trade,
on_bar,
on_mark_price,
on_index_price,
on_funding_rate,
on_option_greeks,
on_option_chain,
on_instrument_status,
on_instrument_close,
on_order_filled,
on_order_canceled,
on_signal,
on_historical_book_deltas,
on_historical_book_depth,
on_historical_quotes,
on_historical_trades,
on_historical_bars,
on_historical_mark_prices,
on_historical_index_prices,
on_historical_funding_rates,
]
);
}
fn validate_strategy_vtable(
location: &str,
type_name: Option<&str>,
vtable: *const StrategyVTable,
errors: &mut PluginManifestValidationErrors,
) {
let vtable = unsafe { &*vtable };
validate_vtable_slots!(
location,
type_name,
vtable,
errors,
[
create,
drop_handle,
type_name,
on_start,
on_stop,
on_resume,
on_reset,
on_dispose,
on_degrade,
on_fault,
on_time_event,
on_data,
on_instrument,
on_book_deltas,
on_book,
on_quote,
on_trade,
on_bar,
on_mark_price,
on_index_price,
on_funding_rate,
on_option_greeks,
on_option_chain,
on_instrument_status,
on_instrument_close,
on_signal,
on_order_initialized,
on_order_submitted,
on_order_accepted,
on_order_rejected,
on_order_filled,
on_order_canceled,
on_order_expired,
on_order_triggered,
on_order_denied,
on_order_emulated,
on_order_released,
on_order_pending_update,
on_order_pending_cancel,
on_order_modify_rejected,
on_order_cancel_rejected,
on_order_updated,
on_position_opened,
on_position_changed,
on_position_closed,
on_market_exit,
on_historical_book_deltas,
on_historical_book_depth,
on_historical_quotes,
on_historical_trades,
on_historical_bars,
on_historical_mark_prices,
on_historical_index_prices,
on_historical_funding_rates,
]
);
}
fn validate_controller_vtable(
location: &str,
type_name: Option<&str>,
vtable: *const ControllerVTable,
errors: &mut PluginManifestValidationErrors,
) {
let vtable = unsafe { &*vtable };
validate_vtable_slots!(
location,
type_name,
vtable,
errors,
[
prepare,
create,
drop_handle,
type_name,
on_start,
on_stop,
on_resume,
on_reset,
on_dispose,
on_degrade,
on_fault,
on_time_event,
]
);
}
fn validate_vtable_slot(
location: &str,
type_name: Option<&str>,
slot: &str,
is_present: bool,
errors: &mut PluginManifestValidationErrors,
) {
if is_present {
return;
}
let type_name = match type_name {
Some("") | None => "<unknown>",
Some(value) => value,
};
errors.push(format!(
"{location} type '{type_name}' vtable.{slot} must not be null"
));
}
fn validate_type_name<'a>(
location: &str,
value: BorrowedStr<'a>,
errors: &mut PluginManifestValidationErrors,
) -> Option<&'a str> {
validate_required_str(&format!("{location}.type_name"), value, errors)
}
fn validate_unique_type_name(
seen_type_names: &mut BTreeMap<String, String>,
location: &str,
type_name: Option<&str>,
errors: &mut PluginManifestValidationErrors,
) {
let Some(type_name) = type_name else {
return;
};
if type_name.is_empty() {
return;
}
if let Some(first_location) = seen_type_names.get(type_name) {
errors.push(format!(
"type name '{type_name}' appears in both {first_location} and {location}"
));
} else {
seen_type_names.insert(type_name.to_string(), location.to_string());
}
}
fn validate_slice<'a, T>(
field: &str,
value: &Slice<'a, T>,
errors: &mut PluginManifestValidationErrors,
) -> Option<&'a [T]> {
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;
}
Some(unsafe { slice::from_raw_parts(value.ptr, value.len) })
}
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
}
}
}
#[repr(C)]
pub struct CustomDataRegistration {
pub type_name: BorrowedStr<'static>,
pub vtable: *const CustomDataVTable,
}
unsafe impl Send for CustomDataRegistration {}
unsafe impl Sync for CustomDataRegistration {}
#[repr(C)]
pub struct ActorRegistration {
pub type_name: BorrowedStr<'static>,
pub vtable: *const ActorVTable,
}
unsafe impl Send for ActorRegistration {}
unsafe impl Sync for ActorRegistration {}
#[repr(C)]
pub struct StrategyRegistration {
pub type_name: BorrowedStr<'static>,
pub vtable: *const StrategyVTable,
}
unsafe impl Send for StrategyRegistration {}
unsafe impl Sync for StrategyRegistration {}
#[repr(C)]
pub struct ControllerRegistration {
pub type_name: BorrowedStr<'static>,
pub vtable: *const ControllerVTable,
}
unsafe impl Send for ControllerRegistration {}
unsafe impl Sync for ControllerRegistration {}
#[cfg(feature = "host")]
#[derive(Clone, Copy)]
pub struct ValidatedPluginManifest<'a> {
manifest: &'a PluginManifest,
}
#[cfg(feature = "host")]
impl Debug for ValidatedPluginManifest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(ValidatedPluginManifest))
.field("plugin_name", &self.plugin_name())
.finish()
}
}
#[cfg(feature = "host")]
impl<'a> ValidatedPluginManifest<'a> {
pub fn new(manifest: &'a PluginManifest) -> Result<Self, PluginManifestValidationErrors> {
manifest.validate()?;
Ok(Self { manifest })
}
#[must_use]
pub fn manifest(self) -> &'a PluginManifest {
self.manifest
}
#[must_use]
pub fn plugin_name(self) -> &'static str {
unsafe { self.manifest.plugin_name.as_str() }
}
#[must_use]
pub fn custom_data(self) -> impl ExactSizeIterator<Item = ValidatedCustomDataRegistration> {
unsafe { self.manifest.custom_data.as_slice() }
.iter()
.map(ValidatedCustomDataRegistration::from_validated_registration)
}
#[must_use]
pub fn actors(self) -> impl ExactSizeIterator<Item = ValidatedActorRegistration> {
unsafe { self.manifest.actors.as_slice() }
.iter()
.map(ValidatedActorRegistration::from_validated_registration)
}
#[must_use]
pub fn strategies(self) -> impl ExactSizeIterator<Item = ValidatedStrategyRegistration> {
unsafe { self.manifest.strategies.as_slice() }
.iter()
.map(ValidatedStrategyRegistration::from_validated_registration)
}
#[must_use]
pub fn controllers(self) -> impl ExactSizeIterator<Item = ValidatedControllerRegistration> {
unsafe { self.manifest.controllers.as_slice() }
.iter()
.map(ValidatedControllerRegistration::from_validated_registration)
}
}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedCustomDataRegistration {
type_name: &'static str,
vtable: ValidatedCustomDataVTable,
}
#[cfg(feature = "host")]
impl ValidatedCustomDataRegistration {
fn from_validated_registration(registration: &CustomDataRegistration) -> Self {
Self {
type_name: unsafe { registration.type_name.as_str() },
vtable: ValidatedCustomDataVTable::from_validated_ptr(registration.vtable),
}
}
#[must_use]
pub fn type_name(self) -> &'static str {
self.type_name
}
#[must_use]
pub fn vtable(self) -> ValidatedCustomDataVTable {
self.vtable
}
}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedActorRegistration {
type_name: &'static str,
vtable: ValidatedActorVTable,
}
#[cfg(feature = "host")]
impl ValidatedActorRegistration {
fn from_validated_registration(registration: &ActorRegistration) -> Self {
Self {
type_name: unsafe { registration.type_name.as_str() },
vtable: ValidatedActorVTable::from_validated_ptr(registration.vtable),
}
}
#[must_use]
pub fn type_name(self) -> &'static str {
self.type_name
}
#[must_use]
pub fn vtable(self) -> ValidatedActorVTable {
self.vtable
}
}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedStrategyRegistration {
type_name: &'static str,
vtable: ValidatedStrategyVTable,
}
#[cfg(feature = "host")]
impl ValidatedStrategyRegistration {
fn from_validated_registration(registration: &StrategyRegistration) -> Self {
Self {
type_name: unsafe { registration.type_name.as_str() },
vtable: ValidatedStrategyVTable::from_validated_ptr(registration.vtable),
}
}
#[must_use]
pub fn type_name(self) -> &'static str {
self.type_name
}
#[must_use]
pub fn vtable(self) -> ValidatedStrategyVTable {
self.vtable
}
}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedControllerRegistration {
type_name: &'static str,
vtable: ValidatedControllerVTable,
}
#[cfg(feature = "host")]
impl ValidatedControllerRegistration {
fn from_validated_registration(registration: &ControllerRegistration) -> Self {
Self {
type_name: unsafe { registration.type_name.as_str() },
vtable: ValidatedControllerVTable::from_validated_ptr(registration.vtable),
}
}
#[must_use]
pub fn type_name(self) -> &'static str {
self.type_name
}
#[must_use]
pub fn vtable(self) -> ValidatedControllerVTable {
self.vtable
}
}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedCustomDataVTable {
ptr: std::ptr::NonNull<CustomDataVTable>,
}
#[cfg(feature = "host")]
impl ValidatedCustomDataVTable {
fn from_validated_ptr(ptr: *const CustomDataVTable) -> Self {
Self {
ptr: std::ptr::NonNull::new(ptr.cast_mut())
.expect("validated manifest stores non-null CustomDataVTable"),
}
}
#[must_use]
pub unsafe fn from_raw_unchecked(ptr: *const CustomDataVTable) -> Self {
Self::from_validated_ptr(ptr)
}
#[must_use]
pub fn as_ptr(self) -> *const CustomDataVTable {
self.ptr.as_ptr()
}
}
#[cfg(feature = "host")]
unsafe impl Send for ValidatedCustomDataVTable {}
#[cfg(feature = "host")]
unsafe impl Sync for ValidatedCustomDataVTable {}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedActorVTable {
ptr: std::ptr::NonNull<ActorVTable>,
}
#[cfg(feature = "host")]
impl ValidatedActorVTable {
fn from_validated_ptr(ptr: *const ActorVTable) -> Self {
Self {
ptr: std::ptr::NonNull::new(ptr.cast_mut())
.expect("validated manifest stores non-null ActorVTable"),
}
}
#[must_use]
pub unsafe fn from_raw_unchecked(ptr: *const ActorVTable) -> Self {
Self::from_validated_ptr(ptr)
}
#[must_use]
pub fn as_ptr(self) -> *const ActorVTable {
self.ptr.as_ptr()
}
}
#[cfg(feature = "host")]
unsafe impl Send for ValidatedActorVTable {}
#[cfg(feature = "host")]
unsafe impl Sync for ValidatedActorVTable {}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedStrategyVTable {
ptr: std::ptr::NonNull<StrategyVTable>,
}
#[cfg(feature = "host")]
impl ValidatedStrategyVTable {
fn from_validated_ptr(ptr: *const StrategyVTable) -> Self {
Self {
ptr: std::ptr::NonNull::new(ptr.cast_mut())
.expect("validated manifest stores non-null StrategyVTable"),
}
}
#[must_use]
pub unsafe fn from_raw_unchecked(ptr: *const StrategyVTable) -> Self {
Self::from_validated_ptr(ptr)
}
#[must_use]
pub fn as_ptr(self) -> *const StrategyVTable {
self.ptr.as_ptr()
}
}
#[cfg(feature = "host")]
unsafe impl Send for ValidatedStrategyVTable {}
#[cfg(feature = "host")]
unsafe impl Sync for ValidatedStrategyVTable {}
#[cfg(feature = "host")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidatedControllerVTable {
ptr: std::ptr::NonNull<ControllerVTable>,
}
#[cfg(feature = "host")]
impl ValidatedControllerVTable {
fn from_validated_ptr(ptr: *const ControllerVTable) -> Self {
Self {
ptr: std::ptr::NonNull::new(ptr.cast_mut())
.expect("validated manifest stores non-null ControllerVTable"),
}
}
#[must_use]
pub unsafe fn from_raw_unchecked(ptr: *const ControllerVTable) -> Self {
Self::from_validated_ptr(ptr)
}
#[must_use]
pub fn as_ptr(self) -> *const ControllerVTable {
self.ptr.as_ptr()
}
}
#[cfg(feature = "host")]
unsafe impl Send for ValidatedControllerVTable {}
#[cfg(feature = "host")]
unsafe impl Sync for ValidatedControllerVTable {}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use rstest::rstest;
use super::*;
#[derive(Clone, PartialEq)]
struct ManifestTestTick;
impl crate::surfaces::custom_data::PluginCustomData for ManifestTestTick {
const TYPE_NAME: &'static str = "ManifestTestTick";
fn ts_event(&self) -> u64 {
0
}
fn ts_init(&self) -> u64 {
0
}
fn to_json(&self) -> anyhow::Result<Vec<u8>> {
Ok(Vec::new())
}
fn from_json(_payload: &[u8]) -> anyhow::Result<Self> {
Ok(Self)
}
fn schema_ipc() -> anyhow::Result<Vec<u8>> {
Ok(Vec::new())
}
fn encode_batch(_items: &[&Self]) -> anyhow::Result<Vec<u8>> {
Ok(Vec::new())
}
fn decode_batch(
_ipc_bytes: &[u8],
_metadata: &[(String, String)],
) -> anyhow::Result<Vec<Self>> {
Ok(Vec::new())
}
}
struct ManifestTestActor;
impl crate::surfaces::actor::PluginActor for ManifestTestActor {
const TYPE_NAME: &'static str = "ManifestTestActor";
fn new(
_host: *const HostVTable,
_ctx: *const crate::host::HostContext,
_config_json: &str,
) -> Self {
Self
}
}
struct ManifestTestStrategy;
impl crate::surfaces::strategy::PluginStrategy for ManifestTestStrategy {
const TYPE_NAME: &'static str = "ManifestTestStrategy";
fn new(
_host: *const HostVTable,
_ctx: *const crate::host::HostContext,
_config_json: &str,
) -> Self {
Self
}
}
struct ManifestTestController;
impl crate::surfaces::controller::PluginController for ManifestTestController {
const TYPE_NAME: &'static str = "ManifestTestController";
fn new(
_host: *const crate::host::ControllerHostVTable,
_ctx: *const crate::host::ControllerHostContext,
_config_json: &str,
) -> Self {
Self
}
}
static VALID_CUSTOM_DATA: LazyLock<[CustomDataRegistration; 1]> = LazyLock::new(|| {
[CustomDataRegistration {
type_name: BorrowedStr::from_str("TestTick"),
vtable: crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>(),
}]
});
static VALID_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
[ActorRegistration {
type_name: BorrowedStr::from_str("TestActor"),
vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
}]
});
static VALID_STRATEGIES: LazyLock<[StrategyRegistration; 1]> = LazyLock::new(|| {
[StrategyRegistration {
type_name: BorrowedStr::from_str("TestStrategy"),
vtable: crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>(),
}]
});
static VALID_CONTROLLERS: LazyLock<[ControllerRegistration; 1]> = LazyLock::new(|| {
[ControllerRegistration {
type_name: BorrowedStr::from_str("TestController"),
vtable: crate::surfaces::controller::controller_vtable::<ManifestTestController>(),
}]
});
static DUPLICATE_CUSTOM_DATA: LazyLock<[CustomDataRegistration; 1]> = LazyLock::new(|| {
[CustomDataRegistration {
type_name: BorrowedStr::from_str("DuplicateType"),
vtable: crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>(),
}]
});
static DUPLICATE_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
[ActorRegistration {
type_name: BorrowedStr::from_str("DuplicateType"),
vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
}]
});
static DUPLICATE_ACTORS_SAME_SLICE: LazyLock<[ActorRegistration; 2]> = LazyLock::new(|| {
[
ActorRegistration {
type_name: BorrowedStr::from_str("DuplicateActor"),
vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
},
ActorRegistration {
type_name: BorrowedStr::from_str("DuplicateActor"),
vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
},
]
});
static EMPTY_TYPE_NAME_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
[ActorRegistration {
type_name: BorrowedStr::empty(),
vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
}]
});
static NULL_VTABLE_CUSTOM_DATA: [CustomDataRegistration; 1] = [CustomDataRegistration {
type_name: BorrowedStr::from_str("NullVTableType"),
vtable: std::ptr::null(),
}];
static INVALID_UTF8: [u8; 1] = [0xff];
fn custom_data_registration(
type_name: &'static str,
vtable: *const CustomDataVTable,
) -> Slice<'static, CustomDataRegistration> {
let entries = Box::leak(Box::new([CustomDataRegistration {
type_name: BorrowedStr::from_str(type_name),
vtable,
}]));
Slice::from_slice(entries)
}
fn actor_registration(
type_name: &'static str,
vtable: *const ActorVTable,
) -> Slice<'static, ActorRegistration> {
let entries = Box::leak(Box::new([ActorRegistration {
type_name: BorrowedStr::from_str(type_name),
vtable,
}]));
Slice::from_slice(entries)
}
fn strategy_registration(
type_name: &'static str,
vtable: *const StrategyVTable,
) -> Slice<'static, StrategyRegistration> {
let entries = Box::leak(Box::new([StrategyRegistration {
type_name: BorrowedStr::from_str(type_name),
vtable,
}]));
Slice::from_slice(entries)
}
fn controller_registration(
type_name: &'static str,
vtable: *const ControllerVTable,
) -> Slice<'static, ControllerRegistration> {
let entries = Box::leak(Box::new([ControllerRegistration {
type_name: BorrowedStr::from_str(type_name),
vtable,
}]));
Slice::from_slice(entries)
}
fn custom_data_vtable_missing_schema_ipc() -> *const CustomDataVTable {
let valid = crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(CustomDataVTable {
type_name: valid.type_name,
schema_ipc: None,
from_json: valid.from_json,
encode_batch: valid.encode_batch,
decode_batch: valid.decode_batch,
ts_event: valid.ts_event,
ts_init: valid.ts_init,
to_json: valid.to_json,
clone_handle: valid.clone_handle,
drop_handle: valid.drop_handle,
eq_handles: valid.eq_handles,
}));
std::ptr::from_ref(&*vtable)
}
fn custom_data_vtable_missing_type_name() -> *const CustomDataVTable {
let valid = crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(CustomDataVTable {
type_name: None,
schema_ipc: valid.schema_ipc,
from_json: valid.from_json,
encode_batch: valid.encode_batch,
decode_batch: valid.decode_batch,
ts_event: valid.ts_event,
ts_init: valid.ts_init,
to_json: valid.to_json,
clone_handle: valid.clone_handle,
drop_handle: valid.drop_handle,
eq_handles: valid.eq_handles,
}));
std::ptr::from_ref(&*vtable)
}
fn actor_vtable_missing_on_quote_and_on_book() -> *const ActorVTable {
let valid = crate::surfaces::actor::actor_vtable::<ManifestTestActor>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(ActorVTable {
create: valid.create,
drop_handle: valid.drop_handle,
type_name: valid.type_name,
on_start: valid.on_start,
on_stop: valid.on_stop,
on_resume: valid.on_resume,
on_reset: valid.on_reset,
on_dispose: valid.on_dispose,
on_degrade: valid.on_degrade,
on_fault: valid.on_fault,
on_time_event: valid.on_time_event,
on_data: valid.on_data,
on_instrument: valid.on_instrument,
on_book_deltas: valid.on_book_deltas,
on_book: None,
on_quote: None,
on_trade: valid.on_trade,
on_bar: valid.on_bar,
on_mark_price: valid.on_mark_price,
on_index_price: valid.on_index_price,
on_funding_rate: valid.on_funding_rate,
on_option_greeks: valid.on_option_greeks,
on_option_chain: valid.on_option_chain,
on_instrument_status: valid.on_instrument_status,
on_instrument_close: valid.on_instrument_close,
on_order_filled: valid.on_order_filled,
on_order_canceled: valid.on_order_canceled,
on_signal: valid.on_signal,
on_historical_book_deltas: valid.on_historical_book_deltas,
on_historical_book_depth: valid.on_historical_book_depth,
on_historical_quotes: valid.on_historical_quotes,
on_historical_trades: valid.on_historical_trades,
on_historical_bars: valid.on_historical_bars,
on_historical_mark_prices: valid.on_historical_mark_prices,
on_historical_index_prices: valid.on_historical_index_prices,
on_historical_funding_rates: valid.on_historical_funding_rates,
}));
std::ptr::from_ref(&*vtable)
}
fn actor_vtable_missing_create() -> *const ActorVTable {
let valid = crate::surfaces::actor::actor_vtable::<ManifestTestActor>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(ActorVTable {
create: None,
drop_handle: valid.drop_handle,
type_name: valid.type_name,
on_start: valid.on_start,
on_stop: valid.on_stop,
on_resume: valid.on_resume,
on_reset: valid.on_reset,
on_dispose: valid.on_dispose,
on_degrade: valid.on_degrade,
on_fault: valid.on_fault,
on_time_event: valid.on_time_event,
on_data: valid.on_data,
on_instrument: valid.on_instrument,
on_book_deltas: valid.on_book_deltas,
on_book: valid.on_book,
on_quote: valid.on_quote,
on_trade: valid.on_trade,
on_bar: valid.on_bar,
on_mark_price: valid.on_mark_price,
on_index_price: valid.on_index_price,
on_funding_rate: valid.on_funding_rate,
on_option_greeks: valid.on_option_greeks,
on_option_chain: valid.on_option_chain,
on_instrument_status: valid.on_instrument_status,
on_instrument_close: valid.on_instrument_close,
on_order_filled: valid.on_order_filled,
on_order_canceled: valid.on_order_canceled,
on_signal: valid.on_signal,
on_historical_book_deltas: valid.on_historical_book_deltas,
on_historical_book_depth: valid.on_historical_book_depth,
on_historical_quotes: valid.on_historical_quotes,
on_historical_trades: valid.on_historical_trades,
on_historical_bars: valid.on_historical_bars,
on_historical_mark_prices: valid.on_historical_mark_prices,
on_historical_index_prices: valid.on_historical_index_prices,
on_historical_funding_rates: valid.on_historical_funding_rates,
}));
std::ptr::from_ref(&*vtable)
}
fn strategy_vtable_missing_on_book_and_on_position_closed() -> *const StrategyVTable {
let valid = crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(StrategyVTable {
create: valid.create,
drop_handle: valid.drop_handle,
type_name: valid.type_name,
on_start: valid.on_start,
on_stop: valid.on_stop,
on_resume: valid.on_resume,
on_reset: valid.on_reset,
on_dispose: valid.on_dispose,
on_degrade: valid.on_degrade,
on_fault: valid.on_fault,
on_time_event: valid.on_time_event,
on_data: valid.on_data,
on_instrument: valid.on_instrument,
on_book_deltas: valid.on_book_deltas,
on_book: None,
on_quote: valid.on_quote,
on_trade: valid.on_trade,
on_bar: valid.on_bar,
on_mark_price: valid.on_mark_price,
on_index_price: valid.on_index_price,
on_funding_rate: valid.on_funding_rate,
on_option_greeks: valid.on_option_greeks,
on_option_chain: valid.on_option_chain,
on_instrument_status: valid.on_instrument_status,
on_instrument_close: valid.on_instrument_close,
on_signal: valid.on_signal,
on_order_initialized: valid.on_order_initialized,
on_order_submitted: valid.on_order_submitted,
on_order_accepted: valid.on_order_accepted,
on_order_rejected: valid.on_order_rejected,
on_order_filled: valid.on_order_filled,
on_order_canceled: valid.on_order_canceled,
on_order_expired: valid.on_order_expired,
on_order_triggered: valid.on_order_triggered,
on_order_denied: valid.on_order_denied,
on_order_emulated: valid.on_order_emulated,
on_order_released: valid.on_order_released,
on_order_pending_update: valid.on_order_pending_update,
on_order_pending_cancel: valid.on_order_pending_cancel,
on_order_modify_rejected: valid.on_order_modify_rejected,
on_order_cancel_rejected: valid.on_order_cancel_rejected,
on_order_updated: valid.on_order_updated,
on_position_opened: valid.on_position_opened,
on_position_changed: valid.on_position_changed,
on_position_closed: None,
on_market_exit: valid.on_market_exit,
on_historical_book_deltas: valid.on_historical_book_deltas,
on_historical_book_depth: valid.on_historical_book_depth,
on_historical_quotes: valid.on_historical_quotes,
on_historical_trades: valid.on_historical_trades,
on_historical_bars: valid.on_historical_bars,
on_historical_mark_prices: valid.on_historical_mark_prices,
on_historical_index_prices: valid.on_historical_index_prices,
on_historical_funding_rates: valid.on_historical_funding_rates,
}));
std::ptr::from_ref(&*vtable)
}
fn strategy_vtable_missing_drop_handle() -> *const StrategyVTable {
let valid = crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(StrategyVTable {
create: valid.create,
drop_handle: None,
type_name: valid.type_name,
on_start: valid.on_start,
on_stop: valid.on_stop,
on_resume: valid.on_resume,
on_reset: valid.on_reset,
on_dispose: valid.on_dispose,
on_degrade: valid.on_degrade,
on_fault: valid.on_fault,
on_time_event: valid.on_time_event,
on_data: valid.on_data,
on_instrument: valid.on_instrument,
on_book_deltas: valid.on_book_deltas,
on_book: valid.on_book,
on_quote: valid.on_quote,
on_trade: valid.on_trade,
on_bar: valid.on_bar,
on_mark_price: valid.on_mark_price,
on_index_price: valid.on_index_price,
on_funding_rate: valid.on_funding_rate,
on_option_greeks: valid.on_option_greeks,
on_option_chain: valid.on_option_chain,
on_instrument_status: valid.on_instrument_status,
on_instrument_close: valid.on_instrument_close,
on_signal: valid.on_signal,
on_order_initialized: valid.on_order_initialized,
on_order_submitted: valid.on_order_submitted,
on_order_accepted: valid.on_order_accepted,
on_order_rejected: valid.on_order_rejected,
on_order_filled: valid.on_order_filled,
on_order_canceled: valid.on_order_canceled,
on_order_expired: valid.on_order_expired,
on_order_triggered: valid.on_order_triggered,
on_order_denied: valid.on_order_denied,
on_order_emulated: valid.on_order_emulated,
on_order_released: valid.on_order_released,
on_order_pending_update: valid.on_order_pending_update,
on_order_pending_cancel: valid.on_order_pending_cancel,
on_order_modify_rejected: valid.on_order_modify_rejected,
on_order_cancel_rejected: valid.on_order_cancel_rejected,
on_order_updated: valid.on_order_updated,
on_position_opened: valid.on_position_opened,
on_position_changed: valid.on_position_changed,
on_position_closed: valid.on_position_closed,
on_market_exit: valid.on_market_exit,
on_historical_book_deltas: valid.on_historical_book_deltas,
on_historical_book_depth: valid.on_historical_book_depth,
on_historical_quotes: valid.on_historical_quotes,
on_historical_trades: valid.on_historical_trades,
on_historical_bars: valid.on_historical_bars,
on_historical_mark_prices: valid.on_historical_mark_prices,
on_historical_index_prices: valid.on_historical_index_prices,
on_historical_funding_rates: valid.on_historical_funding_rates,
}));
std::ptr::from_ref(&*vtable)
}
fn controller_vtable_missing_prepare() -> *const ControllerVTable {
let valid = crate::surfaces::controller::controller_vtable::<ManifestTestController>();
let valid = unsafe { &*valid };
let vtable = Box::leak(Box::new(ControllerVTable {
prepare: None,
create: valid.create,
drop_handle: valid.drop_handle,
type_name: valid.type_name,
on_start: valid.on_start,
on_stop: valid.on_stop,
on_resume: valid.on_resume,
on_reset: valid.on_reset,
on_dispose: valid.on_dispose,
on_degrade: valid.on_degrade,
on_fault: valid.on_fault,
on_time_event: valid.on_time_event,
}));
std::ptr::from_ref(&*vtable)
}
fn valid_manifest() -> PluginManifest {
PluginManifest {
abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
plugin_name: BorrowedStr::from_str("test"),
plugin_vendor: BorrowedStr::from_str("nautech"),
plugin_version: BorrowedStr::from_str("0.0.0"),
build_id: PluginBuildId::current(),
custom_data: Slice::empty(),
actors: Slice::empty(),
strategies: Slice::empty(),
controllers: Slice::empty(),
}
}
#[rstest]
fn current_build_id_carries_compile_time_metadata() {
let id = PluginBuildId::current();
assert_eq!(id.schema_version, PLUGIN_BUILD_ID_VERSION);
assert_eq!(
unsafe { id.nautilus_plugin_version.as_str() },
env!("CARGO_PKG_VERSION")
);
assert!(!unsafe { id.target_triple.as_str() }.is_empty());
assert!(!unsafe { id.build_profile.as_str() }.is_empty());
assert_eq!(
unsafe { id.precision_mode.as_str() },
compiled_precision_mode()
);
assert_eq!(id.fixed_precision, FIXED_PRECISION);
}
#[rstest]
fn empty_manifest_matches_compiled_abi() {
let m = valid_manifest();
assert!(m.matches_compiled_abi());
m.validate().expect("empty plug-point manifest is valid");
}
#[rstest]
#[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
#[case::previous_v3(3)]
#[case::zero(0)]
#[case::max(u32::MAX)]
fn mismatched_manifest_rejects(#[case] abi: u32) {
let m = PluginManifest {
abi_version: abi,
..valid_manifest()
};
assert!(!m.matches_compiled_abi());
}
#[rstest]
fn validate_accepts_manifest_with_all_plug_point_families() {
let m = PluginManifest {
custom_data: Slice::from_slice(&*VALID_CUSTOM_DATA),
actors: Slice::from_slice(&*VALID_ACTORS),
strategies: Slice::from_slice(&*VALID_STRATEGIES),
controllers: Slice::from_slice(&*VALID_CONTROLLERS),
..valid_manifest()
};
m.validate()
.expect("well-formed plug-point registrations are valid");
}
#[cfg(feature = "host")]
#[rstest]
fn validated_manifest_exposes_wrapped_registrations() {
let m = PluginManifest {
custom_data: Slice::from_slice(&*VALID_CUSTOM_DATA),
actors: Slice::from_slice(&*VALID_ACTORS),
strategies: Slice::from_slice(&*VALID_STRATEGIES),
controllers: Slice::from_slice(&*VALID_CONTROLLERS),
..valid_manifest()
};
let manifest = ValidatedPluginManifest::new(&m)
.expect("well-formed plug-point registrations are valid");
let custom_data = manifest.custom_data().next().expect("custom data entry");
let actor = manifest.actors().next().expect("actor entry");
let strategy = manifest.strategies().next().expect("strategy entry");
let controller = manifest.controllers().next().expect("controller entry");
assert_eq!(manifest.plugin_name(), "test");
assert_eq!(custom_data.type_name(), "TestTick");
assert_eq!(actor.type_name(), "TestActor");
assert_eq!(strategy.type_name(), "TestStrategy");
assert_eq!(controller.type_name(), "TestController");
assert_eq!(custom_data.vtable().as_ptr(), VALID_CUSTOM_DATA[0].vtable);
assert_eq!(actor.vtable().as_ptr(), VALID_ACTORS[0].vtable);
assert_eq!(strategy.vtable().as_ptr(), VALID_STRATEGIES[0].vtable);
assert_eq!(controller.vtable().as_ptr(), VALID_CONTROLLERS[0].vtable);
}
#[rstest]
fn validate_rejects_empty_required_manifest_identifiers() {
let m = PluginManifest {
plugin_name: BorrowedStr::empty(),
plugin_version: BorrowedStr::empty(),
..valid_manifest()
};
let errors = m.validate().expect_err("empty identifiers are invalid");
assert!(
errors
.messages()
.iter()
.any(|message| message == "plugin_name must not be empty")
);
assert!(
errors
.messages()
.iter()
.any(|message| message == "plugin_version must not be empty")
);
}
#[rstest]
fn validate_rejects_mismatched_build_id_schema() {
let m = PluginManifest {
build_id: PluginBuildId {
schema_version: PLUGIN_BUILD_ID_VERSION + 1,
..PluginBuildId::current()
},
..valid_manifest()
};
let errors = m
.validate()
.expect_err("mismatched build-id schema is invalid");
let expected = format!(
"build_id.schema_version {} does not match supported schema {}",
PLUGIN_BUILD_ID_VERSION + 1,
PLUGIN_BUILD_ID_VERSION
);
assert!(errors.to_string().contains(&expected));
}
#[rstest]
fn validate_rejects_mismatched_precision_mode() {
let precision_mode = if compiled_precision_mode() == "high-precision" {
"standard"
} else {
"high-precision"
};
let fixed_precision = if FIXED_PRECISION > 9 { 9 } else { 16 };
let m = PluginManifest {
build_id: PluginBuildId {
precision_mode: BorrowedStr::from_str(precision_mode),
fixed_precision,
..PluginBuildId::current()
},
..valid_manifest()
};
let errors = m
.validate()
.expect_err("mismatched precision mode is invalid");
let rendered = errors.to_string();
assert!(rendered.contains(&format!(
"build_id.precision_mode '{precision_mode}' does not match host precision mode '{}'",
compiled_precision_mode()
)));
assert!(rendered.contains(&format!(
"build_id.fixed_precision {fixed_precision} does not match host fixed precision {FIXED_PRECISION}"
)));
}
#[rstest]
fn validate_rejects_empty_type_name_duplicate_type_name_and_null_vtable() {
let m = PluginManifest {
custom_data: Slice::from_slice(&*DUPLICATE_CUSTOM_DATA),
actors: Slice::from_slice(&*DUPLICATE_ACTORS),
strategies: Slice::from_slice(&*VALID_STRATEGIES),
..valid_manifest()
};
let errors = m.validate().expect_err("duplicate type name is invalid");
assert!(
errors
.to_string()
.contains("type name 'DuplicateType' appears in both custom_data[0] and actors[0]")
);
let m = PluginManifest {
actors: Slice::from_slice(&*EMPTY_TYPE_NAME_ACTORS),
..valid_manifest()
};
let errors = m.validate().expect_err("empty type name is invalid");
assert!(
errors
.messages()
.iter()
.any(|message| message == "actors[0].type_name must not be empty")
);
let m = PluginManifest {
custom_data: Slice::from_slice(&NULL_VTABLE_CUSTOM_DATA),
..valid_manifest()
};
let errors = m.validate().expect_err("null vtable is invalid");
assert!(
errors
.messages()
.iter()
.any(|message| message == "custom_data[0].vtable must not be null")
);
}
#[rstest]
fn validate_rejects_null_required_vtable_slots() {
let m = PluginManifest {
custom_data: custom_data_registration(
"BadTick",
custom_data_vtable_missing_schema_ipc(),
),
actors: actor_registration("BadActor", actor_vtable_missing_on_quote_and_on_book()),
strategies: strategy_registration(
"BadStrategy",
strategy_vtable_missing_on_book_and_on_position_closed(),
),
controllers: controller_registration(
"BadController",
controller_vtable_missing_prepare(),
),
..valid_manifest()
};
let errors = m
.validate()
.expect_err("null required vtable slots are invalid");
let rendered = errors.to_string();
assert!(
rendered.contains("custom_data[0] type 'BadTick' vtable.schema_ipc must not be null")
);
assert!(rendered.contains("actors[0] type 'BadActor' vtable.on_book must not be null"));
assert!(rendered.contains("actors[0] type 'BadActor' vtable.on_quote must not be null"));
assert!(
rendered.contains("strategies[0] type 'BadStrategy' vtable.on_book must not be null")
);
assert!(rendered.contains(
"strategies[0] type 'BadStrategy' vtable.on_position_closed must not be null"
));
assert!(
rendered
.contains("controllers[0] type 'BadController' vtable.prepare must not be null")
);
}
#[rstest]
fn validate_rejects_null_identity_constructor_and_drop_vtable_slots() {
let m = PluginManifest {
custom_data: custom_data_registration(
"MissingTypeNameTick",
custom_data_vtable_missing_type_name(),
),
actors: actor_registration("MissingCreateActor", actor_vtable_missing_create()),
strategies: strategy_registration(
"MissingDropStrategy",
strategy_vtable_missing_drop_handle(),
),
..valid_manifest()
};
let errors = m
.validate()
.expect_err("identity, constructor, and drop slots are required");
let rendered = errors.to_string();
assert!(rendered.contains(
"custom_data[0] type 'MissingTypeNameTick' vtable.type_name must not be null"
));
assert!(
rendered.contains("actors[0] type 'MissingCreateActor' vtable.create must not be null")
);
assert!(rendered.contains(
"strategies[0] type 'MissingDropStrategy' vtable.drop_handle must not be null"
));
}
#[rstest]
fn validate_rejects_duplicate_type_names_within_one_plug_point_slice() {
let m = PluginManifest {
actors: Slice::from_slice(&*DUPLICATE_ACTORS_SAME_SLICE),
..valid_manifest()
};
let errors = m
.validate()
.expect_err("duplicate type names in one slice are invalid");
assert!(
errors
.to_string()
.contains("type name 'DuplicateActor' appears in both actors[0] and actors[1]")
);
}
#[rstest]
fn validate_rejects_malformed_raw_string_and_slice_descriptors() {
let mut plugin_name = BorrowedStr::empty();
plugin_name.ptr = INVALID_UTF8.as_ptr();
plugin_name.len = INVALID_UTF8.len();
let mut plugin_vendor = BorrowedStr::empty();
plugin_vendor.len = 1;
let mut custom_data: Slice<'static, CustomDataRegistration> = Slice::empty();
custom_data.len = 1;
let m = PluginManifest {
plugin_name,
plugin_vendor,
custom_data,
..valid_manifest()
};
let errors = m
.validate()
.expect_err("malformed raw descriptors are invalid");
let rendered = errors.to_string();
assert!(rendered.contains("plugin_name is not valid UTF-8"));
assert!(rendered.contains("plugin_vendor has null pointer with non-zero length 1"));
assert!(rendered.contains("custom_data has null pointer with non-zero length 1"));
}
}