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,
186 pub name: String,
188}
189
190#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct PluginAbi {
195 pub abi_major: u32,
196 pub abi_minor: u32,
197 pub name: String,
199 pub version: String,
200 pub description: String,
201 pub capabilities: Vec<AbiCapability>,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207pub enum AbiError {
208 Incompatible { major: u32, minor: u32 },
210 Invalid(String),
212}
213
214impl fmt::Display for AbiError {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 match self {
217 AbiError::Incompatible { major, minor } => write!(
218 f,
219 "built for plugin ABI {major}.{minor}, but this host supports ABI \
220 {ABI_MAJOR}.x; rebuild it against rs-rich-plugin-api with ABI {ABI_MAJOR}"
221 ),
222 AbiError::Invalid(message) => f.write_str(message),
223 }
224 }
225}
226
227impl std::error::Error for AbiError {}
228
229const MANIFEST_MAGIC: &str = "rich-plugin-abi";
231
232impl PluginAbi {
233 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
235 PluginAbi {
236 abi_major: ABI_MAJOR,
237 abi_minor: ABI_MINOR,
238 name: name.into(),
239 version: version.into(),
240 description: String::new(),
241 capabilities: Vec::new(),
242 }
243 }
244
245 pub fn validate(&self) -> Result<(), AbiError> {
247 if !is_supported(self.abi_major, self.abi_minor) {
248 return Err(AbiError::Incompatible {
249 major: self.abi_major,
250 minor: self.abi_minor,
251 });
252 }
253 if !is_valid_name(&self.name) {
254 return Err(AbiError::Invalid(format!(
255 "invalid plugin name {:?}: use lowercase letters, digits, '-', '_' and '.'",
256 self.name
257 )));
258 }
259 for (field, value) in [
260 ("version", &self.version),
261 ("description", &self.description),
262 ] {
263 if value.len() > MAX_FIELD_LEN || value.chars().any(char::is_control) {
264 return Err(AbiError::Invalid(format!(
265 "the {field} must be one line of at most {MAX_FIELD_LEN} bytes"
266 )));
267 }
268 }
269 if self.version.is_empty() {
270 return Err(AbiError::Invalid("the version is empty".into()));
271 }
272 if self.capabilities.len() > MAX_CAPABILITIES {
273 return Err(AbiError::Invalid(format!(
274 "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
275 self.capabilities.len()
276 )));
277 }
278 for (i, capability) in self.capabilities.iter().enumerate() {
279 if !is_valid_name(&capability.name) {
280 return Err(AbiError::Invalid(format!(
281 "invalid {} name {:?}",
282 capability.kind, capability.name
283 )));
284 }
285 if self.capabilities[..i].contains(capability) {
286 return Err(AbiError::Invalid(format!(
287 "{} {:?} is declared twice",
288 capability.kind, capability.name
289 )));
290 }
291 }
292 Ok(())
293 }
294
295 pub fn to_manifest(&self) -> String {
307 let mut out = format!(
308 "{MANIFEST_MAGIC} {}.{}\nname {}\nversion {}\n",
309 self.abi_major, self.abi_minor, self.name, self.version
310 );
311 if !self.description.is_empty() {
312 out.push_str(&format!("description {}\n", self.description));
313 }
314 for capability in &self.capabilities {
315 out.push_str(&format!(
316 "capability {} {}\n",
317 capability.kind, capability.name
318 ));
319 }
320 out
321 }
322
323 pub fn parse_manifest(text: &str) -> Result<PluginAbi, AbiError> {
327 let mut lines = text.lines();
328 let first = lines.next().unwrap_or_default();
329 let version = first
330 .strip_prefix(MANIFEST_MAGIC)
331 .and_then(|rest| rest.strip_prefix(' '))
332 .ok_or_else(|| {
333 AbiError::Invalid(format!(
334 "the manifest must start with `{MANIFEST_MAGIC} MAJOR.MINOR`"
335 ))
336 })?;
337 let (major, minor) = version
338 .split_once('.')
339 .and_then(|(major, minor)| Some((major.parse().ok()?, minor.parse().ok()?)))
340 .ok_or_else(|| AbiError::Invalid(format!("invalid ABI version {version:?}")))?;
341 if !is_supported(major, minor) {
342 return Err(AbiError::Incompatible { major, minor });
343 }
344 let mut abi = PluginAbi {
345 abi_major: major,
346 abi_minor: minor,
347 name: String::new(),
348 version: String::new(),
349 description: String::new(),
350 capabilities: Vec::new(),
351 };
352 for line in lines {
353 if line.trim().is_empty() {
354 continue;
355 }
356 let (key, value) = line.split_once(' ').unwrap_or((line, ""));
357 match key {
358 "name" => abi.name = value.to_string(),
359 "version" => abi.version = value.to_string(),
360 "description" => abi.description = value.to_string(),
361 "capability" => {
362 let (kind, name) = value.split_once(' ').ok_or_else(|| {
363 AbiError::Invalid(format!("expected `capability KIND NAME`: {line:?}"))
364 })?;
365 let kind = CapabilityKind::parse(kind).ok_or_else(|| {
366 AbiError::Invalid(format!(
367 "unknown capability kind {kind:?}; this host knows {}",
368 CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
369 ))
370 })?;
371 abi.capabilities.push(AbiCapability {
372 kind,
373 name: name.to_string(),
374 });
375 }
376 _ => {}
377 }
378 }
379 abi.validate()?;
380 Ok(abi)
381 }
382}
383
384pub mod wasm {
396 pub const MEMORY: &str = "memory";
397 pub const ALLOC: &str = "rich_plugin_alloc";
398 pub const MANIFEST: &str = "rich_plugin_manifest";
399 pub const CALL: &str = "rich_plugin_call";
400 pub const ERROR_BIT: u64 = 1 << 63;
402}
403
404#[repr(C)]
410#[derive(Clone, Copy, Debug)]
411pub struct AbiStr {
412 pub ptr: *const u8,
413 pub len: usize,
414}
415
416impl AbiStr {
417 pub fn new(text: &str) -> Self {
419 AbiStr {
420 ptr: text.as_ptr(),
421 len: text.len(),
422 }
423 }
424
425 pub unsafe fn as_str<'a>(self) -> Result<&'a str, AbiError> {
432 if self.ptr.is_null() {
433 return if self.len == 0 {
434 Ok("")
435 } else {
436 Err(AbiError::Invalid("a null string with a length".into()))
437 };
438 }
439 let bytes = unsafe { std::slice::from_raw_parts(self.ptr, self.len) };
441 std::str::from_utf8(bytes).map_err(|_| AbiError::Invalid("a string is not UTF-8".into()))
442 }
443}
444
445#[repr(C)]
448#[derive(Clone, Copy, Debug)]
449pub struct AbiCapabilityEntry {
450 pub kind: u32,
451 pub name: AbiStr,
452}
453
454#[repr(C)]
457#[derive(Debug)]
458pub struct AbiOutput {
459 pub ptr: *mut u8,
460 pub len: usize,
461 pub cap: usize,
462}
463
464impl AbiOutput {
465 pub const fn empty() -> Self {
467 AbiOutput {
468 ptr: std::ptr::null_mut(),
469 len: 0,
470 cap: 0,
471 }
472 }
473
474 pub fn from_string(text: String) -> Self {
476 let mut bytes = ManuallyDrop::new(text.into_bytes());
477 AbiOutput {
478 ptr: bytes.as_mut_ptr(),
479 len: bytes.len(),
480 cap: bytes.capacity(),
481 }
482 }
483}
484
485pub type AbiCallFn = unsafe extern "C" fn(
489 capability: usize,
490 input: AbiStr,
491 width: u32,
492 output: *mut AbiOutput,
493) -> u32;
494
495pub type AbiFreeFn = unsafe extern "C" fn(output: AbiOutput);
497
498#[repr(C)]
502#[derive(Clone, Copy, Debug)]
503pub struct PluginVTable {
504 pub call: Option<AbiCallFn>,
505 pub free: Option<AbiFreeFn>,
506}
507
508#[derive(Clone, Copy, Debug)]
510pub struct PluginFunctions {
511 pub call: AbiCallFn,
512 pub free: AbiFreeFn,
513}
514
515#[repr(C)]
519#[derive(Debug)]
520pub struct PluginDescriptor {
521 pub abi_major: u32,
522 pub abi_minor: u32,
523 pub name: AbiStr,
524 pub version: AbiStr,
525 pub description: AbiStr,
526 pub capabilities: *const AbiCapabilityEntry,
527 pub capability_count: usize,
528 pub vtable: PluginVTable,
529}
530
531pub unsafe fn read_descriptor(
542 descriptor: *const PluginDescriptor,
543) -> Result<(PluginAbi, PluginFunctions), AbiError> {
544 if descriptor.is_null() {
545 return Err(AbiError::Invalid(format!(
546 "{DYLIB_ENTRY_SYMBOL} returned no descriptor (it failed to initialise)"
547 )));
548 }
549 let (major, minor) = unsafe {
551 let version = descriptor.cast::<u32>();
552 (version.read(), version.add(1).read())
553 };
554 if !is_supported(major, minor) {
555 return Err(AbiError::Incompatible { major, minor });
556 }
557 let descriptor = unsafe { &*descriptor };
559 if descriptor.capability_count > MAX_CAPABILITIES {
560 return Err(AbiError::Invalid(format!(
561 "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
562 descriptor.capability_count
563 )));
564 }
565 let entries: &[AbiCapabilityEntry] = if descriptor.capability_count == 0 {
566 &[]
567 } else if descriptor.capabilities.is_null() {
568 return Err(AbiError::Invalid("a null capability list".into()));
569 } else {
570 unsafe { std::slice::from_raw_parts(descriptor.capabilities, descriptor.capability_count) }
572 };
573 let text = |s: AbiStr, what: &str| -> Result<String, AbiError> {
574 if s.len > MAX_FIELD_LEN {
575 return Err(AbiError::Invalid(format!("the {what} is too long")));
576 }
577 unsafe { s.as_str() }.map(str::to_string)
579 };
580 let mut capabilities = Vec::with_capacity(entries.len());
581 for entry in entries {
582 let kind = CapabilityKind::from_code(entry.kind).ok_or_else(|| {
583 AbiError::Invalid(format!(
584 "unknown capability kind {}; this host knows {}",
585 entry.kind,
586 CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
587 ))
588 })?;
589 capabilities.push(AbiCapability {
590 kind,
591 name: text(entry.name, "capability name")?,
592 });
593 }
594 let abi = PluginAbi {
595 abi_major: major,
596 abi_minor: minor,
597 name: text(descriptor.name, "name")?,
598 version: text(descriptor.version, "version")?,
599 description: text(descriptor.description, "description")?,
600 capabilities,
601 };
602 abi.validate()?;
603 let (Some(call), Some(free)) = (descriptor.vtable.call, descriptor.vtable.free) else {
604 return Err(AbiError::Invalid(
605 "a null function in the vtable (`call` and `free` are required)".into(),
606 ));
607 };
608 Ok((abi, PluginFunctions { call, free }))
609}
610
611pub type ExportFn = fn(input: &str, width: u32) -> Result<String, String>;
618
619pub struct Exports {
622 abi: PluginAbi,
623 functions: Vec<ExportFn>,
624}
625
626impl Exports {
627 pub fn new(name: &str, version: &str) -> Self {
629 Exports {
630 abi: PluginAbi::new(name, version),
631 functions: Vec::new(),
632 }
633 }
634
635 #[must_use]
637 pub fn description(mut self, description: &str) -> Self {
638 self.abi.description = description.to_string();
639 self
640 }
641
642 #[must_use]
644 pub fn capability(mut self, kind: CapabilityKind, name: &str, function: ExportFn) -> Self {
645 self.abi.capabilities.push(AbiCapability {
646 kind,
647 name: name.to_string(),
648 });
649 self.functions.push(function);
650 self
651 }
652
653 #[must_use]
655 pub fn transform(self, name: &str, function: ExportFn) -> Self {
656 self.capability(CapabilityKind::Transform, name, function)
657 }
658
659 #[must_use]
661 pub fn highlighter(self, name: &str, function: ExportFn) -> Self {
662 self.capability(CapabilityKind::Highlighter, name, function)
663 }
664
665 #[must_use]
667 pub fn fence_markup(self, language: &str, function: ExportFn) -> Self {
668 self.capability(CapabilityKind::FenceMarkup, language, function)
669 }
670
671 #[must_use]
673 pub fn fence_ansi(self, language: &str, function: ExportFn) -> Self {
674 self.capability(CapabilityKind::FenceAnsi, language, function)
675 }
676
677 pub fn abi(&self) -> &PluginAbi {
679 &self.abi
680 }
681}
682
683#[doc(hidden)]
686pub struct Exported {
687 exports: Exports,
688 _entries: Vec<AbiCapabilityEntry>,
689 descriptor: PluginDescriptor,
690}
691
692unsafe impl Send for Exported {}
696unsafe impl Sync for Exported {}
698
699impl Exported {
700 pub fn new(exports: Exports, call: AbiCallFn) -> Self {
701 let entries: Vec<AbiCapabilityEntry> = exports
702 .abi
703 .capabilities
704 .iter()
705 .map(|capability| AbiCapabilityEntry {
706 kind: capability.kind.code(),
707 name: AbiStr::new(&capability.name),
708 })
709 .collect();
710 let descriptor = PluginDescriptor {
711 abi_major: exports.abi.abi_major,
712 abi_minor: exports.abi.abi_minor,
713 name: AbiStr::new(&exports.abi.name),
714 version: AbiStr::new(&exports.abi.version),
715 description: AbiStr::new(&exports.abi.description),
716 capabilities: entries.as_ptr(),
717 capability_count: entries.len(),
718 vtable: PluginVTable {
719 call: Some(call),
720 free: Some(free_output),
721 },
722 };
723 Exported {
724 exports,
725 _entries: entries,
726 descriptor,
727 }
728 }
729
730 pub fn descriptor(&'static self) -> *const PluginDescriptor {
731 &self.descriptor
732 }
733
734 pub unsafe fn call(
741 this: Option<&Exported>,
742 capability: usize,
743 input: AbiStr,
744 width: u32,
745 output: *mut AbiOutput,
746 ) -> u32 {
747 let result = catch_unwind(AssertUnwindSafe(|| {
748 let this = this.ok_or("the plugin was called before it was initialised")?;
749 let function = this
750 .exports
751 .functions
752 .get(capability)
753 .ok_or_else(|| format!("no capability number {capability}"))?;
754 let input = unsafe { input.as_str() }.map_err(|e| e.to_string())?;
756 function(input, width)
757 }));
758 let (status, text) = match result {
759 Ok(Ok(text)) => (STATUS_OK, text),
760 Ok(Err(message)) => (STATUS_ERROR, message),
761 Err(_) => (STATUS_ERROR, "the plugin panicked".to_string()),
762 };
763 if output.is_null() {
764 return STATUS_ERROR;
765 }
766 unsafe { output.write(AbiOutput::from_string(text)) };
768 status
769 }
770
771 pub fn entry(
774 slot: &'static std::sync::OnceLock<Exported>,
775 make: fn() -> Exports,
776 call: AbiCallFn,
777 ) -> *const PluginDescriptor {
778 match catch_unwind(AssertUnwindSafe(|| {
779 slot.get_or_init(|| Exported::new(make(), call))
780 })) {
781 Ok(exported) => exported.descriptor(),
782 Err(_) => std::ptr::null(),
783 }
784 }
785}
786
787pub unsafe extern "C" fn free_output(output: AbiOutput) {
794 if !output.ptr.is_null() {
795 drop(unsafe { Vec::from_raw_parts(output.ptr, output.len, output.cap) });
797 }
798}
799
800#[macro_export]
822macro_rules! export_dylib_plugin {
823 ($exports:expr) => {
824 #[doc(hidden)]
825 static __RICH_PLUGIN_EXPORTED: ::std::sync::OnceLock<$crate::abi::Exported> =
826 ::std::sync::OnceLock::new();
827
828 #[doc(hidden)]
829 #[allow(unsafe_code)]
830 unsafe extern "C" fn __rich_plugin_call(
831 capability: usize,
832 input: $crate::abi::AbiStr,
833 width: u32,
834 output: *mut $crate::abi::AbiOutput,
835 ) -> u32 {
836 unsafe {
838 $crate::abi::Exported::call(
839 __RICH_PLUGIN_EXPORTED.get(),
840 capability,
841 input,
842 width,
843 output,
844 )
845 }
846 }
847
848 #[allow(unsafe_code)]
850 #[unsafe(no_mangle)]
851 pub extern "C" fn rich_plugin_entry() -> *const $crate::abi::PluginDescriptor {
852 $crate::abi::Exported::entry(&__RICH_PLUGIN_EXPORTED, $exports, __rich_plugin_call)
853 }
854 };
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860
861 fn upper(input: &str, _: u32) -> Result<String, String> {
862 Ok(input.to_uppercase())
863 }
864
865 fn fails(_: &str, _: u32) -> Result<String, String> {
866 Err("no".into())
867 }
868
869 fn panics(_: &str, _: u32) -> Result<String, String> {
870 panic!("boom")
871 }
872
873 fn exports() -> Exports {
874 Exports::new("shout", "0.1.0")
875 .description("Upper-cases text")
876 .transform("upper", upper)
877 .fence_markup("shout", fails)
878 .highlighter("boom", panics)
879 }
880
881 crate::export_dylib_plugin!(exports);
882
883 unsafe fn call(descriptor: &PluginFunctions, capability: usize, input: &str) -> (u32, String) {
884 let mut output = AbiOutput::empty();
885 let status = unsafe { (descriptor.call)(capability, AbiStr::new(input), 80, &mut output) };
886 let text = unsafe { std::slice::from_raw_parts(output.ptr, output.len) };
887 let text = String::from_utf8(text.to_vec()).unwrap();
888 unsafe { (descriptor.free)(output) };
889 (status, text)
890 }
891
892 fn versions_around_the_host() -> Vec<(u32, u32)> {
895 let mut versions = vec![
896 (ABI_MAJOR, ABI_MINOR),
897 (ABI_MAJOR, ABI_MINOR + 1),
898 (ABI_MAJOR + 1, 0),
899 (ABI_MAJOR + 1, ABI_MINOR),
900 ];
901 if let Some(older) = ABI_MINOR.checked_sub(1) {
902 versions.push((ABI_MAJOR, older));
903 }
904 if let Some(older) = ABI_MAJOR.checked_sub(1) {
905 versions.push((older, ABI_MINOR));
906 }
907 versions
908 }
909
910 #[test]
911 fn the_published_constants_are_the_ones_every_loader_checks() {
912 assert!(is_supported(ABI_MAJOR, ABI_MINOR));
916 let current = PluginAbi::new("shout", "1");
917 assert_eq!(
918 (current.abi_major, current.abi_minor),
919 (ABI_MAJOR, ABI_MINOR)
920 );
921 assert!(
922 current
923 .to_manifest()
924 .starts_with(&format!("rich-plugin-abi {ABI_MAJOR}.{ABI_MINOR}\n"))
925 );
926 for (major, minor) in versions_around_the_host() {
927 let expected = is_supported(major, minor);
928 let manifest = format!("rich-plugin-abi {major}.{minor}\nname shout\nversion 1\n");
929 let parsed = PluginAbi::parse_manifest(&manifest);
930 assert_eq!(parsed.is_ok(), expected, "manifest {major}.{minor}");
931 if !expected {
932 assert_eq!(parsed.unwrap_err(), AbiError::Incompatible { major, minor });
933 }
934
935 let mut abi = PluginAbi::new("shout", "1");
936 (abi.abi_major, abi.abi_minor) = (major, minor);
937 assert_eq!(abi.validate().is_ok(), expected, "validate {major}.{minor}");
938
939 let mut exported = Exported::new(exports(), __rich_plugin_call);
940 (exported.descriptor.abi_major, exported.descriptor.abi_minor) = (major, minor);
941 let read = unsafe { read_descriptor(&exported.descriptor) };
942 assert_eq!(read.is_ok(), expected, "descriptor {major}.{minor}");
943 }
944 }
945
946 #[test]
947 fn a_descriptor_round_trips_through_the_c_abi() {
948 let (abi, vtable) = unsafe { read_descriptor(rich_plugin_entry()) }.unwrap();
949 assert_eq!(abi, *exports().abi());
950 assert_eq!(abi.capabilities[1].kind, CapabilityKind::FenceMarkup);
951 assert_eq!(
952 unsafe { call(&vtable, 0, "hi") },
953 (STATUS_OK, "HI".to_string())
954 );
955 assert_eq!(
956 unsafe { call(&vtable, 1, "hi") },
957 (STATUS_ERROR, "no".to_string())
958 );
959 let (status, message) = unsafe { call(&vtable, 2, "hi") };
960 assert_eq!(status, STATUS_ERROR);
961 assert!(message.contains("panicked"), "{message}");
962 let (status, message) = unsafe { call(&vtable, 9, "hi") };
963 assert_eq!(status, STATUS_ERROR);
964 assert!(message.contains("no capability"), "{message}");
965 }
966
967 #[test]
968 fn another_major_is_refused_before_the_rest_is_read() {
969 let words: [u32; 2] = [ABI_MAJOR + 1, 0];
972 let error = unsafe { read_descriptor(words.as_ptr().cast()) }.unwrap_err();
973 assert_eq!(
974 error,
975 AbiError::Incompatible {
976 major: ABI_MAJOR + 1,
977 minor: 0
978 }
979 );
980 assert!(error.to_string().contains("rebuild"));
981 assert!(unsafe { read_descriptor(std::ptr::null()) }.is_err());
982 }
983
984 #[test]
985 fn a_null_function_in_the_vtable_is_refused() {
986 for (call, free) in [(false, true), (true, false), (false, false)] {
988 let mut exported = Exported::new(exports(), __rich_plugin_call);
989 let vtable = &mut exported.descriptor.vtable;
990 if !call {
991 vtable.call = None;
992 }
993 if !free {
994 vtable.free = None;
995 }
996 let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
997 assert!(error.to_string().contains("null function"), "{error}");
998 }
999 }
1000
1001 #[test]
1002 fn a_newer_minor_loads_but_an_unknown_kind_does_not() {
1003 let mut exported = Exported::new(exports(), __rich_plugin_call);
1004 exported.descriptor.abi_minor = ABI_MINOR + 3;
1005 let (abi, _) = unsafe { read_descriptor(&exported.descriptor) }.unwrap();
1006 assert_eq!(abi.abi_minor, ABI_MINOR + 3);
1007 let entries = [AbiCapabilityEntry {
1008 kind: 99,
1009 name: AbiStr::new("x"),
1010 }];
1011 exported.descriptor.capabilities = entries.as_ptr();
1012 exported.descriptor.capability_count = 1;
1013 let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
1014 assert!(error.to_string().contains("unknown capability kind 99"));
1015 }
1016
1017 #[test]
1018 fn manifests_round_trip_and_are_checked() {
1019 let abi = exports().abi().clone();
1020 let manifest = abi.to_manifest();
1021 assert!(manifest.starts_with("rich-plugin-abi 1.0\nname shout\n"));
1022 assert_eq!(PluginAbi::parse_manifest(&manifest).unwrap(), abi);
1023 let extra = manifest.replace("name shout", "name shout\nhomepage x");
1025 assert_eq!(PluginAbi::parse_manifest(&extra).unwrap(), abi);
1026 let other = manifest.replace("rich-plugin-abi 1.0", "rich-plugin-abi 2.0");
1027 assert_eq!(
1028 PluginAbi::parse_manifest(&other),
1029 Err(AbiError::Incompatible { major: 2, minor: 0 })
1030 );
1031 for bad in [
1032 "",
1033 "hello",
1034 "rich-plugin-abi one",
1035 "rich-plugin-abi 1.0\nname Bad\nversion 1",
1036 "rich-plugin-abi 1.0\nname ok\n",
1037 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability sing x",
1038 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform X",
1039 "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform x\ncapability transform x",
1040 ] {
1041 assert!(PluginAbi::parse_manifest(bad).is_err(), "{bad:?}");
1042 }
1043 }
1044}