1use abi_stable::library::{IsLayoutChecked, LibHeader, LibraryError, RootModule, RootModuleError};
2use abi_stable::{abi_stability::abi_checking::check_layout_compatibility, library::lib_header_from_path};
3use abi_stable::{sabi_types::VersionNumber, std_types::RBoxError, type_layout::TypeLayout};
4use gravel_ffi::PluginDefinition;
5use std::{collections::HashMap, path::Path};
6
7#[derive(Default)]
9pub struct PluginRegistry {
10 plugins: HashMap<String, PluginDefinition>,
11}
12
13impl PluginRegistry {
14 pub fn register(&mut self, plugin: PluginDefinition) -> &mut Self {
20 let name = plugin.meta.name.as_str();
21
22 if self.plugins.contains_key(name) {
23 log::warn!("attempted to register duplicate plugin '{}', skipping", name);
24 return self;
25 }
26
27 log::trace!("registered plugin '{name}'");
28
29 self.plugins.insert(name.to_owned(), plugin);
30 self
31 }
32
33 #[must_use]
34 pub fn get(&self, name: &str) -> Option<&PluginDefinition> {
35 self.plugins.get(name)
36 }
37}
38
39pub fn load_library_from_path<M: RootModule>(path: &Path) -> Result<M, LibraryError> {
40 let header = lib_header_from_path(path)?;
41 check_module_version::<M>(header)?;
42
43 if let IsLayoutChecked::Yes(layout) = header.root_mod_consts().layout() {
44 check_layout::<M>(layout)?;
45 };
46
47 let lib = unsafe { header.unchecked_layout::<M>() }.map_err(RootModuleError::into_library_error::<M>)?;
48 Ok(lib)
49}
50
51fn check_layout<M: RootModule>(plugin_layout: &'static TypeLayout) -> Result<(), LibraryError> {
52 let application_layout = M::LAYOUT;
53
54 check_layout_compatibility(plugin_layout, application_layout).map_err(|e| {
61 let formatted = RBoxError::new(e).to_formatted_error();
62 LibraryError::AbiInstability(formatted)
63 })
64}
65
66fn check_module_version<M: RootModule>(header: &'static LibHeader) -> Result<(), LibraryError> {
67 let app_version = VersionNumber::new(M::VERSION_STRINGS)?;
68 let plugin_version = VersionNumber::new(header.version_strings())?;
69
70 if !are_compatible(&app_version, &plugin_version) {
71 return Err(LibraryError::IncompatibleVersionNumber {
72 library_name: M::NAME,
73 expected_version: app_version,
74 actual_version: plugin_version,
75 });
76 }
77
78 Ok(())
79}
80
81fn are_compatible(app: &VersionNumber, plugin: &VersionNumber) -> bool {
83 app.major == plugin.major
84 && app.minor >= plugin.minor
85 && (app.major != 0 || app.minor == plugin.minor)
86 && (app.minor != plugin.minor || app.patch >= plugin.patch)
87}
88
89#[cfg(test)]
90mod test {
91 use super::*;
92 use abi_stable::sabi_types::VersionStrings;
93 use rstest::rstest;
94
95 #[rstest]
96 #[case("1.0.0", "1.0.0", true)]
98 #[case("1.0.0", "2.0.0", false)]
99 #[case("1.1.0", "1.1.0", true)]
101 #[case("1.2.0", "1.1.0", true)]
102 #[case("1.0.0", "1.1.0", false)]
103 #[case("1.0.7", "1.1.5", false)]
104 #[case("1.0.1", "1.0.0", true)]
106 #[case("1.1.0", "1.0.4", true)]
107 #[case("1.0.2", "1.0.4", false)]
108 #[case("0.1.0", "0.1.0", true)]
110 #[case("0.2.0", "0.1.0", false)]
111 #[case("0.0.0", "0.1.0", false)]
112 #[case("0.0.7", "0.1.5", false)]
113 #[case("0.0.1", "0.0.0", true)]
115 #[case("0.3.4", "0.3.1", true)]
116 #[case("0.1.0", "0.0.4", false)]
117 #[case("0.0.2", "0.0.4", false)]
118 fn version_compatibility(
119 #[case] application: &'static str,
120 #[case] plugin: &'static str,
121 #[case] compatible: bool,
122 ) {
123 let app_version = VersionStrings::new(application).parsed().expect("input error");
124 let plugin_version = VersionStrings::new(plugin).parsed().expect("input error");
125
126 assert_eq!(compatible, are_compatible(&app_version, &plugin_version));
127 }
128}