1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::ffi::c_void;
3use std::mem::{offset_of, size_of};
4use std::ptr::NonNull;
5use std::sync::Arc;
6
7use player_plugin_abi::{
8 AUDIO_PROCESSOR_INTERFACE_ID, BENCHMARK_SINK_INTERFACE_ID, FRAME_PROCESSOR_INTERFACE_ID,
9 NATIVE_DECODER_INTERFACE_ID, PIPELINE_EVENT_HOOK_INTERFACE_ID,
10 POST_DOWNLOAD_PROCESSOR_INTERFACE_ID, SOURCE_NORMALIZER_PACKET_INTERFACE_ID,
11 SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID, VESPER_AUDIO_PROCESSOR_REQUIRED_SIZE,
12 VESPER_BENCHMARK_SINK_REQUIRED_SIZE, VESPER_FRAME_PROCESSOR_REQUIRED_SIZE,
13 VESPER_INTERFACE_MAJOR, VESPER_MAX_CAPABILITY_INSTANCE_ID_BYTES,
14 VESPER_MAX_INTERFACES_PER_PLUGIN, VESPER_MAX_PLUGIN_ID_BYTES, VESPER_MAX_PLUGIN_NAME_BYTES,
15 VESPER_NATIVE_DECODER_REQUIRED_SIZE, VESPER_PIPELINE_EVENT_HOOK_REQUIRED_SIZE,
16 VESPER_PLUGIN_ABI_MAJOR, VESPER_PLUGIN_ABI_MINOR, VESPER_POST_DOWNLOAD_PROCESSOR_REQUIRED_SIZE,
17 VESPER_SOURCE_NORMALIZER_PACKET_REQUIRED_SIZE, VESPER_SOURCE_NORMALIZER_RESOURCE_REQUIRED_SIZE,
18 VesperAudioProcessor, VesperBenchmarkSink, VesperByteSlice, VesperFrameProcessor,
19 VesperInterfaceDescriptor, VesperInterfaceHeader, VesperInterfaceId, VesperNativeDecoder,
20 VesperOwnedBytes, VesperPipelineEventHook, VesperPluginRoot, VesperPostDownloadProcessor,
21 VesperSourceNormalizerPacket, VesperSourceNormalizerResource, VesperStatus, abi_contains,
22 status,
23};
24use thiserror::Error;
25
26use crate::LibraryHolder;
27use player_plugin::{
28 AudioProcessorPluginFactory, BenchmarkSink, FrameProcessorPluginFactory,
29 NativeDecoderPluginFactory, PipelineEventHook, PluginInvocationPolicyError, PluginReference,
30 PluginScopeError, PluginTransport, PostDownloadProcessor, SourceNormalizerPacketPluginFactory,
31 SourceNormalizerResourcePluginFactory,
32};
33
34mod audio_processor;
35mod frame_processor;
36mod runtime;
37mod session;
38mod source_normalizer;
39mod stable;
40
41use runtime::NativeAbiBoundaryError;
42
43pub(crate) use audio_processor::NativeAbiAudioProcessorPluginFactory;
44pub(crate) use frame_processor::NativeAbiFrameProcessorPluginFactory;
45pub(crate) use session::NativeAbiDecoderPluginFactory;
46pub(crate) use source_normalizer::{
47 NativeAbiSourceNormalizerPacketPluginFactory, NativeAbiSourceNormalizerResourcePluginFactory,
48};
49pub(crate) use stable::{
50 NativeAbiBenchmarkSink, NativeAbiPipelineEventHook, NativeAbiPostDownloadProcessor,
51};
52
53const ROOT_REQUIRED_SIZE: u32 = size_of::<VesperPluginRoot>() as u32;
54
55#[derive(Debug, Error, PartialEq, Eq)]
56pub enum NativePluginContractError {
57 #[error("plugin root pointer is null")]
58 NullRoot,
59 #[error("plugin root is truncated: required {required} bytes, got {actual}")]
60 TruncatedRoot { required: u32, actual: u32 },
61 #[error(
62 "plugin root ABI mismatch: host supports {expected_major}.{expected_minor}, plugin reports {actual_major}.{actual_minor}"
63 )]
64 RootVersionMismatch {
65 expected_major: u16,
66 expected_minor: u16,
67 actual_major: u16,
68 actual_minor: u16,
69 },
70 #[error("plugin root field `{field}` is missing")]
71 MissingRootField { field: &'static str },
72 #[error("plugin field `{field}` is empty")]
73 EmptyField { field: &'static str },
74 #[error("plugin field `{field}` is too large: limit {limit} bytes, got {actual}")]
75 FieldTooLarge {
76 field: &'static str,
77 limit: usize,
78 actual: u64,
79 },
80 #[error("plugin field `{field}` has a null pointer with non-zero length")]
81 NullFieldData { field: &'static str },
82 #[error("plugin field `{field}` is not valid UTF-8")]
83 InvalidUtf8 { field: &'static str },
84 #[error("plugin field `{field}` is not a valid reverse-DNS identity: {value}")]
85 InvalidReverseDns { field: &'static str, value: String },
86 #[error("plugin advertises too many interfaces: limit {limit}, got {actual}")]
87 TooManyInterfaces { limit: u32, actual: u32 },
88 #[error("plugin does not advertise any interfaces")]
89 NoInterfaces,
90 #[error("plugin callback `{callback}` returned failure status {status}")]
91 CallbackFailure {
92 callback: &'static str,
93 status: VesperStatus,
94 },
95 #[error(
96 "plugin interface descriptor {index} is truncated: required {required} bytes, got {actual}"
97 )]
98 TruncatedDescriptor {
99 index: u32,
100 required: u32,
101 actual: u32,
102 },
103 #[error("plugin interface descriptor {index} has unsupported version {major}.{minor}")]
104 UnsupportedInterfaceVersion { index: u32, major: u16, minor: u16 },
105 #[error("plugin advertises duplicate interface {interface_id:?} instance `{instance_id}`")]
106 DuplicateInterface {
107 interface_id: VesperInterfaceId,
108 instance_id: String,
109 },
110 #[error(
111 "plugin query returned a null table for interface {interface_id:?} instance `{instance_id}`"
112 )]
113 NullInterface {
114 interface_id: VesperInterfaceId,
115 instance_id: String,
116 },
117 #[error(
118 "plugin query returned a truncated interface header: required {required} bytes, got {actual}"
119 )]
120 TruncatedInterfaceHeader { required: u32, actual: u32 },
121 #[error("plugin query returned metadata that differs from the enumerated interface")]
122 InterfaceMetadataMismatch,
123 #[error("plugin interface {interface_id:?} instance `{instance_id}` has a null context")]
124 NullInterfaceContext {
125 interface_id: VesperInterfaceId,
126 instance_id: String,
127 },
128 #[error(
129 "plugin interface {interface_id:?} instance `{instance_id}` is truncated: required {required} bytes, got {actual}"
130 )]
131 TruncatedInterface {
132 interface_id: VesperInterfaceId,
133 instance_id: String,
134 required: u32,
135 actual: u32,
136 },
137 #[error(
138 "plugin interface {interface_id:?} instance `{instance_id}` is missing callback `{callback}`"
139 )]
140 MissingInterfaceCallback {
141 interface_id: VesperInterfaceId,
142 instance_id: String,
143 callback: &'static str,
144 },
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum PluginContractDiagnosticKind {
149 Compatibility,
150 ContractViolation,
151}
152
153impl NativePluginContractError {
154 pub const fn diagnostic_kind(&self) -> PluginContractDiagnosticKind {
155 match self {
156 Self::RootVersionMismatch { .. } | Self::UnsupportedInterfaceVersion { .. } => {
157 PluginContractDiagnosticKind::Compatibility
158 }
159 _ => PluginContractDiagnosticKind::ContractViolation,
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub(crate) struct CheckedInterfaceDescriptor {
166 pub(crate) interface_id: VesperInterfaceId,
167 pub(crate) major: u16,
168 pub(crate) minor: u16,
169 pub(crate) instance_id: String,
170}
171
172#[derive(Debug, Clone, Copy)]
173pub(crate) enum CheckedInterfaceTable {
174 PostDownload(VesperPostDownloadProcessor),
175 PipelineEventHook(VesperPipelineEventHook),
176 BenchmarkSink(VesperBenchmarkSink),
177 NativeDecoder(VesperNativeDecoder),
178 FrameProcessor(VesperFrameProcessor),
179 AudioProcessor(VesperAudioProcessor),
180 SourceNormalizerPacket(VesperSourceNormalizerPacket),
181 SourceNormalizerResource(VesperSourceNormalizerResource),
182 Unknown,
183}
184
185#[derive(Debug, Clone)]
186pub(crate) struct CheckedInterface {
187 pub(crate) index: u32,
188 pub(crate) descriptor: CheckedInterfaceDescriptor,
189 pub(crate) table: CheckedInterfaceTable,
190}
191
192#[derive(Debug)]
193pub(crate) struct InterfaceLoadDiagnostic {
194 pub(crate) index: u32,
195 pub(crate) descriptor: Option<CheckedInterfaceDescriptor>,
196 pub(crate) error: NativePluginContractError,
197}
198
199struct PendingPluginOwner {
200 owner: NonNull<c_void>,
201 destroy_owner: unsafe extern "C" fn(owner: *mut c_void),
202 armed: bool,
203}
204
205impl PendingPluginOwner {
206 fn new(
207 owner: NonNull<c_void>,
208 destroy_owner: unsafe extern "C" fn(owner: *mut c_void),
209 ) -> Self {
210 Self {
211 owner,
212 destroy_owner,
213 armed: true,
214 }
215 }
216
217 fn disarm(&mut self) {
218 self.armed = false;
219 }
220}
221
222impl Drop for PendingPluginOwner {
223 fn drop(&mut self) {
224 if self.armed {
225 unsafe { (self.destroy_owner)(self.owner.as_ptr()) };
229 }
230 }
231}
232
233#[derive(Debug)]
234pub(crate) struct PluginOwner {
235 owner: NonNull<c_void>,
236 free_bytes: unsafe extern "C" fn(owner: *mut c_void, bytes: VesperOwnedBytes),
237 destroy_owner: unsafe extern "C" fn(owner: *mut c_void),
238 #[allow(dead_code)]
239 library: Option<Arc<LibraryHolder>>,
240}
241
242unsafe impl Send for PluginOwner {}
245unsafe impl Sync for PluginOwner {}
248
249impl PluginOwner {
250 pub(crate) fn free_bytes(&self, bytes: VesperOwnedBytes) {
251 if bytes.data.is_null() && bytes.len == 0 {
252 return;
253 }
254 unsafe { (self.free_bytes)(self.owner.as_ptr(), bytes) };
257 }
258}
259
260impl Drop for PluginOwner {
261 fn drop(&mut self) {
262 unsafe { (self.destroy_owner)(self.owner.as_ptr()) };
265 }
266}
267
268#[derive(Debug)]
269pub(crate) struct CheckedPluginRoot {
270 pub(crate) plugin_id: String,
271 pub(crate) plugin_name: String,
272 pub(crate) interfaces: Vec<CheckedInterface>,
273 pub(crate) diagnostics: Vec<InterfaceLoadDiagnostic>,
274 pub(crate) owner: Arc<PluginOwner>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct PluginInterfaceMetadata {
279 pub interface_id: [u8; 16],
280 pub major: u16,
281 pub minor: u16,
282 pub instance_id: String,
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum PluginInterfaceState {
287 Available,
288 Unavailable,
289 Unknown,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct PluginInterfaceRecord {
294 pub metadata: PluginInterfaceMetadata,
295 pub state: PluginInterfaceState,
296}
297
298impl From<&CheckedInterfaceDescriptor> for PluginInterfaceMetadata {
299 fn from(descriptor: &CheckedInterfaceDescriptor) -> Self {
300 Self {
301 interface_id: descriptor.interface_id.0,
302 major: descriptor.major,
303 minor: descriptor.minor,
304 instance_id: descriptor.instance_id.clone(),
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct PluginInterfaceDiagnostic {
311 pub index: Option<u32>,
312 pub interface: Option<PluginInterfaceMetadata>,
313 pub kind: PluginContractDiagnosticKind,
314 pub message: String,
315}
316
317#[derive(Debug, Error, Clone, PartialEq, Eq)]
318pub enum PluginSelectionError {
319 #[error(transparent)]
320 InvocationPolicyRejected(#[from] PluginInvocationPolicyError),
321 #[error("plugin reference selects transport {actual:?}, but this root uses {expected:?}")]
322 TransportMismatch {
323 expected: PluginTransport,
324 actual: PluginTransport,
325 },
326 #[error("plugin reference id `{requested}` does not match loaded plugin `{loaded}`")]
327 PluginIdMismatch { requested: String, loaded: String },
328 #[error("plugin `{plugin_id}` does not expose {interface} instance `{instance_id}`")]
329 InstanceNotFound {
330 plugin_id: String,
331 interface: &'static str,
332 instance_id: String,
333 },
334 #[error("plugin `{plugin_id}` does not expose interface {interface}")]
335 InterfaceNotFound {
336 plugin_id: String,
337 interface: &'static str,
338 },
339 #[error(
340 "plugin `{plugin_id}` exposes {interface} instance `{instance_id}`, but it is unavailable"
341 )]
342 InstanceUnavailable {
343 plugin_id: String,
344 interface: &'static str,
345 instance_id: String,
346 },
347 #[error("plugin `{plugin_id}` exposes interface {interface}, but it is unavailable")]
348 InterfaceUnavailable {
349 plugin_id: String,
350 interface: &'static str,
351 },
352 #[error(
353 "plugin `{plugin_id}` exposes {count} instances of {interface}; capability_instance_id is required"
354 )]
355 Ambiguous {
356 plugin_id: String,
357 interface: &'static str,
358 count: usize,
359 },
360 #[error("plugin `{plugin_id}` is not loaded for transport {transport:?}")]
361 PluginNotFound {
362 plugin_id: String,
363 transport: PluginTransport,
364 },
365 #[error("WASM plugin `{plugin_id}` interface {interface} requires a lifecycle scope")]
366 ScopeRequired {
367 plugin_id: String,
368 interface: &'static str,
369 },
370 #[error(
371 "failed to register WASM plugin `{plugin_id}` interface {interface} with its lifecycle scope: {source}"
372 )]
373 ScopeRegistration {
374 plugin_id: String,
375 interface: &'static str,
376 #[source]
377 source: PluginScopeError,
378 },
379 #[cfg(feature = "wasm")]
380 #[error(
381 "failed to instantiate WASM plugin `{plugin_id}` interface {interface} instance `{instance_id}`: {message}"
382 )]
383 WasmInstantiation {
384 plugin_id: String,
385 interface: &'static str,
386 instance_id: String,
387 message: String,
388 },
389 #[error(
390 "loaded plugin identity `{plugin_id}` or instance `{instance_id}` cannot form a canonical reference"
391 )]
392 InvalidLoadedIdentity {
393 plugin_id: String,
394 instance_id: String,
395 },
396}
397
398pub struct LoadedNativePlugin {
399 plugin_id: String,
400 plugin_name: String,
401 post_download: BTreeMap<String, Arc<dyn PostDownloadProcessor>>,
402 pipeline_event_hooks: BTreeMap<String, Arc<dyn PipelineEventHook>>,
403 benchmark_sinks: BTreeMap<String, Arc<dyn BenchmarkSink>>,
404 native_decoders: BTreeMap<String, Arc<dyn NativeDecoderPluginFactory>>,
405 frame_processors: BTreeMap<String, Arc<dyn FrameProcessorPluginFactory>>,
406 audio_processors: BTreeMap<String, Arc<dyn AudioProcessorPluginFactory>>,
407 source_packets: BTreeMap<String, Arc<dyn SourceNormalizerPacketPluginFactory>>,
408 source_resources: BTreeMap<String, Arc<dyn SourceNormalizerResourcePluginFactory>>,
409 advertised_instances: BTreeMap<[u8; 16], BTreeSet<String>>,
410 interfaces: Vec<PluginInterfaceRecord>,
411 diagnostics: Vec<PluginInterfaceDiagnostic>,
412 _owner: Arc<PluginOwner>,
413}
414
415impl std::fmt::Debug for LoadedNativePlugin {
416 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 formatter
418 .debug_struct("LoadedNativePlugin")
419 .field("plugin_id", &self.plugin_id)
420 .field("plugin_name", &self.plugin_name)
421 .field("post_download_instances", &self.post_download.keys())
422 .field(
423 "pipeline_event_hook_instances",
424 &self.pipeline_event_hooks.keys(),
425 )
426 .field("benchmark_sink_instances", &self.benchmark_sinks.keys())
427 .field("native_decoder_instances", &self.native_decoders.keys())
428 .field("frame_processor_instances", &self.frame_processors.keys())
429 .field("audio_processor_instances", &self.audio_processors.keys())
430 .field("source_packet_instances", &self.source_packets.keys())
431 .field("source_resource_instances", &self.source_resources.keys())
432 .field("advertised_instances", &self.advertised_instances)
433 .field("interfaces", &self.interfaces)
434 .field("diagnostics", &self.diagnostics)
435 .finish()
436 }
437}
438
439impl LoadedNativePlugin {
440 pub(crate) fn from_checked(root: CheckedPluginRoot) -> Self {
441 let CheckedPluginRoot {
442 plugin_id,
443 plugin_name,
444 interfaces,
445 diagnostics: root_diagnostics,
446 owner,
447 } = root;
448 let mut advertised_instances = BTreeMap::<[u8; 16], BTreeSet<String>>::new();
449 for descriptor in root_diagnostics
450 .iter()
451 .filter_map(|diagnostic| diagnostic.descriptor.as_ref())
452 .filter(|descriptor| is_known_interface(descriptor.interface_id))
453 {
454 advertised_instances
455 .entry(descriptor.interface_id.0)
456 .or_default()
457 .insert(descriptor.instance_id.clone());
458 }
459
460 let mut loaded = Self {
461 plugin_id,
462 plugin_name,
463 post_download: BTreeMap::new(),
464 pipeline_event_hooks: BTreeMap::new(),
465 benchmark_sinks: BTreeMap::new(),
466 native_decoders: BTreeMap::new(),
467 frame_processors: BTreeMap::new(),
468 audio_processors: BTreeMap::new(),
469 source_packets: BTreeMap::new(),
470 source_resources: BTreeMap::new(),
471 advertised_instances,
472 interfaces: root_diagnostics
473 .iter()
474 .filter_map(|diagnostic| diagnostic.descriptor.as_ref())
475 .map(|descriptor| PluginInterfaceRecord {
476 metadata: PluginInterfaceMetadata::from(descriptor),
477 state: PluginInterfaceState::Unavailable,
478 })
479 .collect(),
480 diagnostics: root_diagnostics
481 .into_iter()
482 .map(|diagnostic| PluginInterfaceDiagnostic {
483 index: Some(diagnostic.index),
484 interface: diagnostic
485 .descriptor
486 .as_ref()
487 .map(PluginInterfaceMetadata::from),
488 kind: diagnostic.error.diagnostic_kind(),
489 message: diagnostic.error.to_string(),
490 })
491 .collect(),
492 _owner: owner.clone(),
493 };
494
495 for interface in interfaces {
496 let index = interface.index;
497 let descriptor = interface.descriptor;
498 let instance_id = descriptor.instance_id.clone();
499 let is_unknown = matches!(interface.table, CheckedInterfaceTable::Unknown);
500 if !is_unknown {
501 loaded
502 .advertised_instances
503 .entry(descriptor.interface_id.0)
504 .or_default()
505 .insert(instance_id.clone());
506 }
507 let result: Result<(), NativeAbiBoundaryError> = match interface.table {
508 CheckedInterfaceTable::PostDownload(table) => NativeAbiPostDownloadProcessor::new(
509 &loaded.plugin_id,
510 loaded.plugin_name.clone(),
511 &instance_id,
512 owner.clone(),
513 table,
514 )
515 .map(|value| {
516 loaded.post_download.insert(instance_id, Arc::new(value));
517 }),
518 CheckedInterfaceTable::PipelineEventHook(table) => NativeAbiPipelineEventHook::new(
519 &loaded.plugin_id,
520 &instance_id,
521 owner.clone(),
522 table,
523 )
524 .map(|value| {
525 loaded
526 .pipeline_event_hooks
527 .insert(instance_id, Arc::new(value));
528 }),
529 CheckedInterfaceTable::BenchmarkSink(table) => NativeAbiBenchmarkSink::new(
530 &loaded.plugin_id,
531 loaded.plugin_name.clone(),
532 &instance_id,
533 owner.clone(),
534 table,
535 )
536 .map(|value| {
537 loaded.benchmark_sinks.insert(instance_id, Arc::new(value));
538 }),
539 CheckedInterfaceTable::NativeDecoder(table) => NativeAbiDecoderPluginFactory::new(
540 &loaded.plugin_id,
541 loaded.plugin_name.clone(),
542 &instance_id,
543 owner.clone(),
544 table,
545 )
546 .map(|value| {
547 loaded.native_decoders.insert(instance_id, Arc::new(value));
548 }),
549 CheckedInterfaceTable::FrameProcessor(table) => {
550 NativeAbiFrameProcessorPluginFactory::new(
551 &loaded.plugin_id,
552 loaded.plugin_name.clone(),
553 &instance_id,
554 owner.clone(),
555 table,
556 )
557 .map(|value| {
558 loaded.frame_processors.insert(instance_id, Arc::new(value));
559 })
560 }
561 CheckedInterfaceTable::AudioProcessor(table) => {
562 NativeAbiAudioProcessorPluginFactory::new(
563 &loaded.plugin_id,
564 loaded.plugin_name.clone(),
565 &instance_id,
566 owner.clone(),
567 table,
568 )
569 .map(|value| {
570 loaded.audio_processors.insert(instance_id, Arc::new(value));
571 })
572 }
573 CheckedInterfaceTable::SourceNormalizerPacket(table) => {
574 NativeAbiSourceNormalizerPacketPluginFactory::new(
575 &loaded.plugin_id,
576 loaded.plugin_name.clone(),
577 &instance_id,
578 owner.clone(),
579 table,
580 )
581 .map(|value| {
582 loaded.source_packets.insert(instance_id, Arc::new(value));
583 })
584 }
585 CheckedInterfaceTable::SourceNormalizerResource(table) => {
586 NativeAbiSourceNormalizerResourcePluginFactory::new(
587 &loaded.plugin_id,
588 loaded.plugin_name.clone(),
589 &instance_id,
590 owner.clone(),
591 table,
592 )
593 .map(|value| {
594 loaded.source_resources.insert(instance_id, Arc::new(value));
595 })
596 }
597 CheckedInterfaceTable::Unknown => Ok(()),
598 };
599 let metadata = PluginInterfaceMetadata::from(&descriptor);
600 match result {
601 Ok(()) => loaded.interfaces.push(PluginInterfaceRecord {
602 metadata,
603 state: if is_unknown {
604 PluginInterfaceState::Unknown
605 } else {
606 PluginInterfaceState::Available
607 },
608 }),
609 Err(error) => {
610 loaded.interfaces.push(PluginInterfaceRecord {
611 metadata: metadata.clone(),
612 state: PluginInterfaceState::Unavailable,
613 });
614 loaded.diagnostics.push(PluginInterfaceDiagnostic {
615 index: Some(index),
616 interface: Some(metadata),
617 kind: PluginContractDiagnosticKind::ContractViolation,
618 message: error.to_string(),
619 });
620 }
621 }
622 }
623 loaded
624 }
625
626 pub fn plugin_id(&self) -> &str {
627 &self.plugin_id
628 }
629
630 pub fn plugin_name(&self) -> &str {
631 &self.plugin_name
632 }
633
634 pub fn diagnostics(&self) -> &[PluginInterfaceDiagnostic] {
635 &self.diagnostics
636 }
637
638 pub fn interfaces(&self) -> &[PluginInterfaceRecord] {
639 &self.interfaces
640 }
641
642 pub fn unknown_interfaces(&self) -> impl Iterator<Item = &PluginInterfaceMetadata> {
643 self.interfaces.iter().filter_map(|interface| {
644 (interface.state == PluginInterfaceState::Unknown).then_some(&interface.metadata)
645 })
646 }
647
648 pub fn resolve_post_download(
649 &self,
650 reference: &PluginReference,
651 ) -> Result<Arc<dyn PostDownloadProcessor>, PluginSelectionError> {
652 self.resolve_post_download_selected(reference)
653 .map(|(_, capability)| capability)
654 }
655
656 pub(crate) fn resolve_post_download_selected(
657 &self,
658 reference: &PluginReference,
659 ) -> Result<(String, Arc<dyn PostDownloadProcessor>), PluginSelectionError> {
660 self.resolve(
661 reference,
662 POST_DOWNLOAD_PROCESSOR_INTERFACE_ID,
663 "PostDownloadProcessor",
664 &self.post_download,
665 )
666 }
667
668 pub fn resolve_pipeline_event_hook(
669 &self,
670 reference: &PluginReference,
671 ) -> Result<Arc<dyn PipelineEventHook>, PluginSelectionError> {
672 self.resolve_pipeline_event_hook_selected(reference)
673 .map(|(_, capability)| capability)
674 }
675
676 pub(crate) fn resolve_pipeline_event_hook_selected(
677 &self,
678 reference: &PluginReference,
679 ) -> Result<(String, Arc<dyn PipelineEventHook>), PluginSelectionError> {
680 self.resolve(
681 reference,
682 PIPELINE_EVENT_HOOK_INTERFACE_ID,
683 "PipelineEventHook",
684 &self.pipeline_event_hooks,
685 )
686 }
687
688 pub fn resolve_benchmark_sink(
689 &self,
690 reference: &PluginReference,
691 ) -> Result<Arc<dyn BenchmarkSink>, PluginSelectionError> {
692 self.resolve_benchmark_sink_selected(reference)
693 .map(|(_, capability)| capability)
694 }
695
696 pub(crate) fn resolve_benchmark_sink_selected(
697 &self,
698 reference: &PluginReference,
699 ) -> Result<(String, Arc<dyn BenchmarkSink>), PluginSelectionError> {
700 self.resolve(
701 reference,
702 BENCHMARK_SINK_INTERFACE_ID,
703 "BenchmarkSink",
704 &self.benchmark_sinks,
705 )
706 }
707
708 pub fn resolve_native_decoder(
709 &self,
710 reference: &PluginReference,
711 ) -> Result<Arc<dyn NativeDecoderPluginFactory>, PluginSelectionError> {
712 self.resolve_native_decoder_selected(reference)
713 .map(|(_, capability)| capability)
714 }
715
716 pub(crate) fn resolve_native_decoder_selected(
717 &self,
718 reference: &PluginReference,
719 ) -> Result<(String, Arc<dyn NativeDecoderPluginFactory>), PluginSelectionError> {
720 self.resolve(
721 reference,
722 NATIVE_DECODER_INTERFACE_ID,
723 "NativeDecoder",
724 &self.native_decoders,
725 )
726 }
727
728 pub fn resolve_frame_processor(
729 &self,
730 reference: &PluginReference,
731 ) -> Result<Arc<dyn FrameProcessorPluginFactory>, PluginSelectionError> {
732 self.resolve_frame_processor_selected(reference)
733 .map(|(_, capability)| capability)
734 }
735
736 pub(crate) fn resolve_frame_processor_selected(
737 &self,
738 reference: &PluginReference,
739 ) -> Result<(String, Arc<dyn FrameProcessorPluginFactory>), PluginSelectionError> {
740 self.resolve(
741 reference,
742 FRAME_PROCESSOR_INTERFACE_ID,
743 "FrameProcessor",
744 &self.frame_processors,
745 )
746 }
747
748 pub fn resolve_audio_processor(
749 &self,
750 reference: &PluginReference,
751 ) -> Result<Arc<dyn AudioProcessorPluginFactory>, PluginSelectionError> {
752 self.resolve_audio_processor_selected(reference)
753 .map(|(_, capability)| capability)
754 }
755
756 pub(crate) fn resolve_audio_processor_selected(
757 &self,
758 reference: &PluginReference,
759 ) -> Result<(String, Arc<dyn AudioProcessorPluginFactory>), PluginSelectionError> {
760 self.resolve(
761 reference,
762 AUDIO_PROCESSOR_INTERFACE_ID,
763 "AudioProcessor",
764 &self.audio_processors,
765 )
766 }
767
768 pub fn resolve_source_packet(
769 &self,
770 reference: &PluginReference,
771 ) -> Result<Arc<dyn SourceNormalizerPacketPluginFactory>, PluginSelectionError> {
772 self.resolve_source_packet_selected(reference)
773 .map(|(_, capability)| capability)
774 }
775
776 pub(crate) fn resolve_source_packet_selected(
777 &self,
778 reference: &PluginReference,
779 ) -> Result<(String, Arc<dyn SourceNormalizerPacketPluginFactory>), PluginSelectionError> {
780 self.resolve(
781 reference,
782 SOURCE_NORMALIZER_PACKET_INTERFACE_ID,
783 "SourceNormalizerPacket",
784 &self.source_packets,
785 )
786 }
787
788 pub fn resolve_source_resource(
789 &self,
790 reference: &PluginReference,
791 ) -> Result<Arc<dyn SourceNormalizerResourcePluginFactory>, PluginSelectionError> {
792 self.resolve_source_resource_selected(reference)
793 .map(|(_, capability)| capability)
794 }
795
796 pub(crate) fn resolve_source_resource_selected(
797 &self,
798 reference: &PluginReference,
799 ) -> Result<(String, Arc<dyn SourceNormalizerResourcePluginFactory>), PluginSelectionError>
800 {
801 self.resolve(
802 reference,
803 SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID,
804 "SourceNormalizerResource",
805 &self.source_resources,
806 )
807 }
808
809 fn resolve<T: ?Sized>(
810 &self,
811 reference: &PluginReference,
812 interface_id: VesperInterfaceId,
813 interface: &'static str,
814 instances: &BTreeMap<String, Arc<T>>,
815 ) -> Result<(String, Arc<T>), PluginSelectionError> {
816 if reference.transport() != PluginTransport::Native {
817 return Err(PluginSelectionError::TransportMismatch {
818 expected: PluginTransport::Native,
819 actual: reference.transport(),
820 });
821 }
822 if reference.plugin_id() != self.plugin_id {
823 return Err(PluginSelectionError::PluginIdMismatch {
824 requested: reference.plugin_id().to_owned(),
825 loaded: self.plugin_id.clone(),
826 });
827 }
828 if let Some(instance_id) = reference.capability_instance_id() {
829 return instances
830 .get(instance_id)
831 .cloned()
832 .map(|capability| (instance_id.to_owned(), capability))
833 .ok_or_else(|| {
834 let advertised = self
835 .advertised_instances
836 .get(&interface_id.0)
837 .is_some_and(|values| values.contains(instance_id));
838 if advertised {
839 PluginSelectionError::InstanceUnavailable {
840 plugin_id: self.plugin_id.clone(),
841 interface,
842 instance_id: instance_id.to_owned(),
843 }
844 } else {
845 PluginSelectionError::InstanceNotFound {
846 plugin_id: self.plugin_id.clone(),
847 interface,
848 instance_id: instance_id.to_owned(),
849 }
850 }
851 });
852 }
853 let advertised_count = self
854 .advertised_instances
855 .get(&interface_id.0)
856 .map_or(0, BTreeSet::len);
857 match advertised_count {
858 0 => Err(PluginSelectionError::InterfaceNotFound {
859 plugin_id: self.plugin_id.clone(),
860 interface,
861 }),
862 1 => instances
863 .iter()
864 .next()
865 .map(|(instance_id, capability)| (instance_id.clone(), capability.clone()))
866 .ok_or_else(|| PluginSelectionError::InterfaceUnavailable {
867 plugin_id: self.plugin_id.clone(),
868 interface,
869 }),
870 count => Err(PluginSelectionError::Ambiguous {
871 plugin_id: self.plugin_id.clone(),
872 interface,
873 count,
874 }),
875 }
876 }
877}
878
879impl CheckedPluginRoot {
880 pub(crate) unsafe fn from_raw(
881 root_ptr: *const VesperPluginRoot,
882 library: Option<Arc<LibraryHolder>>,
883 ) -> Result<Self, NativePluginContractError> {
884 if root_ptr.is_null() {
885 return Err(NativePluginContractError::NullRoot);
886 }
887
888 let struct_size = unsafe { root_ptr.cast::<u32>().read_unaligned() };
892 if struct_size < ROOT_REQUIRED_SIZE {
893 return Err(NativePluginContractError::TruncatedRoot {
894 required: ROOT_REQUIRED_SIZE,
895 actual: struct_size,
896 });
897 }
898 let root = unsafe { root_ptr.read_unaligned() };
901 if root.abi_major != VESPER_PLUGIN_ABI_MAJOR || root.abi_minor > VESPER_PLUGIN_ABI_MINOR {
902 return Err(NativePluginContractError::RootVersionMismatch {
903 expected_major: VESPER_PLUGIN_ABI_MAJOR,
904 expected_minor: VESPER_PLUGIN_ABI_MINOR,
905 actual_major: root.abi_major,
906 actual_minor: root.abi_minor,
907 });
908 }
909 let owner_ptr = NonNull::new(root.owner)
910 .ok_or(NativePluginContractError::MissingRootField { field: "owner" })?;
911 let destroy_owner =
912 root.destroy_owner
913 .ok_or(NativePluginContractError::MissingRootField {
914 field: "destroy_owner",
915 })?;
916 let mut pending_owner = PendingPluginOwner::new(owner_ptr, destroy_owner);
917 let free_bytes = root
918 .free_bytes
919 .ok_or(NativePluginContractError::MissingRootField {
920 field: "free_bytes",
921 })?;
922 let owner = Arc::new(PluginOwner {
923 owner: owner_ptr,
924 free_bytes,
925 destroy_owner,
926 library,
927 });
928 pending_owner.disarm();
929
930 let plugin_id =
931 unsafe { copy_utf8(root.plugin_id, "plugin_id", VESPER_MAX_PLUGIN_ID_BYTES, false) }?;
934 if !is_reverse_dns(&plugin_id) {
935 return Err(NativePluginContractError::InvalidReverseDns {
936 field: "plugin_id",
937 value: plugin_id,
938 });
939 }
940 let plugin_name =
941 unsafe {
943 copy_utf8(
944 root.plugin_name,
945 "plugin_name",
946 VESPER_MAX_PLUGIN_NAME_BYTES,
947 false,
948 )
949 }?;
950 if root.interface_count > VESPER_MAX_INTERFACES_PER_PLUGIN {
951 return Err(NativePluginContractError::TooManyInterfaces {
952 limit: VESPER_MAX_INTERFACES_PER_PLUGIN,
953 actual: root.interface_count,
954 });
955 }
956 if root.interface_count == 0 {
957 return Err(NativePluginContractError::NoInterfaces);
958 }
959 let interface_at =
960 root.interface_at
961 .ok_or(NativePluginContractError::MissingRootField {
962 field: "interface_at",
963 })?;
964 let query_interface =
965 root.query_interface
966 .ok_or(NativePluginContractError::MissingRootField {
967 field: "query_interface",
968 })?;
969
970 let mut interfaces = Vec::with_capacity(root.interface_count as usize);
971 let mut diagnostics = Vec::new();
972 let mut seen = HashSet::with_capacity(root.interface_count as usize);
973 for index in 0..root.interface_count {
974 match unsafe {
978 load_interface(
979 owner.as_ref(),
980 interface_at,
981 query_interface,
982 index,
983 &mut seen,
984 )
985 } {
986 Ok(interface) => interfaces.push(interface),
987 Err((
988 _descriptor,
989 error @ NativePluginContractError::DuplicateInterface { .. },
990 )) => {
991 return Err(error);
992 }
993 Err((descriptor, error)) => diagnostics.push(InterfaceLoadDiagnostic {
994 index,
995 descriptor,
996 error,
997 }),
998 }
999 }
1000
1001 Ok(Self {
1002 plugin_id,
1003 plugin_name,
1004 interfaces,
1005 diagnostics,
1006 owner,
1007 })
1008 }
1009}
1010
1011unsafe fn load_interface(
1012 owner: &PluginOwner,
1013 interface_at: unsafe extern "C" fn(
1014 owner: *mut c_void,
1015 index: u32,
1016 out: *mut VesperInterfaceDescriptor,
1017 ) -> VesperStatus,
1018 query_interface: unsafe extern "C" fn(
1019 owner: *mut c_void,
1020 interface_id: *const VesperInterfaceId,
1021 instance_id: VesperByteSlice,
1022 requested_major: u16,
1023 minimum_minor: u16,
1024 out: *mut *const VesperInterfaceHeader,
1025 ) -> VesperStatus,
1026 index: u32,
1027 seen: &mut HashSet<(VesperInterfaceId, String)>,
1028) -> Result<
1029 CheckedInterface,
1030 (
1031 Option<CheckedInterfaceDescriptor>,
1032 NativePluginContractError,
1033 ),
1034> {
1035 let mut raw_descriptor = VesperInterfaceDescriptor::default();
1036 let result = unsafe { interface_at(owner.owner.as_ptr(), index, &mut raw_descriptor) };
1039 require_ok("interface_at", result).map_err(|error| (None, error))?;
1040 if raw_descriptor.struct_size < size_of::<VesperInterfaceDescriptor>() as u32 {
1041 return Err((
1042 None,
1043 NativePluginContractError::TruncatedDescriptor {
1044 index,
1045 required: size_of::<VesperInterfaceDescriptor>() as u32,
1046 actual: raw_descriptor.struct_size,
1047 },
1048 ));
1049 }
1050 let instance_id =
1051 unsafe {
1054 copy_utf8(
1055 raw_descriptor.instance_id,
1056 "capability_instance_id",
1057 VESPER_MAX_CAPABILITY_INSTANCE_ID_BYTES,
1058 false,
1059 )
1060 }
1061 .map_err(|error| (None, error))?;
1062 if !is_reverse_dns(&instance_id) {
1063 return Err((
1064 None,
1065 NativePluginContractError::InvalidReverseDns {
1066 field: "capability_instance_id",
1067 value: instance_id,
1068 },
1069 ));
1070 }
1071 let descriptor = CheckedInterfaceDescriptor {
1072 interface_id: raw_descriptor.interface_id,
1073 major: raw_descriptor.major,
1074 minor: raw_descriptor.minor,
1075 instance_id,
1076 };
1077 let key = (descriptor.interface_id, descriptor.instance_id.clone());
1078 if !seen.insert(key) {
1079 return Err((
1080 Some(descriptor.clone()),
1081 NativePluginContractError::DuplicateInterface {
1082 interface_id: descriptor.interface_id,
1083 instance_id: descriptor.instance_id.clone(),
1084 },
1085 ));
1086 }
1087 if !is_known_interface(descriptor.interface_id) {
1088 return Ok(CheckedInterface {
1089 index,
1090 descriptor,
1091 table: CheckedInterfaceTable::Unknown,
1092 });
1093 }
1094 if descriptor.major != VESPER_INTERFACE_MAJOR {
1095 return Err((
1096 Some(descriptor.clone()),
1097 NativePluginContractError::UnsupportedInterfaceVersion {
1098 index,
1099 major: descriptor.major,
1100 minor: descriptor.minor,
1101 },
1102 ));
1103 }
1104
1105 let instance_bytes = VesperByteSlice {
1106 data: descriptor.instance_id.as_ptr(),
1107 len: descriptor.instance_id.len() as u64,
1108 };
1109 let mut table_ptr = std::ptr::null();
1110 let result = unsafe {
1113 query_interface(
1114 owner.owner.as_ptr(),
1115 &descriptor.interface_id,
1116 instance_bytes,
1117 descriptor.major,
1118 0,
1119 &mut table_ptr,
1120 )
1121 };
1122 require_ok("query_interface", result).map_err(|error| (Some(descriptor.clone()), error))?;
1123 if table_ptr.is_null() {
1124 return Err((
1125 Some(descriptor.clone()),
1126 NativePluginContractError::NullInterface {
1127 interface_id: descriptor.interface_id,
1128 instance_id: descriptor.instance_id.clone(),
1129 },
1130 ));
1131 }
1132 let table =
1133 unsafe { check_interface_table(table_ptr, &descriptor) }
1136 .map_err(|error| (Some(descriptor.clone()), error))?;
1137 Ok(CheckedInterface {
1138 index,
1139 descriptor,
1140 table,
1141 })
1142}
1143
1144fn is_known_interface(interface_id: VesperInterfaceId) -> bool {
1145 matches!(
1146 interface_id,
1147 POST_DOWNLOAD_PROCESSOR_INTERFACE_ID
1148 | PIPELINE_EVENT_HOOK_INTERFACE_ID
1149 | BENCHMARK_SINK_INTERFACE_ID
1150 | NATIVE_DECODER_INTERFACE_ID
1151 | FRAME_PROCESSOR_INTERFACE_ID
1152 | AUDIO_PROCESSOR_INTERFACE_ID
1153 | SOURCE_NORMALIZER_PACKET_INTERFACE_ID
1154 | SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID
1155 )
1156}
1157
1158fn require_ok(
1159 callback: &'static str,
1160 value: VesperStatus,
1161) -> Result<(), NativePluginContractError> {
1162 if value == status::OK {
1163 Ok(())
1164 } else {
1165 Err(NativePluginContractError::CallbackFailure {
1166 callback,
1167 status: value,
1168 })
1169 }
1170}
1171
1172unsafe fn copy_utf8(
1173 bytes: VesperByteSlice,
1174 field: &'static str,
1175 limit: usize,
1176 allow_empty: bool,
1177) -> Result<String, NativePluginContractError> {
1178 if bytes.len == 0 {
1179 return if allow_empty {
1180 Ok(String::new())
1181 } else {
1182 Err(NativePluginContractError::EmptyField { field })
1183 };
1184 }
1185 if bytes.len > limit as u64 {
1186 return Err(NativePluginContractError::FieldTooLarge {
1187 field,
1188 limit,
1189 actual: bytes.len,
1190 });
1191 }
1192 if bytes.data.is_null() {
1193 return Err(NativePluginContractError::NullFieldData { field });
1194 }
1195 let len = bytes.len as usize;
1196 let slice = unsafe { std::slice::from_raw_parts(bytes.data, len) };
1199 let value =
1200 std::str::from_utf8(slice).map_err(|_| NativePluginContractError::InvalidUtf8 { field })?;
1201 Ok(value.to_owned())
1202}
1203
1204fn is_reverse_dns(value: &str) -> bool {
1205 let mut segments = value.split('.');
1206 let Some(first) = segments.next() else {
1207 return false;
1208 };
1209 let Some(second) = segments.next() else {
1210 return false;
1211 };
1212 valid_identity_segment(first)
1213 && valid_identity_segment(second)
1214 && segments.all(valid_identity_segment)
1215}
1216
1217fn valid_identity_segment(segment: &str) -> bool {
1218 let bytes = segment.as_bytes();
1219 matches!(bytes.first(), Some(b'a'..=b'z'))
1220 && matches!(bytes.last(), Some(b'a'..=b'z' | b'0'..=b'9'))
1221 && bytes
1222 .iter()
1223 .all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-'))
1224}
1225
1226unsafe fn check_interface_table(
1227 table_ptr: *const VesperInterfaceHeader,
1228 descriptor: &CheckedInterfaceDescriptor,
1229) -> Result<CheckedInterfaceTable, NativePluginContractError> {
1230 let struct_size = unsafe { table_ptr.cast::<u32>().read_unaligned() };
1232 if struct_size < size_of::<VesperInterfaceHeader>() as u32 {
1233 return Err(NativePluginContractError::TruncatedInterfaceHeader {
1234 required: size_of::<VesperInterfaceHeader>() as u32,
1235 actual: struct_size,
1236 });
1237 }
1238 let header = unsafe { table_ptr.read_unaligned() };
1240 if header.interface_id != descriptor.interface_id
1241 || header.major != descriptor.major
1242 || header.minor != descriptor.minor
1243 {
1244 return Err(NativePluginContractError::InterfaceMetadataMismatch);
1245 }
1246 if header.context.is_null() {
1247 return Err(NativePluginContractError::NullInterfaceContext {
1248 interface_id: descriptor.interface_id,
1249 instance_id: descriptor.instance_id.clone(),
1250 });
1251 }
1252
1253 macro_rules! full_table {
1254 ($type:ty, $required:expr, $variant:ident, [$($field:ident),+ $(,)?]) => {{
1255 ensure_table_size(descriptor, struct_size, $required)?;
1256 let table = unsafe { table_ptr.cast::<$type>().read_unaligned() };
1259 $(require_callback(descriptor, stringify!($field), table.$field.is_some())?;)+
1260 CheckedInterfaceTable::$variant(table)
1261 }};
1262 }
1263
1264 let table = if descriptor.interface_id == POST_DOWNLOAD_PROCESSOR_INTERFACE_ID {
1265 full_table!(
1266 VesperPostDownloadProcessor,
1267 VESPER_POST_DOWNLOAD_PROCESSOR_REQUIRED_SIZE,
1268 PostDownload,
1269 [capabilities_json, process_json, assemble_json]
1270 )
1271 } else if descriptor.interface_id == PIPELINE_EVENT_HOOK_INTERFACE_ID {
1272 full_table!(
1273 VesperPipelineEventHook,
1274 VESPER_PIPELINE_EVENT_HOOK_REQUIRED_SIZE,
1275 PipelineEventHook,
1276 [on_event_json]
1277 )
1278 } else if descriptor.interface_id == BENCHMARK_SINK_INTERFACE_ID {
1279 ensure_table_size(descriptor, struct_size, VESPER_BENCHMARK_SINK_REQUIRED_SIZE)?;
1280 let on_event_batch_json =
1281 unsafe {
1283 read_field::<Option<player_plugin_abi::VesperJsonCallFn>>(
1284 table_ptr,
1285 struct_size,
1286 offset_of!(VesperBenchmarkSink, on_event_batch_json) as u32,
1287 )
1288 }
1289 .flatten();
1290 require_callback(
1291 descriptor,
1292 "on_event_batch_json",
1293 on_event_batch_json.is_some(),
1294 )?;
1295 let flush_json =
1296 unsafe {
1299 read_field::<Option<player_plugin_abi::VesperGetJsonFn>>(
1300 table_ptr,
1301 struct_size,
1302 offset_of!(VesperBenchmarkSink, flush_json) as u32,
1303 )
1304 }
1305 .flatten();
1306 CheckedInterfaceTable::BenchmarkSink(VesperBenchmarkSink {
1307 header,
1308 on_event_batch_json,
1309 flush_json,
1310 })
1311 } else if descriptor.interface_id == NATIVE_DECODER_INTERFACE_ID {
1312 ensure_table_size(descriptor, struct_size, VESPER_NATIVE_DECODER_REQUIRED_SIZE)?;
1313 let mut table = unsafe { read_decoder_prefix(table_ptr, struct_size, header) };
1315 require_callback(
1316 descriptor,
1317 "capabilities_json",
1318 table.capabilities_json.is_some(),
1319 )?;
1320 require_callback(
1321 descriptor,
1322 "native_requirements_json",
1323 table.native_requirements_json.is_some(),
1324 )?;
1325 require_callback(
1326 descriptor,
1327 "open_session_json",
1328 table.open_session_json.is_some(),
1329 )?;
1330 require_callback(descriptor, "send_packet", table.send_packet.is_some())?;
1331 require_callback(
1332 descriptor,
1333 "receive_native_frame",
1334 table.receive_native_frame.is_some(),
1335 )?;
1336 require_callback(
1337 descriptor,
1338 "release_native_frame",
1339 table.release_native_frame.is_some(),
1340 )?;
1341 require_callback(descriptor, "flush_session", table.flush_session.is_some())?;
1342 require_callback(descriptor, "close_session", table.close_session.is_some())?;
1343 table.receive_pcm_frame =
1344 unsafe {
1346 read_field(
1347 table_ptr,
1348 struct_size,
1349 offset_of!(VesperNativeDecoder, receive_pcm_frame) as u32,
1350 )
1351 }
1352 .flatten();
1353 CheckedInterfaceTable::NativeDecoder(table)
1354 } else if descriptor.interface_id == FRAME_PROCESSOR_INTERFACE_ID {
1355 full_table!(
1356 VesperFrameProcessor,
1357 VESPER_FRAME_PROCESSOR_REQUIRED_SIZE,
1358 FrameProcessor,
1359 [
1360 capabilities_json,
1361 open_session_json,
1362 submit_frame_json,
1363 receive_frame,
1364 release_frame,
1365 flush_session,
1366 close_session
1367 ]
1368 )
1369 } else if descriptor.interface_id == AUDIO_PROCESSOR_INTERFACE_ID {
1370 full_table!(
1371 VesperAudioProcessor,
1372 VESPER_AUDIO_PROCESSOR_REQUIRED_SIZE,
1373 AudioProcessor,
1374 [
1375 capabilities_json,
1376 open_session_json,
1377 configure_session_json,
1378 process_pcm_frame,
1379 flush_session,
1380 close_session
1381 ]
1382 )
1383 } else if descriptor.interface_id == SOURCE_NORMALIZER_PACKET_INTERFACE_ID {
1384 ensure_table_size(
1385 descriptor,
1386 struct_size,
1387 VESPER_SOURCE_NORMALIZER_PACKET_REQUIRED_SIZE,
1388 )?;
1389 let mut table = unsafe { read_packet_prefix(table_ptr, struct_size, header) };
1391 require_callback(
1392 descriptor,
1393 "capabilities_json",
1394 table.capabilities_json.is_some(),
1395 )?;
1396 require_callback(
1397 descriptor,
1398 "open_session_json",
1399 table.open_session_json.is_some(),
1400 )?;
1401 require_callback(descriptor, "read_packet", table.read_packet.is_some())?;
1402 require_callback(descriptor, "release_packet", table.release_packet.is_some())?;
1403 require_callback(descriptor, "flush_session", table.flush_session.is_some())?;
1404 require_callback(descriptor, "close_session", table.close_session.is_some())?;
1405 table.seek_session_json =
1406 unsafe {
1408 read_field(
1409 table_ptr,
1410 struct_size,
1411 offset_of!(VesperSourceNormalizerPacket, seek_session_json) as u32,
1412 )
1413 }
1414 .flatten();
1415 CheckedInterfaceTable::SourceNormalizerPacket(table)
1416 } else if descriptor.interface_id == SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID {
1417 full_table!(
1418 VesperSourceNormalizerResource,
1419 VESPER_SOURCE_NORMALIZER_RESOURCE_REQUIRED_SIZE,
1420 SourceNormalizerResource,
1421 [
1422 capabilities_json,
1423 open_session_json,
1424 poll_session,
1425 wait_session_update,
1426 cancel_session,
1427 close_session
1428 ]
1429 )
1430 } else {
1431 return Ok(CheckedInterfaceTable::Unknown);
1432 };
1433 Ok(table)
1434}
1435
1436fn ensure_table_size(
1437 descriptor: &CheckedInterfaceDescriptor,
1438 actual: u32,
1439 required: u32,
1440) -> Result<(), NativePluginContractError> {
1441 if actual < required {
1442 Err(NativePluginContractError::TruncatedInterface {
1443 interface_id: descriptor.interface_id,
1444 instance_id: descriptor.instance_id.clone(),
1445 required,
1446 actual,
1447 })
1448 } else {
1449 Ok(())
1450 }
1451}
1452
1453fn require_callback(
1454 descriptor: &CheckedInterfaceDescriptor,
1455 callback: &'static str,
1456 present: bool,
1457) -> Result<(), NativePluginContractError> {
1458 if present {
1459 Ok(())
1460 } else {
1461 Err(NativePluginContractError::MissingInterfaceCallback {
1462 interface_id: descriptor.interface_id,
1463 instance_id: descriptor.instance_id.clone(),
1464 callback,
1465 })
1466 }
1467}
1468
1469unsafe fn read_field<T: Copy>(
1470 table_ptr: *const VesperInterfaceHeader,
1471 struct_size: u32,
1472 offset: u32,
1473) -> Option<T> {
1474 if !abi_contains(struct_size, offset, size_of::<T>() as u32) {
1475 return None;
1476 }
1477 Some(unsafe {
1481 table_ptr
1482 .cast::<u8>()
1483 .add(offset as usize)
1484 .cast::<T>()
1485 .read_unaligned()
1486 })
1487}
1488
1489unsafe fn read_decoder_prefix(
1490 table_ptr: *const VesperInterfaceHeader,
1491 struct_size: u32,
1492 header: VesperInterfaceHeader,
1493) -> VesperNativeDecoder {
1494 macro_rules! field {
1495 ($name:ident) => {
1496 unsafe {
1498 read_field(
1499 table_ptr,
1500 struct_size,
1501 offset_of!(VesperNativeDecoder, $name) as u32,
1502 )
1503 }
1504 .flatten()
1505 };
1506 }
1507 VesperNativeDecoder {
1508 header,
1509 capabilities_json: field!(capabilities_json),
1510 native_requirements_json: field!(native_requirements_json),
1511 open_session_json: field!(open_session_json),
1512 send_packet: field!(send_packet),
1513 receive_native_frame: field!(receive_native_frame),
1514 release_native_frame: field!(release_native_frame),
1515 flush_session: field!(flush_session),
1516 close_session: field!(close_session),
1517 receive_pcm_frame: None,
1518 }
1519}
1520
1521unsafe fn read_packet_prefix(
1522 table_ptr: *const VesperInterfaceHeader,
1523 struct_size: u32,
1524 header: VesperInterfaceHeader,
1525) -> VesperSourceNormalizerPacket {
1526 macro_rules! field {
1527 ($name:ident) => {
1528 unsafe {
1530 read_field(
1531 table_ptr,
1532 struct_size,
1533 offset_of!(VesperSourceNormalizerPacket, $name) as u32,
1534 )
1535 }
1536 .flatten()
1537 };
1538 }
1539 VesperSourceNormalizerPacket {
1540 header,
1541 capabilities_json: field!(capabilities_json),
1542 open_session_json: field!(open_session_json),
1543 read_packet: field!(read_packet),
1544 release_packet: field!(release_packet),
1545 flush_session: field!(flush_session),
1546 close_session: field!(close_session),
1547 seek_session_json: None,
1548 }
1549}
1550
1551#[cfg(test)]
1552mod tests {
1553 use std::sync::atomic::{AtomicUsize, Ordering};
1554
1555 use player_plugin_abi::{VESPER_INTERFACE_MINOR, VesperJsonOut, VesperPluginEntryPoint};
1556
1557 use super::*;
1558
1559 const PLUGIN_ID: &[u8] = b"dev.vesper.fixture";
1560 const PLUGIN_NAME: &[u8] = b"Plugin fixture";
1561 const INSTANCE_ID: &[u8] = b"dev.vesper.fixture.event-hook";
1562 const SECOND_INSTANCE_ID: &[u8] = b"dev.vesper.fixture.event-hook-secondary";
1563 const BAD_INSTANCE_ID: &[u8] = b"dev.vesper.fixture.bad-frame";
1564 const UNKNOWN_INSTANCE_ID: &[u8] = b"dev.vesper.fixture.future";
1565 const UNKNOWN_INTERFACE_ID: VesperInterfaceId = VesperInterfaceId([0x55; 16]);
1566
1567 struct FixtureOwner {
1568 destroyed: AtomicUsize,
1569 query_calls: AtomicUsize,
1570 table: *const VesperInterfaceHeader,
1571 table_size_override: u32,
1572 }
1573
1574 unsafe extern "C" fn no_op_free(_owner: *mut c_void, _bytes: VesperOwnedBytes) {}
1575
1576 unsafe extern "C" fn destroy(owner: *mut c_void) {
1577 let owner = unsafe { &*owner.cast::<FixtureOwner>() };
1579 owner.destroyed.fetch_add(1, Ordering::SeqCst);
1580 }
1581
1582 unsafe extern "C" fn interface_at(
1583 _owner: *mut c_void,
1584 index: u32,
1585 out: *mut VesperInterfaceDescriptor,
1586 ) -> VesperStatus {
1587 if index != 0 || out.is_null() {
1588 return status::NOT_FOUND;
1589 }
1590 let out = unsafe { &mut *out };
1592 *out = VesperInterfaceDescriptor {
1593 struct_size: size_of::<VesperInterfaceDescriptor>() as u32,
1594 interface_id: PIPELINE_EVENT_HOOK_INTERFACE_ID,
1595 major: VESPER_INTERFACE_MAJOR,
1596 minor: VESPER_INTERFACE_MINOR,
1597 instance_id: VesperByteSlice {
1598 data: INSTANCE_ID.as_ptr(),
1599 len: INSTANCE_ID.len() as u64,
1600 },
1601 };
1602 status::OK
1603 }
1604
1605 unsafe extern "C" fn unknown_interface_at(
1606 _owner: *mut c_void,
1607 index: u32,
1608 out: *mut VesperInterfaceDescriptor,
1609 ) -> VesperStatus {
1610 if index != 0 || out.is_null() {
1611 return status::NOT_FOUND;
1612 }
1613 unsafe {
1615 *out = VesperInterfaceDescriptor {
1616 struct_size: size_of::<VesperInterfaceDescriptor>() as u32,
1617 interface_id: UNKNOWN_INTERFACE_ID,
1618 major: 42,
1619 minor: 7,
1620 instance_id: VesperByteSlice {
1621 data: UNKNOWN_INSTANCE_ID.as_ptr(),
1622 len: UNKNOWN_INSTANCE_ID.len() as u64,
1623 },
1624 }
1625 };
1626 status::OK
1627 }
1628
1629 unsafe extern "C" fn multiple_hooks_interface_at(
1630 _owner: *mut c_void,
1631 index: u32,
1632 out: *mut VesperInterfaceDescriptor,
1633 ) -> VesperStatus {
1634 let instance_id = match index {
1635 0 => INSTANCE_ID,
1636 1 => SECOND_INSTANCE_ID,
1637 _ => return status::NOT_FOUND,
1638 };
1639 if out.is_null() {
1640 return status::INVALID_ARGUMENT;
1641 }
1642 unsafe {
1644 *out = VesperInterfaceDescriptor {
1645 struct_size: size_of::<VesperInterfaceDescriptor>() as u32,
1646 interface_id: PIPELINE_EVENT_HOOK_INTERFACE_ID,
1647 major: VESPER_INTERFACE_MAJOR,
1648 minor: VESPER_INTERFACE_MINOR,
1649 instance_id: VesperByteSlice {
1650 data: instance_id.as_ptr(),
1651 len: instance_id.len() as u64,
1652 },
1653 }
1654 };
1655 status::OK
1656 }
1657
1658 unsafe extern "C" fn unknown_status_interface_at(
1659 _owner: *mut c_void,
1660 _index: u32,
1661 _out: *mut VesperInterfaceDescriptor,
1662 ) -> VesperStatus {
1663 0xffff_ff00
1664 }
1665
1666 unsafe extern "C" fn mixed_interface_at(
1667 owner: *mut c_void,
1668 index: u32,
1669 out: *mut VesperInterfaceDescriptor,
1670 ) -> VesperStatus {
1671 if index == 1 {
1672 return unsafe { interface_at(owner, 0, out) };
1674 }
1675 if index != 0 || out.is_null() {
1676 return status::NOT_FOUND;
1677 }
1678 unsafe {
1680 *out = VesperInterfaceDescriptor {
1681 struct_size: size_of::<VesperInterfaceDescriptor>() as u32,
1682 interface_id: FRAME_PROCESSOR_INTERFACE_ID,
1683 major: VESPER_INTERFACE_MAJOR + 1,
1684 minor: 0,
1685 instance_id: VesperByteSlice {
1686 data: BAD_INSTANCE_ID.as_ptr(),
1687 len: BAD_INSTANCE_ID.len() as u64,
1688 },
1689 }
1690 };
1691 status::OK
1692 }
1693
1694 unsafe extern "C" fn query_interface(
1695 owner: *mut c_void,
1696 interface_id: *const VesperInterfaceId,
1697 _instance_id: VesperByteSlice,
1698 requested_major: u16,
1699 _minimum_minor: u16,
1700 out: *mut *const VesperInterfaceHeader,
1701 ) -> VesperStatus {
1702 if owner.is_null() || interface_id.is_null() || out.is_null() {
1703 return status::INVALID_ARGUMENT;
1704 }
1705 let owner = unsafe { &*owner.cast::<FixtureOwner>() };
1707 owner.query_calls.fetch_add(1, Ordering::SeqCst);
1708 let interface_id = unsafe { *interface_id };
1710 if interface_id != PIPELINE_EVENT_HOOK_INTERFACE_ID
1711 || requested_major != VESPER_INTERFACE_MAJOR
1712 {
1713 return status::NOT_FOUND;
1714 }
1715 if owner.table_size_override != 0 {
1716 unsafe {
1719 owner
1720 .table
1721 .cast_mut()
1722 .cast::<u32>()
1723 .write(owner.table_size_override)
1724 };
1725 }
1726 unsafe { *out = owner.table };
1728 status::OK
1729 }
1730
1731 unsafe extern "C" fn on_event(
1732 _context: *mut c_void,
1733 _input: VesperByteSlice,
1734 out: *mut VesperJsonOut,
1735 ) -> VesperStatus {
1736 if out.is_null() {
1737 status::INVALID_ARGUMENT
1738 } else {
1739 status::OK
1740 }
1741 }
1742
1743 fn root_for(owner: &mut FixtureOwner, plugin_id: &[u8]) -> VesperPluginRoot {
1744 VesperPluginRoot {
1745 struct_size: size_of::<VesperPluginRoot>() as u32,
1746 abi_major: VESPER_PLUGIN_ABI_MAJOR,
1747 abi_minor: VESPER_PLUGIN_ABI_MINOR,
1748 owner: std::ptr::from_mut(owner).cast(),
1749 plugin_id: VesperByteSlice {
1750 data: plugin_id.as_ptr(),
1751 len: plugin_id.len() as u64,
1752 },
1753 plugin_name: VesperByteSlice {
1754 data: PLUGIN_NAME.as_ptr(),
1755 len: PLUGIN_NAME.len() as u64,
1756 },
1757 interface_count: 1,
1758 reserved: 0,
1759 interface_at: Some(interface_at),
1760 query_interface: Some(query_interface),
1761 free_bytes: Some(no_op_free),
1762 destroy_owner: Some(destroy),
1763 }
1764 }
1765
1766 fn hook_table() -> VesperPipelineEventHook {
1767 VesperPipelineEventHook {
1768 header: VesperInterfaceHeader::new(
1769 size_of::<VesperPipelineEventHook>() as u32,
1770 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1771 VESPER_INTERFACE_MAJOR,
1772 VESPER_INTERFACE_MINOR,
1773 NonNull::<u8>::dangling().as_ptr().cast(),
1774 ),
1775 on_event_json: Some(on_event),
1776 }
1777 }
1778
1779 #[test]
1780 fn checked_root_enumerates_and_queries_typed_interface() {
1781 let mut hook = VesperPipelineEventHook {
1782 header: VesperInterfaceHeader::new(
1783 size_of::<VesperPipelineEventHook>() as u32,
1784 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1785 VESPER_INTERFACE_MAJOR,
1786 VESPER_INTERFACE_MINOR,
1787 NonNull::<u8>::dangling().as_ptr().cast(),
1788 ),
1789 on_event_json: Some(on_event),
1790 };
1791 let mut owner = FixtureOwner {
1792 destroyed: AtomicUsize::new(0),
1793 query_calls: AtomicUsize::new(0),
1794 table: std::ptr::from_mut(&mut hook).cast(),
1795 table_size_override: 0,
1796 };
1797 let root = root_for(&mut owner, PLUGIN_ID);
1798 let checked =
1799 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid root");
1801 assert_eq!(checked.plugin_id, "dev.vesper.fixture");
1802 assert_eq!(checked.plugin_name, "Plugin fixture");
1803 assert_eq!(checked.interfaces.len(), 1);
1804 assert!(checked.diagnostics.is_empty());
1805 assert!(matches!(
1806 checked.interfaces[0].table,
1807 CheckedInterfaceTable::PipelineEventHook(_)
1808 ));
1809 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 0);
1810 drop(checked);
1811 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1812 }
1813
1814 #[test]
1815 fn validation_failure_after_owner_creation_destroys_once() {
1816 let mut hook = VesperPipelineEventHook {
1817 header: VesperInterfaceHeader::new(
1818 size_of::<VesperPipelineEventHook>() as u32,
1819 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1820 VESPER_INTERFACE_MAJOR,
1821 VESPER_INTERFACE_MINOR,
1822 NonNull::<u8>::dangling().as_ptr().cast(),
1823 ),
1824 on_event_json: Some(on_event),
1825 };
1826 let mut owner = FixtureOwner {
1827 destroyed: AtomicUsize::new(0),
1828 query_calls: AtomicUsize::new(0),
1829 table: std::ptr::from_mut(&mut hook).cast(),
1830 table_size_override: 0,
1831 };
1832 let root = root_for(&mut owner, b"not-reverse-dns");
1833 let error =
1834 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect_err("invalid identity");
1836 assert!(matches!(
1837 error,
1838 NativePluginContractError::InvalidReverseDns { .. }
1839 ));
1840 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1841 }
1842
1843 #[test]
1844 fn missing_free_bytes_destroys_pending_owner_once() {
1845 let mut hook = VesperPipelineEventHook {
1846 header: VesperInterfaceHeader::new(
1847 size_of::<VesperPipelineEventHook>() as u32,
1848 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1849 VESPER_INTERFACE_MAJOR,
1850 VESPER_INTERFACE_MINOR,
1851 NonNull::<u8>::dangling().as_ptr().cast(),
1852 ),
1853 on_event_json: Some(on_event),
1854 };
1855 let mut owner = FixtureOwner {
1856 destroyed: AtomicUsize::new(0),
1857 query_calls: AtomicUsize::new(0),
1858 table: std::ptr::from_mut(&mut hook).cast(),
1859 table_size_override: 0,
1860 };
1861 let mut root = root_for(&mut owner, PLUGIN_ID);
1862 root.free_bytes = None;
1863 let error =
1864 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect_err("missing free_bytes");
1866 assert_eq!(
1867 error,
1868 NativePluginContractError::MissingRootField {
1869 field: "free_bytes"
1870 }
1871 );
1872 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1873 }
1874
1875 #[test]
1876 fn maximum_length_plugin_name_is_accepted() {
1877 let mut hook = VesperPipelineEventHook {
1878 header: VesperInterfaceHeader::new(
1879 size_of::<VesperPipelineEventHook>() as u32,
1880 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1881 VESPER_INTERFACE_MAJOR,
1882 VESPER_INTERFACE_MINOR,
1883 NonNull::<u8>::dangling().as_ptr().cast(),
1884 ),
1885 on_event_json: Some(on_event),
1886 };
1887 let mut owner = FixtureOwner {
1888 destroyed: AtomicUsize::new(0),
1889 query_calls: AtomicUsize::new(0),
1890 table: std::ptr::from_mut(&mut hook).cast(),
1891 table_size_override: 0,
1892 };
1893 let plugin_name = vec![b'n'; VESPER_MAX_PLUGIN_NAME_BYTES];
1894 let mut root = root_for(&mut owner, PLUGIN_ID);
1895 root.plugin_name = VesperByteSlice {
1896 data: plugin_name.as_ptr(),
1897 len: plugin_name.len() as u64,
1898 };
1899 let checked =
1900 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("maximum name");
1902 assert_eq!(checked.plugin_name.len(), VESPER_MAX_PLUGIN_NAME_BYTES);
1903 drop(checked);
1904 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1905 }
1906
1907 #[test]
1908 fn unknown_future_interface_is_retained_without_querying_it() {
1909 let mut hook = VesperPipelineEventHook {
1910 header: VesperInterfaceHeader::new(
1911 size_of::<VesperPipelineEventHook>() as u32,
1912 PIPELINE_EVENT_HOOK_INTERFACE_ID,
1913 VESPER_INTERFACE_MAJOR,
1914 VESPER_INTERFACE_MINOR,
1915 NonNull::<u8>::dangling().as_ptr().cast(),
1916 ),
1917 on_event_json: Some(on_event),
1918 };
1919 let mut owner = FixtureOwner {
1920 destroyed: AtomicUsize::new(0),
1921 query_calls: AtomicUsize::new(0),
1922 table: std::ptr::from_mut(&mut hook).cast(),
1923 table_size_override: 0,
1924 };
1925 let mut root = root_for(&mut owner, PLUGIN_ID);
1926 root.interface_at = Some(unknown_interface_at);
1927 let checked =
1928 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("future interface");
1930 assert_eq!(checked.interfaces.len(), 1);
1931 assert_eq!(checked.interfaces[0].descriptor.major, 42);
1932 assert!(matches!(
1933 checked.interfaces[0].table,
1934 CheckedInterfaceTable::Unknown
1935 ));
1936 assert!(checked.diagnostics.is_empty());
1937 assert_eq!(owner.query_calls.load(Ordering::SeqCst), 0);
1938 let loaded = LoadedNativePlugin::from_checked(checked);
1939 let unknown = loaded
1940 .unknown_interfaces()
1941 .next()
1942 .expect("unknown interface metadata");
1943 assert_eq!(unknown.interface_id, UNKNOWN_INTERFACE_ID.0);
1944 assert_eq!(unknown.major, 42);
1945 assert_eq!(unknown.minor, 7);
1946 assert_eq!(unknown.instance_id, "dev.vesper.fixture.future");
1947 drop(loaded);
1948 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1949 }
1950
1951 #[test]
1952 fn loaded_plugin_resolves_explicit_and_only_hook_instance() {
1953 let mut hook = hook_table();
1954 let mut owner = FixtureOwner {
1955 destroyed: AtomicUsize::new(0),
1956 query_calls: AtomicUsize::new(0),
1957 table: std::ptr::from_mut(&mut hook).cast(),
1958 table_size_override: 0,
1959 };
1960 let root = root_for(&mut owner, PLUGIN_ID);
1961 let checked =
1962 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid root");
1964 let loaded = LoadedNativePlugin::from_checked(checked);
1965 let implicit = PluginReference::new("dev.vesper.fixture", None, PluginTransport::Native)
1966 .expect("valid reference");
1967 let explicit = PluginReference::new(
1968 "dev.vesper.fixture",
1969 Some("dev.vesper.fixture.event-hook".to_owned()),
1970 PluginTransport::Native,
1971 )
1972 .expect("valid reference");
1973
1974 let implicit_hook = loaded
1975 .resolve_pipeline_event_hook(&implicit)
1976 .expect("only instance");
1977 let explicit_hook = loaded
1978 .resolve_pipeline_event_hook(&explicit)
1979 .expect("explicit instance");
1980 assert!(Arc::ptr_eq(&implicit_hook, &explicit_hook));
1981 drop(loaded);
1982 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 0);
1983 drop(implicit_hook);
1984 drop(explicit_hook);
1985 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
1986 }
1987
1988 #[test]
1989 fn loaded_plugin_requires_instance_for_multiple_hook_implementations() {
1990 let mut hook = hook_table();
1991 let mut owner = FixtureOwner {
1992 destroyed: AtomicUsize::new(0),
1993 query_calls: AtomicUsize::new(0),
1994 table: std::ptr::from_mut(&mut hook).cast(),
1995 table_size_override: 0,
1996 };
1997 let mut root = root_for(&mut owner, PLUGIN_ID);
1998 root.interface_count = 2;
1999 root.interface_at = Some(multiple_hooks_interface_at);
2000 let checked =
2001 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid root");
2003 let loaded = LoadedNativePlugin::from_checked(checked);
2004 let implicit = PluginReference::new("dev.vesper.fixture", None, PluginTransport::Native)
2005 .expect("valid reference");
2006 assert_eq!(
2007 loaded
2008 .resolve_pipeline_event_hook(&implicit)
2009 .err()
2010 .expect("ambiguous selection"),
2011 PluginSelectionError::Ambiguous {
2012 plugin_id: "dev.vesper.fixture".to_owned(),
2013 interface: "PipelineEventHook",
2014 count: 2,
2015 }
2016 );
2017
2018 let explicit = PluginReference::new(
2019 "dev.vesper.fixture",
2020 Some("dev.vesper.fixture.event-hook-secondary".to_owned()),
2021 PluginTransport::Native,
2022 )
2023 .expect("valid reference");
2024 let selected = loaded
2025 .resolve_pipeline_event_hook(&explicit)
2026 .expect("explicit instance");
2027 drop(selected);
2028 drop(loaded);
2029 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2030 }
2031
2032 #[test]
2033 fn failed_wrapper_still_requires_explicit_instance_and_preserves_index() {
2034 let mut hook = hook_table();
2035 let mut owner = FixtureOwner {
2036 destroyed: AtomicUsize::new(0),
2037 query_calls: AtomicUsize::new(0),
2038 table: std::ptr::from_mut(&mut hook).cast(),
2039 table_size_override: 0,
2040 };
2041 let root = root_for(&mut owner, PLUGIN_ID);
2042 let mut checked =
2043 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid root");
2045 checked.interfaces.push(CheckedInterface {
2046 index: 1,
2047 descriptor: CheckedInterfaceDescriptor {
2048 interface_id: PIPELINE_EVENT_HOOK_INTERFACE_ID,
2049 major: VESPER_INTERFACE_MAJOR,
2050 minor: VESPER_INTERFACE_MINOR,
2051 instance_id: "dev.vesper.fixture.event-hook-secondary".to_owned(),
2052 },
2053 table: CheckedInterfaceTable::PipelineEventHook(VesperPipelineEventHook {
2054 header: VesperInterfaceHeader::new(
2055 size_of::<VesperPipelineEventHook>() as u32,
2056 PIPELINE_EVENT_HOOK_INTERFACE_ID,
2057 VESPER_INTERFACE_MAJOR,
2058 VESPER_INTERFACE_MINOR,
2059 NonNull::<u8>::dangling().as_ptr().cast(),
2060 ),
2061 on_event_json: None,
2062 }),
2063 });
2064
2065 let loaded = LoadedNativePlugin::from_checked(checked);
2066 assert_eq!(loaded.diagnostics().len(), 1);
2067 assert_eq!(loaded.interfaces().len(), 2);
2068 assert_eq!(
2069 loaded.interfaces()[0].state,
2070 PluginInterfaceState::Available
2071 );
2072 assert_eq!(
2073 loaded.interfaces()[1].state,
2074 PluginInterfaceState::Unavailable
2075 );
2076 let diagnostic = &loaded.diagnostics()[0];
2077 assert_eq!(diagnostic.index, Some(1));
2078 assert_eq!(
2079 diagnostic.kind,
2080 PluginContractDiagnosticKind::ContractViolation
2081 );
2082 let metadata = diagnostic.interface.as_ref().expect("interface metadata");
2083 assert_eq!(metadata.interface_id, PIPELINE_EVENT_HOOK_INTERFACE_ID.0);
2084 assert_eq!(metadata.major, VESPER_INTERFACE_MAJOR);
2085 assert_eq!(metadata.minor, VESPER_INTERFACE_MINOR);
2086 assert_eq!(
2087 metadata.instance_id,
2088 "dev.vesper.fixture.event-hook-secondary"
2089 );
2090
2091 let implicit = PluginReference::new("dev.vesper.fixture", None, PluginTransport::Native)
2092 .expect("valid reference");
2093 assert_eq!(
2094 loaded
2095 .resolve_pipeline_event_hook(&implicit)
2096 .err()
2097 .expect("advertised ambiguity"),
2098 PluginSelectionError::Ambiguous {
2099 plugin_id: "dev.vesper.fixture".to_owned(),
2100 interface: "PipelineEventHook",
2101 count: 2,
2102 }
2103 );
2104 let unavailable = PluginReference::new(
2105 "dev.vesper.fixture",
2106 Some("dev.vesper.fixture.event-hook-secondary".to_owned()),
2107 PluginTransport::Native,
2108 )
2109 .expect("valid reference");
2110 assert_eq!(
2111 loaded
2112 .resolve_pipeline_event_hook(&unavailable)
2113 .err()
2114 .expect("unavailable instance"),
2115 PluginSelectionError::InstanceUnavailable {
2116 plugin_id: "dev.vesper.fixture".to_owned(),
2117 interface: "PipelineEventHook",
2118 instance_id: "dev.vesper.fixture.event-hook-secondary".to_owned(),
2119 }
2120 );
2121 drop(loaded);
2122 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2123 }
2124
2125 #[test]
2126 fn loaded_plugin_rejects_transport_and_plugin_id_mismatch() {
2127 let mut hook = hook_table();
2128 let mut owner = FixtureOwner {
2129 destroyed: AtomicUsize::new(0),
2130 query_calls: AtomicUsize::new(0),
2131 table: std::ptr::from_mut(&mut hook).cast(),
2132 table_size_override: 0,
2133 };
2134 let root = root_for(&mut owner, PLUGIN_ID);
2135 let checked =
2136 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid root");
2138 let loaded = LoadedNativePlugin::from_checked(checked);
2139
2140 let wasm = PluginReference::new("dev.vesper.fixture", None, PluginTransport::Wasm)
2141 .expect("valid reference");
2142 assert_eq!(
2143 loaded
2144 .resolve_pipeline_event_hook(&wasm)
2145 .err()
2146 .expect("transport mismatch"),
2147 PluginSelectionError::TransportMismatch {
2148 expected: PluginTransport::Native,
2149 actual: PluginTransport::Wasm,
2150 }
2151 );
2152
2153 let other_plugin = PluginReference::new("dev.vesper.other", None, PluginTransport::Native)
2154 .expect("valid reference");
2155 assert_eq!(
2156 loaded
2157 .resolve_pipeline_event_hook(&other_plugin)
2158 .err()
2159 .expect("plugin id mismatch"),
2160 PluginSelectionError::PluginIdMismatch {
2161 requested: "dev.vesper.other".to_owned(),
2162 loaded: "dev.vesper.fixture".to_owned(),
2163 }
2164 );
2165 drop(loaded);
2166 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2167 }
2168
2169 #[test]
2170 fn unknown_root_callback_status_is_preserved_in_diagnostics() {
2171 let mut hook = hook_table();
2172 let mut owner = FixtureOwner {
2173 destroyed: AtomicUsize::new(0),
2174 query_calls: AtomicUsize::new(0),
2175 table: std::ptr::from_mut(&mut hook).cast(),
2176 table_size_override: 0,
2177 };
2178 let mut root = root_for(&mut owner, PLUGIN_ID);
2179 root.interface_at = Some(unknown_status_interface_at);
2180 let checked =
2181 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("isolated failure");
2183 let loaded = LoadedNativePlugin::from_checked(checked);
2184 assert_eq!(loaded.diagnostics().len(), 1);
2185 assert!(loaded.diagnostics()[0].message.contains("4294967040"));
2186 drop(loaded);
2187 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2188 }
2189
2190 #[test]
2191 fn invalid_interface_does_not_block_valid_sibling() {
2192 let mut hook = VesperPipelineEventHook {
2193 header: VesperInterfaceHeader::new(
2194 size_of::<VesperPipelineEventHook>() as u32,
2195 PIPELINE_EVENT_HOOK_INTERFACE_ID,
2196 VESPER_INTERFACE_MAJOR,
2197 VESPER_INTERFACE_MINOR,
2198 NonNull::<u8>::dangling().as_ptr().cast(),
2199 ),
2200 on_event_json: Some(on_event),
2201 };
2202 let mut owner = FixtureOwner {
2203 destroyed: AtomicUsize::new(0),
2204 query_calls: AtomicUsize::new(0),
2205 table: std::ptr::from_mut(&mut hook).cast(),
2206 table_size_override: 0,
2207 };
2208 let mut root = root_for(&mut owner, PLUGIN_ID);
2209 root.interface_count = 2;
2210 root.interface_at = Some(mixed_interface_at);
2211 let checked =
2212 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("valid sibling");
2214 assert_eq!(checked.interfaces.len(), 1);
2215 assert!(matches!(
2216 checked.interfaces[0].table,
2217 CheckedInterfaceTable::PipelineEventHook(_)
2218 ));
2219 assert_eq!(checked.diagnostics.len(), 1);
2220 assert_eq!(checked.diagnostics[0].index, 0);
2221 assert!(matches!(
2222 checked.diagnostics[0].error,
2223 NativePluginContractError::UnsupportedInterfaceVersion { .. }
2224 ));
2225 assert_eq!(owner.query_calls.load(Ordering::SeqCst), 1);
2226 let loaded = LoadedNativePlugin::from_checked(checked);
2227 assert_eq!(loaded.diagnostics().len(), 1);
2228 assert_eq!(
2229 loaded.diagnostics()[0].kind,
2230 PluginContractDiagnosticKind::Compatibility
2231 );
2232 let implicit = PluginReference::new("dev.vesper.fixture", None, PluginTransport::Native)
2233 .expect("valid implicit reference");
2234 assert_eq!(
2235 loaded
2236 .resolve_frame_processor(&implicit)
2237 .err()
2238 .expect("known rejected interface is unavailable"),
2239 PluginSelectionError::InterfaceUnavailable {
2240 plugin_id: "dev.vesper.fixture".to_owned(),
2241 interface: "FrameProcessor",
2242 }
2243 );
2244 let explicit = PluginReference::new(
2245 "dev.vesper.fixture",
2246 Some(String::from_utf8_lossy(BAD_INSTANCE_ID).into_owned()),
2247 PluginTransport::Native,
2248 )
2249 .expect("valid explicit reference");
2250 assert_eq!(
2251 loaded
2252 .resolve_frame_processor(&explicit)
2253 .err()
2254 .expect("known rejected instance is unavailable"),
2255 PluginSelectionError::InstanceUnavailable {
2256 plugin_id: "dev.vesper.fixture".to_owned(),
2257 interface: "FrameProcessor",
2258 instance_id: String::from_utf8_lossy(BAD_INSTANCE_ID).into_owned(),
2259 }
2260 );
2261 drop(loaded);
2262 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2263 }
2264
2265 #[test]
2266 fn zero_interface_root_is_rejected_and_destroyed() {
2267 let mut hook = VesperPipelineEventHook {
2268 header: VesperInterfaceHeader::new(
2269 size_of::<VesperPipelineEventHook>() as u32,
2270 PIPELINE_EVENT_HOOK_INTERFACE_ID,
2271 VESPER_INTERFACE_MAJOR,
2272 VESPER_INTERFACE_MINOR,
2273 NonNull::<u8>::dangling().as_ptr().cast(),
2274 ),
2275 on_event_json: Some(on_event),
2276 };
2277 let mut owner = FixtureOwner {
2278 destroyed: AtomicUsize::new(0),
2279 query_calls: AtomicUsize::new(0),
2280 table: std::ptr::from_mut(&mut hook).cast(),
2281 table_size_override: 0,
2282 };
2283 let mut root = root_for(&mut owner, PLUGIN_ID);
2284 root.interface_count = 0;
2285 let error =
2286 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect_err("zero interfaces");
2288 assert_eq!(error, NativePluginContractError::NoInterfaces);
2289 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2290 }
2291
2292 #[test]
2293 fn future_root_minor_is_rejected_as_a_compatibility_error() {
2294 let mut hook = hook_table();
2295 let mut owner = FixtureOwner {
2296 destroyed: AtomicUsize::new(0),
2297 query_calls: AtomicUsize::new(0),
2298 table: std::ptr::from_mut(&mut hook).cast(),
2299 table_size_override: 0,
2300 };
2301 let mut root = root_for(&mut owner, PLUGIN_ID);
2302 root.abi_minor = VESPER_PLUGIN_ABI_MINOR.saturating_add(1);
2303
2304 let error =
2305 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect_err("future root minor");
2307 assert_eq!(
2308 error,
2309 NativePluginContractError::RootVersionMismatch {
2310 expected_major: VESPER_PLUGIN_ABI_MAJOR,
2311 expected_minor: VESPER_PLUGIN_ABI_MINOR,
2312 actual_major: VESPER_PLUGIN_ABI_MAJOR,
2313 actual_minor: VESPER_PLUGIN_ABI_MINOR.saturating_add(1),
2314 }
2315 );
2316 assert_eq!(
2317 error.diagnostic_kind(),
2318 PluginContractDiagnosticKind::Compatibility
2319 );
2320 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 0);
2321 }
2322
2323 #[test]
2324 fn truncated_interface_is_isolated_before_callback_read() {
2325 let original_size = size_of::<VesperPipelineEventHook>() as u32;
2326 let mut hook = VesperPipelineEventHook {
2327 header: VesperInterfaceHeader::new(
2328 original_size,
2329 PIPELINE_EVENT_HOOK_INTERFACE_ID,
2330 VESPER_INTERFACE_MAJOR,
2331 VESPER_INTERFACE_MINOR,
2332 NonNull::<u8>::dangling().as_ptr().cast(),
2333 ),
2334 on_event_json: Some(on_event),
2335 };
2336 let mut owner = FixtureOwner {
2337 destroyed: AtomicUsize::new(0),
2338 query_calls: AtomicUsize::new(0),
2339 table: std::ptr::from_mut(&mut hook).cast(),
2340 table_size_override: VESPER_PIPELINE_EVENT_HOOK_REQUIRED_SIZE - 1,
2341 };
2342 let root = root_for(&mut owner, PLUGIN_ID);
2343 let checked =
2344 unsafe { CheckedPluginRoot::from_raw(&root, None) }.expect("isolated table failure");
2346 assert!(checked.interfaces.is_empty());
2347 assert_eq!(checked.diagnostics.len(), 1);
2348 assert!(matches!(
2349 checked.diagnostics[0].error,
2350 NativePluginContractError::TruncatedInterface { .. }
2351 ));
2352 assert_eq!(
2353 checked.diagnostics[0].error.diagnostic_kind(),
2354 PluginContractDiagnosticKind::ContractViolation
2355 );
2356 drop(checked);
2357 assert_eq!(owner.destroyed.load(Ordering::SeqCst), 1);
2358 }
2359
2360 #[test]
2361 fn audio_processor_table_rejects_missing_required_process_callback() {
2362 unsafe extern "C" fn get_json(
2363 _context: *mut c_void,
2364 _out: *mut player_plugin_abi::VesperJsonOut,
2365 ) -> VesperStatus {
2366 status::FAILURE
2367 }
2368 unsafe extern "C" fn open(
2369 _context: *mut c_void,
2370 _config: VesperByteSlice,
2371 _out: *mut player_plugin_abi::VesperOpenSessionOut,
2372 ) -> VesperStatus {
2373 status::FAILURE
2374 }
2375 unsafe extern "C" fn configure(
2376 _context: *mut c_void,
2377 _session_id: u64,
2378 _policy: VesperByteSlice,
2379 _out: *mut player_plugin_abi::VesperJsonOut,
2380 ) -> VesperStatus {
2381 status::FAILURE
2382 }
2383 unsafe extern "C" fn session_operation(
2384 _context: *mut c_void,
2385 _session_id: u64,
2386 _out: *mut player_plugin_abi::VesperJsonOut,
2387 ) -> VesperStatus {
2388 status::FAILURE
2389 }
2390
2391 let table = VesperAudioProcessor {
2392 header: VesperInterfaceHeader::new(
2393 size_of::<VesperAudioProcessor>() as u32,
2394 AUDIO_PROCESSOR_INTERFACE_ID,
2395 VESPER_INTERFACE_MAJOR,
2396 VESPER_INTERFACE_MINOR,
2397 NonNull::<u8>::dangling().as_ptr().cast(),
2398 ),
2399 capabilities_json: Some(get_json),
2400 open_session_json: Some(open),
2401 configure_session_json: Some(configure),
2402 process_pcm_frame: None,
2403 flush_session: Some(session_operation),
2404 close_session: Some(session_operation),
2405 };
2406 let descriptor = CheckedInterfaceDescriptor {
2407 interface_id: AUDIO_PROCESSOR_INTERFACE_ID,
2408 major: VESPER_INTERFACE_MAJOR,
2409 minor: VESPER_INTERFACE_MINOR,
2410 instance_id: "dev.vesper.fixture.audio".to_owned(),
2411 };
2412 let error =
2413 unsafe { check_interface_table(std::ptr::from_ref(&table.header), &descriptor) }
2415 .expect_err("missing audio process callback");
2416 assert_eq!(
2417 error,
2418 NativePluginContractError::MissingInterfaceCallback {
2419 interface_id: AUDIO_PROCESSOR_INTERFACE_ID,
2420 instance_id: "dev.vesper.fixture.audio".to_owned(),
2421 callback: "process_pcm_frame",
2422 }
2423 );
2424 }
2425
2426 #[test]
2427 fn audio_processor_table_rejects_truncated_required_prefix() {
2428 let table = VesperAudioProcessor {
2429 header: VesperInterfaceHeader::new(
2430 VESPER_AUDIO_PROCESSOR_REQUIRED_SIZE - 1,
2431 AUDIO_PROCESSOR_INTERFACE_ID,
2432 VESPER_INTERFACE_MAJOR,
2433 VESPER_INTERFACE_MINOR,
2434 NonNull::<u8>::dangling().as_ptr().cast(),
2435 ),
2436 capabilities_json: None,
2437 open_session_json: None,
2438 configure_session_json: None,
2439 process_pcm_frame: None,
2440 flush_session: None,
2441 close_session: None,
2442 };
2443 let descriptor = CheckedInterfaceDescriptor {
2444 interface_id: AUDIO_PROCESSOR_INTERFACE_ID,
2445 major: VESPER_INTERFACE_MAJOR,
2446 minor: VESPER_INTERFACE_MINOR,
2447 instance_id: "dev.vesper.fixture.audio".to_owned(),
2448 };
2449 let error =
2450 unsafe { check_interface_table(std::ptr::from_ref(&table.header), &descriptor) }
2453 .expect_err("truncated audio processor table");
2454 assert!(matches!(
2455 error,
2456 NativePluginContractError::TruncatedInterface { .. }
2457 ));
2458 }
2459
2460 #[test]
2461 fn entry_signature_matches_root_pointer_contract() {
2462 unsafe extern "C" fn entry() -> *const VesperPluginRoot {
2463 std::ptr::null()
2464 }
2465 let _entry: VesperPluginEntryPoint = entry;
2466 }
2467}