1#![allow(unsafe_code)]
33
34use std::fmt;
35use std::mem::ManuallyDrop;
36use std::panic::{AssertUnwindSafe, catch_unwind};
37
38use crate::is_valid_name;
39
40pub const ABI_MAJOR: u32 = 1;
59pub const ABI_MINOR: u32 = 0;
74
75pub const fn is_supported(major: u32, minor: u32) -> bool {
92 let _ = minor;
95 major == ABI_MAJOR
96}
97
98pub const DYLIB_ENTRY_SYMBOL: &str = "rich_plugin_entry";
100
101pub const MAX_CAPABILITIES: usize = 64;
103pub const MAX_FIELD_LEN: usize = 1024;
105
106pub const STATUS_OK: u32 = 0;
108pub const STATUS_ERROR: u32 = 1;
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
114#[non_exhaustive]
115pub enum CapabilityKind {
116 Transform,
119 Highlighter,
124 FenceMarkup,
126 FenceAnsi,
129}
130
131impl CapabilityKind {
132 pub const ALL: [CapabilityKind; 4] = [
134 CapabilityKind::Transform,
135 CapabilityKind::Highlighter,
136 CapabilityKind::FenceMarkup,
137 CapabilityKind::FenceAnsi,
138 ];
139
140 pub const fn code(self) -> u32 {
142 match self {
143 CapabilityKind::Transform => 1,
144 CapabilityKind::Highlighter => 2,
145 CapabilityKind::FenceMarkup => 3,
146 CapabilityKind::FenceAnsi => 4,
147 }
148 }
149
150 pub fn from_code(code: u32) -> Option<Self> {
152 Self::ALL.into_iter().find(|kind| kind.code() == code)
153 }
154
155 pub const fn as_str(self) -> &'static str {
157 match self {
158 CapabilityKind::Transform => "transform",
159 CapabilityKind::Highlighter => "highlighter",
160 CapabilityKind::FenceMarkup => "fence-markup",
161 CapabilityKind::FenceAnsi => "fence-ansi",
162 }
163 }
164
165 pub fn parse(name: &str) -> Option<Self> {
167 Self::ALL.into_iter().find(|kind| kind.as_str() == name)
168 }
169
170 pub const fn produces_ansi(self) -> bool {
172 matches!(self, CapabilityKind::FenceAnsi)
173 }
174}
175
176impl fmt::Display for CapabilityKind {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(self.as_str())
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
184pub struct AbiCapability {
185 pub kind: CapabilityKind,
187 pub name: String,
189}
190
191#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct PluginAbi {
196 pub abi_major: u32,
198 pub abi_minor: u32,
200 pub name: String,
202 pub version: String,
204 pub description: String,
206 pub capabilities: Vec<AbiCapability>,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq)]
212pub enum AbiError {
213 Incompatible {
215 major: u32,
217 minor: u32,
219 },
220 Invalid(String),
222}
223
224impl fmt::Display for AbiError {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 match self {
227 AbiError::Incompatible { major, minor } => write!(
228 f,
229 "built for plugin ABI {major}.{minor}, but this host supports ABI \
230 {ABI_MAJOR}.x; rebuild it against rs-rich-plugin-api with ABI {ABI_MAJOR}"
231 ),
232 AbiError::Invalid(message) => f.write_str(message),
233 }
234 }
235}
236
237impl std::error::Error for AbiError {}
238
239const MANIFEST_MAGIC: &str = "rich-plugin-abi";
241
242impl PluginAbi {
243 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
245 PluginAbi {
246 abi_major: ABI_MAJOR,
247 abi_minor: ABI_MINOR,
248 name: name.into(),
249 version: version.into(),
250 description: String::new(),
251 capabilities: Vec::new(),
252 }
253 }
254
255 pub fn validate(&self) -> Result<(), AbiError> {
257 if !is_supported(self.abi_major, self.abi_minor) {
258 return Err(AbiError::Incompatible {
259 major: self.abi_major,
260 minor: self.abi_minor,
261 });
262 }
263 if !is_valid_name(&self.name) {
264 return Err(AbiError::Invalid(format!(
265 "invalid plugin name {:?}: use lowercase letters, digits, '-', '_' and '.'",
266 self.name
267 )));
268 }
269 for (field, value) in [
270 ("version", &self.version),
271 ("description", &self.description),
272 ] {
273 if value.len() > MAX_FIELD_LEN || value.chars().any(char::is_control) {
274 return Err(AbiError::Invalid(format!(
275 "the {field} must be one line of at most {MAX_FIELD_LEN} bytes"
276 )));
277 }
278 }
279 if self.version.is_empty() {
280 return Err(AbiError::Invalid("the version is empty".into()));
281 }
282 if self.capabilities.len() > MAX_CAPABILITIES {
283 return Err(AbiError::Invalid(format!(
284 "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
285 self.capabilities.len()
286 )));
287 }
288 for (i, capability) in self.capabilities.iter().enumerate() {
289 if !is_valid_name(&capability.name) {
290 return Err(AbiError::Invalid(format!(
291 "invalid {} name {:?}",
292 capability.kind, capability.name
293 )));
294 }
295 if self.capabilities[..i].contains(capability) {
296 return Err(AbiError::Invalid(format!(
297 "{} {:?} is declared twice",
298 capability.kind, capability.name
299 )));
300 }
301 }
302 Ok(())
303 }
304
305 pub fn to_manifest(&self) -> String {
317 let mut out = format!(
318 "{MANIFEST_MAGIC} {}.{}\nname {}\nversion {}\n",
319 self.abi_major, self.abi_minor, self.name, self.version
320 );
321 if !self.description.is_empty() {
322 out.push_str(&format!("description {}\n", self.description));
323 }
324 for capability in &self.capabilities {
325 out.push_str(&format!(
326 "capability {} {}\n",
327 capability.kind, capability.name
328 ));
329 }
330 out
331 }
332
333 pub fn parse_manifest(text: &str) -> Result<PluginAbi, AbiError> {
337 let mut lines = text.lines();
338 let first = lines.next().unwrap_or_default();
339 let version = first
340 .strip_prefix(MANIFEST_MAGIC)
341 .and_then(|rest| rest.strip_prefix(' '))
342 .ok_or_else(|| {
343 AbiError::Invalid(format!(
344 "the manifest must start with `{MANIFEST_MAGIC} MAJOR.MINOR`"
345 ))
346 })?;
347 let (major, minor) = version
348 .split_once('.')
349 .and_then(|(major, minor)| Some((major.parse().ok()?, minor.parse().ok()?)))
350 .ok_or_else(|| AbiError::Invalid(format!("invalid ABI version {version:?}")))?;
351 if !is_supported(major, minor) {
352 return Err(AbiError::Incompatible { major, minor });
353 }
354 let mut abi = PluginAbi {
355 abi_major: major,
356 abi_minor: minor,
357 name: String::new(),
358 version: String::new(),
359 description: String::new(),
360 capabilities: Vec::new(),
361 };
362 for line in lines {
363 if line.trim().is_empty() {
364 continue;
365 }
366 let (key, value) = line.split_once(' ').unwrap_or((line, ""));
367 match key {
368 "name" => abi.name = value.to_string(),
369 "version" => abi.version = value.to_string(),
370 "description" => abi.description = value.to_string(),
371 "capability" => {
372 let (kind, name) = value.split_once(' ').ok_or_else(|| {
373 AbiError::Invalid(format!("expected `capability KIND NAME`: {line:?}"))
374 })?;
375 let kind = CapabilityKind::parse(kind).ok_or_else(|| {
376 AbiError::Invalid(format!(
377 "unknown capability kind {kind:?}; this host knows {}",
378 CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
379 ))
380 })?;
381 abi.capabilities.push(AbiCapability {
382 kind,
383 name: name.to_string(),
384 });
385 }
386 _ => {}
387 }
388 }
389 abi.validate()?;
390 Ok(abi)
391 }
392}
393
394pub mod wasm {
406 pub const MEMORY: &str = "memory";
408 pub const ALLOC: &str = "rich_plugin_alloc";
410 pub const MANIFEST: &str = "rich_plugin_manifest";
412 pub const CALL: &str = "rich_plugin_call";
414 pub const ERROR_BIT: u64 = 1 << 63;
416}
417
418#[repr(C)]
424#[derive(Clone, Copy, Debug)]
425pub struct AbiStr {
426 pub ptr: *const u8,
428 pub len: usize,
430}
431
432impl AbiStr {
433 pub fn new(text: &str) -> Self {
435 AbiStr {
436 ptr: text.as_ptr(),
437 len: text.len(),
438 }
439 }
440
441 pub unsafe fn as_str<'a>(self) -> Result<&'a str, AbiError> {
448 if self.ptr.is_null() {
449 return if self.len == 0 {
450 Ok("")
451 } else {
452 Err(AbiError::Invalid("a null string with a length".into()))
453 };
454 }
455 let bytes = unsafe { std::slice::from_raw_parts(self.ptr, self.len) };
457 std::str::from_utf8(bytes).map_err(|_| AbiError::Invalid("a string is not UTF-8".into()))
458 }
459}
460
461#[repr(C)]
464#[derive(Clone, Copy, Debug)]
465pub struct AbiCapabilityEntry {
466 pub kind: u32,
468 pub name: AbiStr,
470}
471
472#[repr(C)]
475#[derive(Debug)]
476pub struct AbiOutput {
477 pub ptr: *mut u8,
479 pub len: usize,
481 pub cap: usize,
483}
484
485impl AbiOutput {
486 pub const fn empty() -> Self {
488 AbiOutput {
489 ptr: std::ptr::null_mut(),
490 len: 0,
491 cap: 0,
492 }
493 }
494
495 pub fn from_string(text: String) -> Self {
497 let mut bytes = ManuallyDrop::new(text.into_bytes());
498 AbiOutput {
499 ptr: bytes.as_mut_ptr(),
500 len: bytes.len(),
501 cap: bytes.capacity(),
502 }
503 }
504}
505
506pub type AbiCallFn = unsafe extern "C" fn(
510 capability: usize,
511 input: AbiStr,
512 width: u32,
513 output: *mut AbiOutput,
514) -> u32;
515
516pub type AbiFreeFn = unsafe extern "C" fn(output: AbiOutput);
518
519#[repr(C)]
523#[derive(Clone, Copy, Debug)]
524pub struct PluginVTable {
525 pub call: Option<AbiCallFn>,
527 pub free: Option<AbiFreeFn>,
529}
530
531#[derive(Clone, Copy, Debug)]
533pub struct PluginFunctions {
534 pub call: AbiCallFn,
536 pub free: AbiFreeFn,
538}
539
540#[repr(C)]
544#[derive(Debug)]
545pub struct PluginDescriptor {
546 pub abi_major: u32,
548 pub abi_minor: u32,
550 pub name: AbiStr,
552 pub version: AbiStr,
554 pub description: AbiStr,
556 pub capabilities: *const AbiCapabilityEntry,
558 pub capability_count: usize,
560 pub vtable: PluginVTable,
562}
563
564pub unsafe fn read_descriptor(
575 descriptor: *const PluginDescriptor,
576) -> Result<(PluginAbi, PluginFunctions), AbiError> {
577 if descriptor.is_null() {
578 return Err(AbiError::Invalid(format!(
579 "{DYLIB_ENTRY_SYMBOL} returned no descriptor (it failed to initialise)"
580 )));
581 }
582 let (major, minor) = unsafe {
584 let version = descriptor.cast::<u32>();
585 (version.read(), version.add(1).read())
586 };
587 if !is_supported(major, minor) {
588 return Err(AbiError::Incompatible { major, minor });
589 }
590 let descriptor = unsafe { &*descriptor };
592 if descriptor.capability_count > MAX_CAPABILITIES {
593 return Err(AbiError::Invalid(format!(
594 "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
595 descriptor.capability_count
596 )));
597 }
598 let entries: &[AbiCapabilityEntry] = if descriptor.capability_count == 0 {
599 &[]
600 } else if descriptor.capabilities.is_null() {
601 return Err(AbiError::Invalid("a null capability list".into()));
602 } else {
603 unsafe { std::slice::from_raw_parts(descriptor.capabilities, descriptor.capability_count) }
605 };
606 let text = |s: AbiStr, what: &str| -> Result<String, AbiError> {
607 if s.len > MAX_FIELD_LEN {
608 return Err(AbiError::Invalid(format!("the {what} is too long")));
609 }
610 unsafe { s.as_str() }.map(str::to_string)
612 };
613 let mut capabilities = Vec::with_capacity(entries.len());
614 for entry in entries {
615 let kind = CapabilityKind::from_code(entry.kind).ok_or_else(|| {
616 AbiError::Invalid(format!(
617 "unknown capability kind {}; this host knows {}",
618 entry.kind,
619 CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
620 ))
621 })?;
622 capabilities.push(AbiCapability {
623 kind,
624 name: text(entry.name, "capability name")?,
625 });
626 }
627 let abi = PluginAbi {
628 abi_major: major,
629 abi_minor: minor,
630 name: text(descriptor.name, "name")?,
631 version: text(descriptor.version, "version")?,
632 description: text(descriptor.description, "description")?,
633 capabilities,
634 };
635 abi.validate()?;
636 let (Some(call), Some(free)) = (descriptor.vtable.call, descriptor.vtable.free) else {
637 return Err(AbiError::Invalid(
638 "a null function in the vtable (`call` and `free` are required)".into(),
639 ));
640 };
641 Ok((abi, PluginFunctions { call, free }))
642}
643
644pub type ExportFn = fn(input: &str, width: u32) -> Result<String, String>;
651
652pub struct Exports {
655 abi: PluginAbi,
656 functions: Vec<ExportFn>,
657}
658
659impl Exports {
660 pub fn new(name: &str, version: &str) -> Self {
662 Exports {
663 abi: PluginAbi::new(name, version),
664 functions: Vec::new(),
665 }
666 }
667
668 #[must_use]
670 pub fn description(mut self, description: &str) -> Self {
671 self.abi.description = description.to_string();
672 self
673 }
674
675 #[must_use]
677 pub fn capability(mut self, kind: CapabilityKind, name: &str, function: ExportFn) -> Self {
678 self.abi.capabilities.push(AbiCapability {
679 kind,
680 name: name.to_string(),
681 });
682 self.functions.push(function);
683 self
684 }
685
686 #[must_use]
688 pub fn transform(self, name: &str, function: ExportFn) -> Self {
689 self.capability(CapabilityKind::Transform, name, function)
690 }
691
692 #[must_use]
694 pub fn highlighter(self, name: &str, function: ExportFn) -> Self {
695 self.capability(CapabilityKind::Highlighter, name, function)
696 }
697
698 #[must_use]
700 pub fn fence_markup(self, language: &str, function: ExportFn) -> Self {
701 self.capability(CapabilityKind::FenceMarkup, language, function)
702 }
703
704 #[must_use]
706 pub fn fence_ansi(self, language: &str, function: ExportFn) -> Self {
707 self.capability(CapabilityKind::FenceAnsi, language, function)
708 }
709
710 pub fn abi(&self) -> &PluginAbi {
712 &self.abi
713 }
714}
715
716#[doc(hidden)]
719pub struct Exported {
720 exports: Exports,
721 _entries: Vec<AbiCapabilityEntry>,
722 descriptor: PluginDescriptor,
723}
724
725unsafe impl Send for Exported {}
729unsafe impl Sync for Exported {}
731
732impl Exported {
733 pub fn new(exports: Exports, call: AbiCallFn) -> Self {
734 let entries: Vec<AbiCapabilityEntry> = exports
735 .abi
736 .capabilities
737 .iter()
738 .map(|capability| AbiCapabilityEntry {
739 kind: capability.kind.code(),
740 name: AbiStr::new(&capability.name),
741 })
742 .collect();
743 let descriptor = PluginDescriptor {
744 abi_major: exports.abi.abi_major,
745 abi_minor: exports.abi.abi_minor,
746 name: AbiStr::new(&exports.abi.name),
747 version: AbiStr::new(&exports.abi.version),
748 description: AbiStr::new(&exports.abi.description),
749 capabilities: entries.as_ptr(),
750 capability_count: entries.len(),
751 vtable: PluginVTable {
752 call: Some(call),
753 free: Some(free_output),
754 },
755 };
756 Exported {
757 exports,
758 _entries: entries,
759 descriptor,
760 }
761 }
762
763 pub fn descriptor(&'static self) -> *const PluginDescriptor {
764 &self.descriptor
765 }
766
767 pub unsafe fn call(
774 this: Option<&Exported>,
775 capability: usize,
776 input: AbiStr,
777 width: u32,
778 output: *mut AbiOutput,
779 ) -> u32 {
780 let result = catch_unwind(AssertUnwindSafe(|| {
781 let this = this.ok_or("the plugin was called before it was initialised")?;
782 let function = this
783 .exports
784 .functions
785 .get(capability)
786 .ok_or_else(|| format!("no capability number {capability}"))?;
787 let input = unsafe { input.as_str() }.map_err(|e| e.to_string())?;
789 function(input, width)
790 }));
791 let (status, text) = match result {
792 Ok(Ok(text)) => (STATUS_OK, text),
793 Ok(Err(message)) => (STATUS_ERROR, message),
794 Err(_) => (STATUS_ERROR, "the plugin panicked".to_string()),
795 };
796 if output.is_null() {
797 return STATUS_ERROR;
798 }
799 unsafe { output.write(AbiOutput::from_string(text)) };
801 status
802 }
803
804 pub fn entry(
807 slot: &'static std::sync::OnceLock<Exported>,
808 make: fn() -> Exports,
809 call: AbiCallFn,
810 ) -> *const PluginDescriptor {
811 match catch_unwind(AssertUnwindSafe(|| {
812 slot.get_or_init(|| Exported::new(make(), call))
813 })) {
814 Ok(exported) => exported.descriptor(),
815 Err(_) => std::ptr::null(),
816 }
817 }
818}
819
820pub unsafe extern "C" fn free_output(output: AbiOutput) {
827 if !output.ptr.is_null() {
828 drop(unsafe { Vec::from_raw_parts(output.ptr, output.len, output.cap) });
830 }
831}
832
833#[macro_export]
855macro_rules! export_dylib_plugin {
856 ($exports:expr) => {
857 #[doc(hidden)]
858 static __RICH_PLUGIN_EXPORTED: ::std::sync::OnceLock<$crate::abi::Exported> =
859 ::std::sync::OnceLock::new();
860
861 #[doc(hidden)]
862 #[allow(unsafe_code)]
863 unsafe extern "C" fn __rich_plugin_call(
864 capability: usize,
865 input: $crate::abi::AbiStr,
866 width: u32,
867 output: *mut $crate::abi::AbiOutput,
868 ) -> u32 {
869 unsafe {
871 $crate::abi::Exported::call(
872 __RICH_PLUGIN_EXPORTED.get(),
873 capability,
874 input,
875 width,
876 output,
877 )
878 }
879 }
880
881 #[allow(unsafe_code)]
883 #[unsafe(no_mangle)]
884 pub extern "C" fn rich_plugin_entry() -> *const $crate::abi::PluginDescriptor {
885 $crate::abi::Exported::entry(&__RICH_PLUGIN_EXPORTED, $exports, __rich_plugin_call)
886 }
887 };
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 fn upper(input: &str, _: u32) -> Result<String, String> {
895 Ok(input.to_uppercase())
896 }
897
898 fn fails(_: &str, _: u32) -> Result<String, String> {
899 Err("no".into())
900 }
901
902 fn panics(_: &str, _: u32) -> Result<String, String> {
903 panic!("boom")
904 }
905
906 fn exports() -> Exports {
907 Exports::new("shout", "0.1.0")
908 .description("Upper-cases text")
909 .transform("upper", upper)
910 .fence_markup("shout", fails)
911 .highlighter("boom", panics)
912 }
913
914 crate::export_dylib_plugin!(exports);
915
916 unsafe fn call(descriptor: &PluginFunctions, capability: usize, input: &str) -> (u32, String) {
917 let mut output = AbiOutput::empty();
918 let status = unsafe { (descriptor.call)(capability, AbiStr::new(input), 80, &mut output) };
919 let text = unsafe { std::slice::from_raw_parts(output.ptr, output.len) };
920 let text = String::from_utf8(text.to_vec()).unwrap();
921 unsafe { (descriptor.free)(output) };
922 (status, text)
923 }
924
925 fn versions_around_the_host() -> Vec<(u32, u32)> {
928 let mut versions = vec![
929 (ABI_MAJOR, ABI_MINOR),
930 (ABI_MAJOR, ABI_MINOR + 1),
931 (ABI_MAJOR + 1, 0),
932 (ABI_MAJOR + 1, ABI_MINOR),
933 ];
934 if let Some(older) = ABI_MINOR.checked_sub(1) {
935 versions.push((ABI_MAJOR, older));
936 }
937 if let Some(older) = ABI_MAJOR.checked_sub(1) {
938 versions.push((older, ABI_MINOR));
939 }
940 versions
941 }
942
943 #[test]
944 fn the_published_constants_are_the_ones_every_loader_checks() {
945 assert!(is_supported(ABI_MAJOR, ABI_MINOR));
949 let current = PluginAbi::new("shout", "1");
950 assert_eq!(
951 (current.abi_major, current.abi_minor),
952 (ABI_MAJOR, ABI_MINOR)
953 );
954 assert!(
955 current
956 .to_manifest()
957 .starts_with(&format!("rich-plugin-abi {ABI_MAJOR}.{ABI_MINOR}\n"))
958 );
959 for (major, minor) in versions_around_the_host() {
960 let expected = is_supported(major, minor);
961 let manifest = format!("rich-plugin-abi {major}.{minor}\nname shout\nversion 1\n");
962 let parsed = PluginAbi::parse_manifest(&manifest);
963 assert_eq!(parsed.is_ok(), expected, "manifest {major}.{minor}");
964 if !expected {
965 assert_eq!(parsed.unwrap_err(), AbiError::Incompatible { major, minor });
966 }
967
968 let mut abi = PluginAbi::new("shout", "1");
969 (abi.abi_major, abi.abi_minor) = (major, minor);
970 assert_eq!(abi.validate().is_ok(), expected, "validate {major}.{minor}");
971
972 let mut exported = Exported::new(exports(), __rich_plugin_call);
973 (exported.descriptor.abi_major, exported.descriptor.abi_minor) = (major, minor);
974 let read = unsafe { read_descriptor(&exported.descriptor) };
975 assert_eq!(read.is_ok(), expected, "descriptor {major}.{minor}");
976 }
977 }
978
979 #[test]
980 fn a_descriptor_round_trips_through_the_c_abi() {
981 let (abi, vtable) = unsafe { read_descriptor(rich_plugin_entry()) }.unwrap();
982 assert_eq!(abi, *exports().abi());
983 assert_eq!(abi.capabilities[1].kind, CapabilityKind::FenceMarkup);
984 assert_eq!(
985 unsafe { call(&vtable, 0, "hi") },
986 (STATUS_OK, "HI".to_string())
987 );
988 assert_eq!(
989 unsafe { call(&vtable, 1, "hi") },
990 (STATUS_ERROR, "no".to_string())
991 );
992 let (status, message) = unsafe { call(&vtable, 2, "hi") };
993 assert_eq!(status, STATUS_ERROR);
994 assert!(message.contains("panicked"), "{message}");
995 let (status, message) = unsafe { call(&vtable, 9, "hi") };
996 assert_eq!(status, STATUS_ERROR);
997 assert!(message.contains("no capability"), "{message}");
998 }
999
1000 #[test]
1001 fn another_major_is_refused_before_the_rest_is_read() {
1002 let words: [u32; 2] = [ABI_MAJOR + 1, 0];
1005 let error = unsafe { read_descriptor(words.as_ptr().cast()) }.unwrap_err();
1006 assert_eq!(
1007 error,
1008 AbiError::Incompatible {
1009 major: ABI_MAJOR + 1,
1010 minor: 0
1011 }
1012 );
1013 assert!(error.to_string().contains("rebuild"));
1014 assert!(unsafe { read_descriptor(std::ptr::null()) }.is_err());
1015 }
1016
1017 #[test]
1018 fn a_null_function_in_the_vtable_is_refused() {
1019 for (call, free) in [(false, true), (true, false), (false, false)] {
1021 let mut exported = Exported::new(exports(), __rich_plugin_call);
1022 let vtable = &mut exported.descriptor.vtable;
1023 if !call {
1024 vtable.call = None;
1025 }
1026 if !free {
1027 vtable.free = None;
1028 }
1029 let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
1030 assert!(error.to_string().contains("null function"), "{error}");
1031 }
1032 }
1033
1034 #[test]
1035 fn a_newer_minor_loads_but_an_unknown_kind_does_not() {
1036 let mut exported = Exported::new(exports(), __rich_plugin_call);
1037 exported.descriptor.abi_minor = ABI_MINOR + 3;
1038 let (abi, _) = unsafe { read_descriptor(&exported.descriptor) }.unwrap();
1039 assert_eq!(abi.abi_minor, ABI_MINOR + 3);
1040 let entries = [AbiCapabilityEntry {
1041 kind: 99,
1042 name: AbiStr::new("x"),
1043 }];
1044 exported.descriptor.capabilities = entries.as_ptr();
1045 exported.descriptor.capability_count = 1;
1046 let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
1047 assert!(error.to_string().contains("unknown capability kind 99"));
1048 }
1049
1050 #[test]
1051 fn manifests_round_trip_and_are_checked() {
1052 let abi = exports().abi().clone();
1053 let manifest = abi.to_manifest();
1054 assert!(manifest.starts_with("rich-plugin-abi 1.0\nname shout\n"));
1055 assert_eq!(PluginAbi::parse_manifest(&manifest).unwrap(), abi);
1056 let extra = manifest.replace("name shout", "name shout\nhomepage x");
1058 assert_eq!(PluginAbi::parse_manifest(&extra).unwrap(), abi);
1059 let other = manifest.replace("rich-plugin-abi 1.0", "rich-plugin-abi 2.0");
1060 assert_eq!(
1061 PluginAbi::parse_manifest(&other),
1062 Err(AbiError::Incompatible { major: 2, minor: 0 })
1063 );
1064 for bad in [
1065 "",
1066 "hello",
1067 "rich-plugin-abi one",
1068 "rich-plugin-abi 1.0\nname Bad\nversion 1",
1069 "rich-plugin-abi 1.0\nname ok\n",
1070 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability sing x",
1071 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform X",
1072 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform x\ncapability transform x",
1073 ] {
1074 assert!(PluginAbi::parse_manifest(bad).is_err(), "{bad:?}");
1075 }
1076 }
1077}