Skip to main content

player_plugin_loader/
native_abi.rs

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            // SAFETY: the root transferred this unique owner to the host and
226            // supplied the matching destroy callback. The guard is armed only
227            // until ownership moves into `PluginOwner`.
228            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
242// SAFETY: the native root contract requires the owner and all interface factories
243// to support concurrent shared calls. Sessions remain separately serialized.
244unsafe impl Send for PluginOwner {}
245// SAFETY: same contract as above; the pointer is never dereferenced by the
246// loader and is only passed back to validated plugin callbacks.
247unsafe 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        // SAFETY: the allocation came from this root owner and the checked
255        // wrapper transfers it back exactly once.
256        unsafe { (self.free_bytes)(self.owner.as_ptr(), bytes) };
257    }
258}
259
260impl Drop for PluginOwner {
261    fn drop(&mut self) {
262        // SAFETY: `owner` is unique to this root and `PluginOwner` is the only
263        // object that invokes the validated destroy callback.
264        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        // SAFETY: the entry contract guarantees a readable root prefix. Read
889        // only `struct_size` before deciding whether the complete root prefix is
890        // available.
891        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        // SAFETY: the size check above proves that the complete root prefix
899        // is readable under the entry contract.
900        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            // SAFETY: identity bytes are borrowed from the root owner until it
932            // is destroyed, and they are copied before returning.
933            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            // SAFETY: same root-owned identity contract as `plugin_id`.
942            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            // SAFETY: the validated root callbacks borrow only host-owned
975            // inputs for each synchronous call. Returned descriptors and
976            // tables remain backed by `owner` while copied and checked.
977            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    // SAFETY: all pointers are host-owned for this synchronous call and the
1037    // callback was validated before this helper was called.
1038    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        // SAFETY: the descriptor borrows owner-backed bytes which are copied
1052        // before the next plugin call.
1053        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    // SAFETY: query inputs are borrowed for this call, and the output pointer
1111    // is host-owned. A successful result promises an owner-backed table.
1112    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        // SAFETY: query success promises a root-owned table. The helper reads
1134        // only fields covered by its advertised size.
1135        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    // SAFETY: the caller guarantees the borrowed range is readable for the
1197    // root or callback lifetime, and the bounded length was checked above.
1198    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    // SAFETY: query success promises at least a readable `struct_size` word.
1231    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    // SAFETY: the size check proves the common header prefix is readable.
1239    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            // SAFETY: the required size for these ABI tables covers the full
1257            // concrete table, including every field copied here.
1258            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            // SAFETY: the required prefix covers this field.
1282            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            // SAFETY: optional fields are copied only when their complete
1297            // storage is present in the advertised table size.
1298            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        // SAFETY: each required field lies within the validated prefix.
1314        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            // SAFETY: optional tail read is size-gated.
1345            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        // SAFETY: each required field lies within the validated prefix.
1390        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            // SAFETY: optional tail read is size-gated.
1407            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    // SAFETY: the size gate above proves the complete field lies in the table
1478    // allocation promised by the plugin. Unaligned reads avoid assumptions
1479    // about a maliciously offset base pointer.
1480    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            // SAFETY: the caller validated the required decoder prefix.
1497            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            // SAFETY: the caller validated the required packet prefix.
1529            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        // SAFETY: tests pass a live `FixtureOwner` as the root owner.
1578        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        // SAFETY: the loader provides a live host-initialized output.
1591        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        // SAFETY: the loader provides a live host-initialized output.
1614        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        // SAFETY: the loader provides a live host-initialized output.
1643        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            // SAFETY: this forwards the same validated callback arguments.
1673            return unsafe { interface_at(owner, 0, out) };
1674        }
1675        if index != 0 || out.is_null() {
1676            return status::NOT_FOUND;
1677        }
1678        // SAFETY: the loader provides a live host-initialized output.
1679        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        // SAFETY: pointers come from the checked test root call.
1706        let owner = unsafe { &*owner.cast::<FixtureOwner>() };
1707        owner.query_calls.fetch_add(1, Ordering::SeqCst);
1708        // SAFETY: validated non-null above and borrowed for this call.
1709        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            // SAFETY: the fixture table is writable for the duration of the
1717            // test and restored by the caller after validation.
1718            unsafe {
1719                owner
1720                    .table
1721                    .cast_mut()
1722                    .cast::<u32>()
1723                    .write(owner.table_size_override)
1724            };
1725        }
1726        // SAFETY: validated non-null above.
1727        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            // SAFETY: the complete fixture root and table outlive validation.
1800            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            // SAFETY: the complete fixture root outlives validation.
1835            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            // SAFETY: the complete fixture root outlives validation.
1865            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            // SAFETY: the fixture root, name, and table outlive validation.
1901            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            // SAFETY: the complete fixture root outlives validation.
1929            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            // SAFETY: the complete fixture root and table outlive validation.
1963            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            // SAFETY: the complete fixture root and table outlive validation.
2002            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            // SAFETY: the complete fixture root and table outlive validation.
2044            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            // SAFETY: the complete fixture root and table outlive validation.
2137            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            // SAFETY: the complete fixture root outlives validation.
2182            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            // SAFETY: the complete fixture root and table outlive validation.
2213            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            // SAFETY: the complete fixture root outlives validation.
2287            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            // SAFETY: the complete fixture root outlives validation.
2306            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            // SAFETY: the fixture root and header outlive validation.
2345            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            // SAFETY: the complete fixture table outlives this validation call.
2414            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            // SAFETY: the fixture table storage is complete; its advertised
2451            // size intentionally claims a truncated required prefix.
2452            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}