#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use polyplug::Runtime;
use polyplug::compatibility::CapabilityGraph;
use polyplug::error::RuntimeError;
use polyplug::loader::{BundleLoader, ManifestData};
use polyplug_abi::runtime::{Compatibility, ReloadPhaseType, RuntimeConfig};
use polyplug_abi::{
AbiErrorCode, DispatchMechanisms, DispatchType, GuestContractInstance, GuestContractInterface,
HostApi, HostContractInstance, HostContractInterface, NativeDispatch, PluginDescriptor,
StringView, Version,
};
use polyplug_utils::{BundleId, GuestContractId, HostContractId};
unsafe extern "C" fn noop_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(GuestContractInstance::null()) };
}
}
unsafe extern "C" fn noop_destroy_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_instance: GuestContractInstance,
) {
}
fn leak_guest_interface(contract_id: u64) -> &'static GuestContractInterface {
Box::leak(Box::new(GuestContractInterface {
contract_id: GuestContractId::from_u64(contract_id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: noop_create_instance,
destroy_instance: noop_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}))
}
struct DepProbeLoader {
declared_contract_id: u64,
declared_resolved: Arc<Mutex<Option<bool>>>,
probing_bundle: String,
}
impl BundleLoader for DepProbeLoader {
fn loader_name(&self) -> &'static str {
"dep-probe"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
manifest: &ManifestData,
_source: &polyplug::loader::BundleSource,
runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
let host_abi: *const HostApi = runtime.host_abi();
let bundle_id: BundleId = BundleId::new(&manifest.name);
runtime.push_init_bundle_id(bundle_id.id());
if manifest.name == self.probing_bundle {
let handle = unsafe {
((*host_abi).find_guest_contract)(host_abi, self.declared_contract_id, 0_u32)
};
*self.declared_resolved.lock().unwrap() = Some(!handle.is_null());
} else {
register_provider_for_loader(runtime, self.declared_contract_id, bundle_id.id());
}
runtime.pop_init_bundle_id();
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
Err(polyplug::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
fn register_provider_for_loader(runtime: &Runtime, contract_id: u64, bundle_id: u64) {
let interface: &'static GuestContractInterface = leak_guest_interface(contract_id);
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"provider-A"),
contract_name: StringView::from_static(b"declared.dep"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
unsafe {
runtime.registry().register_guest_contract(
descriptor,
interface,
"declared.dep".to_owned(),
BundleId::from_u64(bundle_id),
)
}
.expect("provider registration should succeed");
}
fn write_provider_bundle(dir: &std::path::Path, name: &str) -> PathBuf {
let bundle_dir: PathBuf = dir.join(name);
std::fs::create_dir_all(&bundle_dir).expect("create dir");
std::fs::write(bundle_dir.join("dummy.so"), b"").expect("write so");
let id: u64 = polyplug_utils::bundle_id(name);
let manifest: String = format!(
"id = {id}\n\
name = \"{name}\"\n\
loader = \"dep-probe\"\n\
file = \"dummy.so\"\n\
version = \"1.0\"\n\
provides = [\"declared.dep@1\"]\n\
function_count = {{ \"declared.dep@1\" = 0 }}\n"
);
std::fs::write(bundle_dir.join("manifest.toml"), manifest).expect("write manifest");
bundle_dir
}
fn write_dependent_bundle(dir: &std::path::Path, name: &str, declared_contract_id: u64) -> PathBuf {
let bundle_dir: PathBuf = dir.join(name);
std::fs::create_dir_all(&bundle_dir).expect("create dir");
std::fs::write(bundle_dir.join("dummy.so"), b"").expect("write so");
let id: u64 = polyplug_utils::bundle_id(name);
let manifest: String = format!(
"id = {id}\n\
name = \"{name}\"\n\
loader = \"dep-probe\"\n\
file = \"dummy.so\"\n\
version = \"1.0\"\n\n\
[[dependency]]\n\
kind = \"contract\"\n\
contract = \"declared.dep\"\n\
min_version = \"1.0\"\n\
contract_id = {declared_contract_id}\n"
);
std::fs::write(bundle_dir.join("manifest.toml"), manifest).expect("write manifest");
bundle_dir
}
#[test]
fn builder_discovered_bundle_declares_deps_and_has_descriptor() {
let declared_contract_id: u64 = GuestContractId::new("declared.dep", 1_u32).id();
let temp: tempfile::TempDir = tempfile::TempDir::new().expect("temp dir");
write_provider_bundle(temp.path(), "bundle_a_provider");
write_dependent_bundle(temp.path(), "bundle_b_dependent", declared_contract_id);
let declared_resolved: Arc<Mutex<Option<bool>>> = Arc::new(Mutex::new(None));
let runtime: Arc<Runtime> = Runtime::builder()
.plugin_dir(temp.path().to_path_buf())
.loader(DepProbeLoader {
declared_contract_id,
declared_resolved: Arc::clone(&declared_resolved),
probing_bundle: "bundle_b_dependent".to_owned(),
})
.build()
.expect("runtime build with discovered bundles should succeed");
assert_eq!(
*declared_resolved.lock().unwrap(),
Some(true),
"discovered dependent bundle must resolve its declared dependency during init"
);
let provider_id: BundleId = BundleId::new("bundle_a_provider");
let descriptor = runtime
.registry()
.get_bundle_descriptor(provider_id)
.expect("discovered bundle must have a registered descriptor");
assert_eq!(
descriptor.name, "bundle_a_provider",
"discovered bundle descriptor must carry the bundle name"
);
}
fn build_bare_runtime() -> Arc<Runtime> {
Runtime::builder()
.build()
.expect("bare runtime build should succeed")
}
#[test]
fn register_guest_contract_null_descriptor_is_invalid_pointer() {
let runtime: Arc<Runtime> = build_bare_runtime();
let host_abi: *const HostApi = runtime.host_abi();
let interface: &'static GuestContractInterface =
leak_guest_interface(GuestContractId::new("x", 1).id());
let mut err: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe {
((*host_abi).register_guest_contract)(
host_abi,
core::ptr::null(),
interface as *const GuestContractInterface,
&mut err,
)
};
assert_eq!(err.code, AbiErrorCode::InvalidPointer as u32);
}
#[test]
fn register_guest_contract_null_interface_is_invalid_pointer() {
let runtime: Arc<Runtime> = build_bare_runtime();
let host_abi: *const HostApi = runtime.host_abi();
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"n"),
contract_name: StringView::from_static(b"c"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let mut err: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe {
((*host_abi).register_guest_contract)(
host_abi,
&descriptor as *const PluginDescriptor,
core::ptr::null(),
&mut err,
)
};
assert_eq!(err.code, AbiErrorCode::InvalidPointer as u32);
}
unsafe extern "C" fn reentrant_create_instance(
this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
let runtime_ptr: *const Runtime = unsafe { (*this).runtime as *const Runtime };
if !runtime_ptr.is_null() {
let runtime: &Runtime = unsafe { &*runtime_ptr };
let nested: &'static HostContractInterface = leak_inert_host_interface(0xDEAD_u64, false);
let _ = runtime.register_host_contract(0xDEAD_u64, nested);
}
if !out_instance.is_null() {
unsafe { out_instance.write(HostContractInstance::null()) };
}
}
unsafe extern "C" fn inert_destroy_instance(
_this: *const HostContractInterface,
_instance: HostContractInstance,
) {
}
unsafe extern "C" fn inert_create_instance(
_this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(HostContractInstance::null()) };
}
}
fn leak_inert_host_interface(id: u64, singleton: bool) -> &'static HostContractInterface {
Box::leak(Box::new(HostContractInterface {
contract_id: HostContractId::from(id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton,
dispatch_type: DispatchType::Native,
runtime: core::ptr::null_mut(),
user_data: core::ptr::null_mut(),
create_instance: inert_create_instance,
destroy_instance: inert_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}))
}
#[test]
fn get_host_contract_reentrant_register_does_not_deadlock() {
let runtime: Arc<Runtime> = build_bare_runtime();
let contract_id: u64 = 0xC0DE_u64;
let interface: &'static HostContractInterface = Box::leak(Box::new(HostContractInterface {
contract_id: HostContractId::from(contract_id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
dispatch_type: DispatchType::Native,
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
user_data: core::ptr::null_mut(),
create_instance: reentrant_create_instance,
destroy_instance: inert_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}));
runtime
.register_host_contract(contract_id, interface)
.expect("register host contract");
let host_abi: *const HostApi = runtime.host_abi();
let _instance = unsafe { ((*host_abi).get_host_contract)(host_abi, contract_id, 0_u32) };
}
static SINGLETON_CALLS: Mutex<u32> = Mutex::new(0);
unsafe extern "C" fn flaky_singleton_create_instance(
_this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
let mut calls = SINGLETON_CALLS.lock().unwrap();
*calls += 1;
let instance: HostContractInstance = if *calls == 1 {
HostContractInstance::null()
} else {
HostContractInstance {
data: core::ptr::NonNull::<core::ffi::c_void>::dangling().as_ptr(),
}
};
if !out_instance.is_null() {
unsafe { out_instance.write(instance) };
}
}
#[test]
fn get_host_contract_does_not_cache_null_singleton() {
*SINGLETON_CALLS.lock().unwrap() = 0;
let runtime: Arc<Runtime> = build_bare_runtime();
let contract_id: u64 = 0x5151_u64;
let interface: &'static HostContractInterface = Box::leak(Box::new(HostContractInterface {
contract_id: HostContractId::from(contract_id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: true,
dispatch_type: DispatchType::Native,
runtime: core::ptr::null_mut(),
user_data: core::ptr::null_mut(),
create_instance: flaky_singleton_create_instance,
destroy_instance: inert_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}));
runtime
.register_host_contract(contract_id, interface)
.expect("register host contract");
let host_abi: *const HostApi = runtime.host_abi();
let first = unsafe { ((*host_abi).get_host_contract)(host_abi, contract_id, 0_u32) };
assert!(first.is_null(), "first singleton creation returned null");
let second = unsafe { ((*host_abi).get_host_contract)(host_abi, contract_id, 0_u32) };
assert!(
!second.is_null(),
"null singleton must not be cached: a later call must retry and succeed"
);
}
fn manifest_with(
name: &str,
provides: Vec<String>,
deps: Vec<polyplug::loader::RawManifestDependency>,
) -> ManifestData {
let mut m: ManifestData = ManifestData::parse_from_str(&format!(
"loader=\"native\"\nname=\"{name}\"\nfile=\"x.so\"\n"
))
.expect("parse base manifest");
m.id = polyplug_utils::bundle_id(name);
m.version = "1.0.0".to_owned();
m.provides = provides;
m.dependencies = deps;
m
}
#[test]
fn capability_graph_bybundle_matches_versioned_provides() {
let provider: ManifestData = manifest_with(
"reporter_bundle",
vec!["data.Reporter@1.0".to_owned()],
Vec::new(),
);
let dep: polyplug::loader::RawManifestDependency = polyplug::loader::RawManifestDependency {
kind: "bundle".to_owned(),
contract: "data.Reporter".to_owned(),
min_version: "1.0".to_owned(),
bundle: Some("reporter_bundle".to_owned()),
contract_id: GuestContractId::new("data.Reporter", 1),
bundle_id: Some(BundleId::new("reporter_bundle")),
};
let dependent: ManifestData = manifest_with("consumer_bundle", Vec::new(), vec![dep]);
let manifests: Vec<(PathBuf, ManifestData)> = vec![
(PathBuf::from("reporter_bundle"), provider),
(PathBuf::from("consumer_bundle"), dependent),
];
let graph = CapabilityGraph::from_manifests(&manifests);
assert!(
graph.is_ok(),
"ByBundle dep on a bare contract must be satisfied by a versioned provides entry: {:?}",
graph.err()
);
}
struct NeverLoadsLoader;
impl BundleLoader for NeverLoadsLoader {
fn loader_name(&self) -> &'static str {
"reload-probe"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
true
}
fn load(
&self,
_manifest: &ManifestData,
_source: &polyplug::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
Ok(())
}
}
#[test]
fn reload_with_tampered_manifest_id_fails_and_fires_failed_callback() {
let temp: tempfile::TempDir = tempfile::TempDir::new().expect("temp dir");
let bundle_dir: PathBuf = temp.path().join("tampered");
std::fs::create_dir_all(&bundle_dir).expect("create dir");
std::fs::write(bundle_dir.join("plugin.so"), b"").expect("write so");
let bogus_id: u64 = polyplug_utils::bundle_id("tampered").wrapping_add(1);
let manifest: String = format!(
"id = {bogus_id}\n\
name = \"tampered\"\n\
loader = \"reload-probe\"\n\
file = \"plugin.so\"\n\
version = \"1.0\"\n"
);
std::fs::write(bundle_dir.join("manifest.toml"), manifest).expect("write manifest");
let failed_fired: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
let failed_fired_cb: Arc<Mutex<bool>> = Arc::clone(&failed_fired);
let config: RuntimeConfig = RuntimeConfig {
compatibility: Compatibility::Strict,
hot_reload_enabled: true,
on_reload: None,
on_reload_user_data: core::ptr::null_mut(),
..Default::default()
};
let runtime: Arc<Runtime> = Runtime::builder()
.config(config)
.loader(NeverLoadsLoader)
.on_reload(move |_ud, phase| {
if phase.phase_type == ReloadPhaseType::Failed {
*failed_fired_cb.lock().unwrap() = true;
}
})
.build()
.expect("runtime build");
let plugin_path: PathBuf = bundle_dir.join("plugin.so");
let result: Result<(), RuntimeError> = runtime.reload_bundle(plugin_path.as_path());
match result {
Err(RuntimeError::Loader(polyplug::error::LoaderError::BundleTampered { .. })) => {}
other => panic!("expected BundleTampered on reload of tampered manifest, got {other:?}"),
}
assert!(
*failed_fired.lock().unwrap(),
"reload of a tampered manifest must fire the Failed callback"
);
}
fn dep_with_contract_id(
contract: &str,
min_version: &str,
contract_id: GuestContractId,
) -> polyplug::loader::RawManifestDependency {
polyplug::loader::RawManifestDependency {
kind: "contract".to_owned(),
contract: contract.to_owned(),
min_version: min_version.to_owned(),
bundle: None,
contract_id,
bundle_id: None,
}
}
#[test]
fn validate_accepts_matching_dependency_contract_id() {
let mut m: ManifestData = manifest_with("dep_match", Vec::new(), Vec::new());
let correct: GuestContractId = GuestContractId::new("math", 1);
m.dependencies = vec![dep_with_contract_id("math", "1.0", correct)];
assert!(
m.validate().is_ok(),
"validate must accept a dependency whose contract_id matches the canonical hash"
);
}
#[test]
fn validate_rejects_mismatched_dependency_contract_id() {
let mut m: ManifestData = manifest_with("dep_mismatch", Vec::new(), Vec::new());
let wrong: GuestContractId =
GuestContractId::from_u64(GuestContractId::new("math", 1).id().wrapping_add(7));
m.dependencies = vec![dep_with_contract_id("math", "1.0", wrong)];
match m.validate() {
Err(polyplug::error::LoaderError::ManifestParse { reason, .. }) => {
assert!(
reason.contains("contract_id"),
"mismatch error must mention contract_id: {reason}"
);
}
other => panic!("expected ManifestParse for mismatched contract_id, got {other:?}"),
}
}
#[test]
fn validate_accepts_absent_dependency_contract_id() {
let mut m: ManifestData = manifest_with("dep_absent", Vec::new(), Vec::new());
m.dependencies = vec![dep_with_contract_id(
"math",
"1.0",
GuestContractId::default(),
)];
assert!(
m.validate().is_ok(),
"validate must accept a dependency with an absent (0) contract_id"
);
}
#[test]
fn register_guest_contract_duplicate_returns_duplicate_provider_code() {
let runtime: Arc<Runtime> = build_bare_runtime();
let host_abi: *const HostApi = runtime.host_abi();
let contract_id: u64 = GuestContractId::new("dup.contract", 1).id();
let interface: &'static GuestContractInterface = leak_guest_interface(contract_id);
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"dup-plugin"),
contract_name: StringView::from_static(b"dup.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
runtime.push_init_bundle_id(0xD0D0_u64);
let mut first: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe {
((*host_abi).register_guest_contract)(
host_abi,
&descriptor as *const PluginDescriptor,
interface as *const GuestContractInterface,
&mut first,
)
};
assert_eq!(first.code, AbiErrorCode::Ok as u32, "first must register");
let mut second: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe {
((*host_abi).register_guest_contract)(
host_abi,
&descriptor as *const PluginDescriptor,
interface as *const GuestContractInterface,
&mut second,
)
};
runtime.pop_init_bundle_id();
assert_eq!(
second.code,
AbiErrorCode::DuplicateProvider as u32,
"same-bundle duplicate must return DuplicateProvider, not Generic"
);
let len: usize = unsafe { ((*host_abi).get_error_len)(host_abi) };
assert!(len > 0, "last error must be set on registration failure");
let mut buf: Vec<u8> = vec![0_u8; len];
let written: usize = unsafe { ((*host_abi).get_last_error)(host_abi, buf.as_mut_ptr(), len) };
let msg: String = String::from_utf8_lossy(&buf[..written]).into_owned();
assert!(
msg.contains("duplicate provider"),
"last error must carry the registry detail, got: {msg}"
);
}