Skip to main content

player_plugin_loader/
registry.rs

1use super::*;
2use crate::diagnostics::{
3    decoder_capability_summary, source_normalizer_packet_capability_summary,
4    source_normalizer_resource_capability_summary,
5};
6#[cfg(feature = "wasm")]
7use player_plugin::PluginOwnerDisposalError;
8use player_plugin::{
9    AudioProcessorPluginFactory, PluginInvocationPolicy, PluginInvocationWorkload, PluginReference,
10    PluginReferenceError, PluginScope, PluginTransport,
11};
12#[cfg(feature = "installed-catalog")]
13use player_plugin_package::{
14    PluginArtifactTransport, VerifiedInstalledArtifact, VerifiedInstalledPluginCatalog,
15};
16#[cfg(feature = "wasm")]
17use player_plugin_wasm_host::{
18    WASM_PLUGIN_FLUSH_TIMEOUT_MILLIS, WasmPluginRuntime, WasmPluginRuntimeError,
19};
20#[cfg(feature = "installed-catalog")]
21use std::collections::BTreeSet;
22use std::collections::HashMap;
23
24/// One native artifact supplied by a host-owned plugin catalog or loaded registry.
25///
26/// The path is an internal locator. Capability selection always uses a
27/// [`PluginReference`], and loading verifies that the Root ABI identity matches
28/// `plugin_id` exactly.
29#[derive(Debug, Clone)]
30pub struct NativePluginArtifact {
31    plugin_id: String,
32    path: PathBuf,
33    loaded: Option<Arc<LoadedNativePlugin>>,
34}
35
36impl PartialEq for NativePluginArtifact {
37    fn eq(&self, other: &Self) -> bool {
38        self.plugin_id == other.plugin_id
39            && self.path == other.path
40            && match (&self.loaded, &other.loaded) {
41                (Some(left), Some(right)) => Arc::ptr_eq(left, right),
42                (None, None) => true,
43                _ => false,
44            }
45    }
46}
47
48impl Eq for NativePluginArtifact {}
49
50impl NativePluginArtifact {
51    /// Constructs a path binding. The caller must verify the artifact before loading it.
52    /// Use [`PluginRegistry::native_artifact`] to reuse an already loaded instance.
53    pub fn new(
54        plugin_id: impl Into<String>,
55        path: impl Into<PathBuf>,
56    ) -> Result<Self, PluginReferenceError> {
57        let plugin_id = plugin_id.into();
58        PluginReference::new(plugin_id.clone(), None, PluginTransport::Native)?;
59        Ok(Self {
60            plugin_id,
61            path: path.into(),
62            loaded: None,
63        })
64    }
65
66    pub fn plugin_id(&self) -> &str {
67        &self.plugin_id
68    }
69
70    pub fn path(&self) -> &Path {
71        &self.path
72    }
73
74    fn load(&self) -> Result<Arc<LoadedNativePlugin>, PluginLoadError> {
75        match &self.loaded {
76            Some(plugin) => Ok(plugin.clone()),
77            None => LoadedNativePlugin::load_host_verified(&self.path).map(Arc::new),
78        }
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83struct PluginIdentityKey {
84    transport: PluginTransport,
85    plugin_id: String,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89struct PluginInterfaceKey {
90    transport: PluginTransport,
91    plugin_id: String,
92    interface_id: [u8; 16],
93    instance_id: String,
94}
95
96fn interface_references(
97    plugin: &LoadedNativePlugin,
98    interface_id: [u8; 16],
99) -> Result<Vec<PluginReference>, PluginReferenceError> {
100    plugin
101        .interfaces()
102        .iter()
103        .filter(|interface| interface.metadata.interface_id == interface_id)
104        .map(|interface| {
105            PluginReference::new(
106                plugin.plugin_id(),
107                Some(interface.metadata.instance_id.clone()),
108                PluginTransport::Native,
109            )
110        })
111        .collect()
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct RegisteredPluginInterface {
116    pub artifact_path: PathBuf,
117    pub transport: PluginTransport,
118    pub plugin_id: String,
119    pub interface: PluginInterfaceRecord,
120}
121
122#[derive(Clone)]
123pub struct ResolvedPluginCapability<T: ?Sized> {
124    reference: PluginReference,
125    capability: Arc<T>,
126}
127
128impl<T: ?Sized> std::fmt::Debug for ResolvedPluginCapability<T> {
129    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        formatter
131            .debug_struct("ResolvedPluginCapability")
132            .field("reference", &self.reference)
133            .finish_non_exhaustive()
134    }
135}
136
137impl<T: ?Sized> ResolvedPluginCapability<T> {
138    pub fn reference(&self) -> &PluginReference {
139        &self.reference
140    }
141
142    pub fn capability(&self) -> Arc<T> {
143        self.capability.clone()
144    }
145}
146
147#[derive(Debug, Error)]
148pub enum PluginRegistryBuildError {
149    #[error("failed to load native plugin artifact `{path}`: {source}")]
150    Load {
151        path: String,
152        #[source]
153        source: PluginLoadError,
154    },
155    #[error(
156        "native plugin artifact `{path}` declared identity `{expected_plugin_id}` but its Root ABI reports `{actual_plugin_id}`"
157    )]
158    PluginIdentityMismatch {
159        path: String,
160        expected_plugin_id: String,
161        actual_plugin_id: String,
162    },
163    #[error(
164        "duplicate plugin identity {transport:?}:{plugin_id} from `{first_path}` and `{duplicate_path}`"
165    )]
166    DuplicatePluginIdentity {
167        transport: PluginTransport,
168        plugin_id: String,
169        first_path: String,
170        duplicate_path: String,
171    },
172    #[error(
173        "duplicate interface identity {transport:?}:{plugin_id}:{interface_id:?}:{instance_id}"
174    )]
175    DuplicateInterfaceIdentity {
176        transport: PluginTransport,
177        plugin_id: String,
178        interface_id: [u8; 16],
179        instance_id: String,
180    },
181    #[cfg(feature = "wasm")]
182    #[error("failed to initialize the WASM plugin runtime: {source}")]
183    WasmRuntime {
184        #[source]
185        source: WasmPluginRuntimeError,
186    },
187    #[cfg(feature = "wasm")]
188    #[error("failed to load WASM plugin artifact `{path}` for `{plugin_id}`: {source}")]
189    WasmLoad {
190        path: String,
191        plugin_id: String,
192        #[source]
193        source: WasmPluginLoadError,
194    },
195    #[cfg(feature = "installed-catalog")]
196    #[error("invalid verified installed plugin artifact: {message}")]
197    InstalledCatalog { message: String },
198    #[cfg(feature = "installed-catalog")]
199    #[error(
200        "installed plugin artifact `{path}` for `{plugin_id}` does not match its declared capabilities: {message}"
201    )]
202    InstalledCapabilityMismatch {
203        path: String,
204        plugin_id: String,
205        message: String,
206    },
207    #[cfg(all(feature = "installed-catalog", not(feature = "wasm")))]
208    #[error("installed WASM plugin `{plugin_id}` requires the loader `wasm` feature")]
209    InstalledWasmUnsupported { plugin_id: String },
210}
211
212/// Aggregated loader-side report for inspected dynamic plugin paths.
213#[derive(Debug, Clone, PartialEq, Eq, Default)]
214pub struct PluginRegistryReport {
215    pub total: usize,
216    pub loaded: usize,
217    pub failed: usize,
218    pub decoder_supported: usize,
219    pub decoder_unsupported: usize,
220    pub frame_processor_supported: usize,
221    pub frame_processor_unsupported: usize,
222    pub source_normalizer_supported: usize,
223    pub source_normalizer_unsupported: usize,
224    pub unsupported_kind: usize,
225    pub best_supported_decoder_name: Option<String>,
226    pub best_supported_frame_processor_name: Option<String>,
227    pub best_supported_source_normalizer_name: Option<String>,
228    pub diagnostic_notes: Vec<String>,
229}
230
231/// Structured report for dynamic plugins loaded from host-provided paths.
232#[derive(Debug, Clone, Default)]
233pub struct PluginRegistry {
234    records: Vec<PluginDiagnosticRecord>,
235    record_references: Vec<Option<PluginReference>>,
236    plugins: HashMap<PluginIdentityKey, Arc<LoadedNativePlugin>>,
237    #[cfg(feature = "wasm")]
238    wasm_plugins: HashMap<PluginIdentityKey, Arc<LoadedWasmPlugin>>,
239    plugin_paths: HashMap<PluginIdentityKey, PathBuf>,
240    interfaces: Vec<RegisteredPluginInterface>,
241    interface_index: HashMap<PluginInterfaceKey, usize>,
242}
243
244impl PluginRegistry {
245    /// Inspects unsigned raw native libraries under explicit development policy.
246    pub fn inspect_decoder_support_development(
247        paths: impl IntoIterator<Item = impl AsRef<Path>>,
248        request: DecoderPluginMatchRequest,
249    ) -> Self {
250        let mut registry = Self::default();
251        for path in paths {
252            let path = path.as_ref().to_path_buf();
253            let Some(plugin) = registry.load_inspected_native_development(&path) else {
254                continue;
255            };
256            let Some(references) = registry.inspected_interface_references(
257                &path,
258                &plugin,
259                player_plugin_abi::NATIVE_DECODER_INTERFACE_ID.0,
260            ) else {
261                continue;
262            };
263            if references.is_empty() {
264                registry.push_record(
265                    PluginDiagnosticRecord::unsupported_native_interface(
266                        path,
267                        &plugin,
268                        "NativeDecoder",
269                    ),
270                    None,
271                );
272                continue;
273            }
274            for reference in references {
275                let record = PluginDiagnosticRecord::from_native_decoder_interface(
276                    path.clone(),
277                    &plugin,
278                    &reference,
279                    &request,
280                );
281                registry.push_record(record, Some(reference));
282            }
283        }
284        registry
285    }
286
287    /// Inspects unsigned raw native libraries under explicit development policy.
288    pub fn inspect_frame_processor_support_development(
289        paths: impl IntoIterator<Item = impl AsRef<Path>>,
290    ) -> Self {
291        let mut registry = Self::default();
292        for path in paths {
293            let path = path.as_ref().to_path_buf();
294            let Some(plugin) = registry.load_inspected_native_development(&path) else {
295                continue;
296            };
297            let Some(references) = registry.inspected_interface_references(
298                &path,
299                &plugin,
300                player_plugin_abi::FRAME_PROCESSOR_INTERFACE_ID.0,
301            ) else {
302                continue;
303            };
304            if references.is_empty() {
305                registry.push_record(
306                    PluginDiagnosticRecord::unsupported_native_interface(
307                        path,
308                        &plugin,
309                        "FrameProcessor",
310                    ),
311                    None,
312                );
313                continue;
314            }
315            for reference in references {
316                let record = PluginDiagnosticRecord::from_native_frame_processor_interface(
317                    path.clone(),
318                    &plugin,
319                    &reference,
320                );
321                registry.push_record(record, Some(reference));
322            }
323        }
324        registry
325    }
326
327    /// Inspects host-verified native artifacts and binds each declared plugin
328    /// identity to the Root ABI identity before exposing capability records.
329    pub fn inspect_frame_processor_support_artifacts(
330        artifacts: impl IntoIterator<Item = NativePluginArtifact>,
331    ) -> Self {
332        let mut registry = Self::default();
333        for artifact in artifacts {
334            let path = artifact.path.clone();
335            let Some(plugin) = registry.load_inspected_native_artifact(&artifact) else {
336                continue;
337            };
338            let Some(references) = registry.inspected_interface_references(
339                &path,
340                &plugin,
341                player_plugin_abi::FRAME_PROCESSOR_INTERFACE_ID.0,
342            ) else {
343                continue;
344            };
345            if references.is_empty() {
346                registry.push_record(
347                    PluginDiagnosticRecord::unsupported_native_interface(
348                        path,
349                        &plugin,
350                        "FrameProcessor",
351                    ),
352                    None,
353                );
354                continue;
355            }
356            for reference in references {
357                let record = PluginDiagnosticRecord::from_native_frame_processor_interface(
358                    path.clone(),
359                    &plugin,
360                    &reference,
361                );
362                registry.push_record(record, Some(reference));
363            }
364        }
365        registry
366    }
367
368    /// Inspects unsigned raw native libraries under explicit development policy.
369    pub fn inspect_source_normalizer_support_development(
370        paths: impl IntoIterator<Item = impl AsRef<Path>>,
371    ) -> Self {
372        let mut registry = Self::default();
373        for path in paths {
374            let path = path.as_ref().to_path_buf();
375            let Some(plugin) = registry.load_inspected_native_development(&path) else {
376                continue;
377            };
378            let Some(packet_references) = registry.inspected_interface_references(
379                &path,
380                &plugin,
381                player_plugin_abi::SOURCE_NORMALIZER_PACKET_INTERFACE_ID.0,
382            ) else {
383                continue;
384            };
385            let Some(resource_references) = registry.inspected_interface_references(
386                &path,
387                &plugin,
388                player_plugin_abi::SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID.0,
389            ) else {
390                continue;
391            };
392            if packet_references.is_empty() && resource_references.is_empty() {
393                registry.push_record(
394                    PluginDiagnosticRecord::unsupported_native_interface(
395                        path,
396                        &plugin,
397                        "SourceNormalizerPacket or SourceNormalizerResource",
398                    ),
399                    None,
400                );
401                continue;
402            }
403            for reference in resource_references {
404                let record = PluginDiagnosticRecord::from_native_source_resource_interface(
405                    path.clone(),
406                    &plugin,
407                    &reference,
408                );
409                registry.push_record(record, Some(reference));
410            }
411            for reference in packet_references {
412                let record = PluginDiagnosticRecord::from_native_source_packet_interface(
413                    path.clone(),
414                    &plugin,
415                    &reference,
416                );
417                registry.push_record(record, Some(reference));
418            }
419        }
420        registry
421    }
422
423    /// Inspects host-verified native artifacts and binds each declared plugin
424    /// identity to the Root ABI identity before exposing capability records.
425    pub fn inspect_source_normalizer_support_artifacts(
426        artifacts: impl IntoIterator<Item = NativePluginArtifact>,
427    ) -> Self {
428        let mut registry = Self::default();
429        for artifact in artifacts {
430            let path = artifact.path.clone();
431            let Some(plugin) = registry.load_inspected_native_artifact(&artifact) else {
432                continue;
433            };
434            let Some(packet_references) = registry.inspected_interface_references(
435                &path,
436                &plugin,
437                player_plugin_abi::SOURCE_NORMALIZER_PACKET_INTERFACE_ID.0,
438            ) else {
439                continue;
440            };
441            let Some(resource_references) = registry.inspected_interface_references(
442                &path,
443                &plugin,
444                player_plugin_abi::SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID.0,
445            ) else {
446                continue;
447            };
448            if packet_references.is_empty() && resource_references.is_empty() {
449                registry.push_record(
450                    PluginDiagnosticRecord::unsupported_native_interface(
451                        path,
452                        &plugin,
453                        "SourceNormalizerPacket or SourceNormalizerResource",
454                    ),
455                    None,
456                );
457                continue;
458            }
459            for reference in resource_references {
460                let record = PluginDiagnosticRecord::from_native_source_resource_interface(
461                    path.clone(),
462                    &plugin,
463                    &reference,
464                );
465                registry.push_record(record, Some(reference));
466            }
467            for reference in packet_references {
468                let record = PluginDiagnosticRecord::from_native_source_packet_interface(
469                    path.clone(),
470                    &plugin,
471                    &reference,
472                );
473                registry.push_record(record, Some(reference));
474            }
475        }
476        registry
477    }
478
479    pub fn from_records(records: Vec<PluginDiagnosticRecord>) -> Self {
480        Self {
481            record_references: vec![None; records.len()],
482            records,
483            ..Self::default()
484        }
485    }
486
487    /// Builds a diagnostic-only registry from records with their validated
488    /// canonical references already attached.
489    ///
490    /// This constructor does not load plugin instances. It is intended for
491    /// host adapters and synthetic fixtures that have already established the
492    /// same identity mapping produced by native or WASM inspection.
493    pub fn from_records_with_references(
494        entries: impl IntoIterator<Item = (PluginDiagnosticRecord, Option<PluginReference>)>,
495    ) -> Self {
496        let (records, record_references): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
497        Self {
498            records,
499            record_references,
500            ..Self::default()
501        }
502    }
503
504    fn load_inspected_native_development(
505        &mut self,
506        path: &Path,
507    ) -> Option<Arc<LoadedNativePlugin>> {
508        let plugin = match LoadedNativePlugin::load_development(path) {
509            Ok(plugin) => Arc::new(plugin),
510            Err(error) => {
511                self.push_record(
512                    PluginDiagnosticRecord::load_failed(path.to_path_buf(), error),
513                    None,
514                );
515                return None;
516            }
517        };
518        self.register_inspected_native(path, plugin)
519    }
520
521    fn load_inspected_native_artifact(
522        &mut self,
523        artifact: &NativePluginArtifact,
524    ) -> Option<Arc<LoadedNativePlugin>> {
525        let path = artifact.path();
526        let plugin = match artifact.load() {
527            Ok(plugin) => plugin,
528            Err(error) => {
529                self.push_record(
530                    PluginDiagnosticRecord::load_failed(path.to_path_buf(), error),
531                    None,
532                );
533                return None;
534            }
535        };
536        if plugin.plugin_id() != artifact.plugin_id() {
537            let error = PluginRegistryBuildError::PluginIdentityMismatch {
538                path: path.display().to_string(),
539                expected_plugin_id: artifact.plugin_id().to_owned(),
540                actual_plugin_id: plugin.plugin_id().to_owned(),
541            };
542            self.push_record(
543                PluginDiagnosticRecord::load_failed_message(path.to_path_buf(), error.to_string()),
544                None,
545            );
546            return None;
547        }
548        self.register_inspected_native(path, plugin)
549    }
550
551    fn register_inspected_native(
552        &mut self,
553        path: &Path,
554        plugin: Arc<LoadedNativePlugin>,
555    ) -> Option<Arc<LoadedNativePlugin>> {
556        let root_diagnostics = plugin
557            .diagnostics()
558            .iter()
559            .filter(|diagnostic| diagnostic.interface.is_none())
560            .map(|diagnostic| diagnostic.message.clone())
561            .collect::<Vec<_>>();
562        if let Err(error) = self.insert_native(path.to_path_buf(), plugin.clone()) {
563            self.push_record(
564                PluginDiagnosticRecord::load_failed_message(path.to_path_buf(), error.to_string()),
565                None,
566            );
567            return None;
568        }
569        for message in root_diagnostics {
570            self.push_record(
571                PluginDiagnosticRecord::load_failed_message(path.to_path_buf(), message),
572                None,
573            );
574        }
575        Some(plugin)
576    }
577
578    fn inspected_interface_references(
579        &mut self,
580        path: &Path,
581        plugin: &LoadedNativePlugin,
582        interface_id: [u8; 16],
583    ) -> Option<Vec<PluginReference>> {
584        match interface_references(plugin, interface_id) {
585            Ok(references) => Some(references),
586            Err(error) => {
587                self.push_record(
588                    PluginDiagnosticRecord::load_failed_message(
589                        path.to_path_buf(),
590                        format!("validated Root ABI identity could not form a reference: {error}"),
591                    ),
592                    None,
593                );
594                None
595            }
596        }
597    }
598
599    fn push_record(&mut self, record: PluginDiagnosticRecord, reference: Option<PluginReference>) {
600        self.records.push(record);
601        self.record_references.push(reference);
602    }
603
604    /// Loads unsigned raw native libraries under explicit development policy.
605    pub fn load_native_development(
606        paths: impl IntoIterator<Item = impl AsRef<Path>>,
607    ) -> Result<Self, PluginRegistryBuildError> {
608        let mut registry = Self::default();
609        for path in paths {
610            let path = path.as_ref().to_path_buf();
611            let plugin = LoadedNativePlugin::load_development(&path).map_err(|source| {
612                PluginRegistryBuildError::Load {
613                    path: path.display().to_string(),
614                    source,
615                }
616            })?;
617            registry.insert_native(path, Arc::new(plugin))?;
618        }
619        Ok(registry)
620    }
621
622    pub fn load_native_artifacts(
623        artifacts: impl IntoIterator<Item = NativePluginArtifact>,
624    ) -> Result<Self, PluginRegistryBuildError> {
625        let mut registry = Self::default();
626        registry.extend_native_artifacts(artifacts)?;
627        Ok(registry)
628    }
629
630    fn extend_native_artifacts(
631        &mut self,
632        artifacts: impl IntoIterator<Item = NativePluginArtifact>,
633    ) -> Result<(), PluginRegistryBuildError> {
634        for artifact in artifacts {
635            let plugin = artifact
636                .load()
637                .map_err(|source| PluginRegistryBuildError::Load {
638                    path: artifact.path.display().to_string(),
639                    source,
640                })?;
641            let path = artifact.path;
642            if plugin.plugin_id() != artifact.plugin_id {
643                return Err(PluginRegistryBuildError::PluginIdentityMismatch {
644                    path: path.display().to_string(),
645                    expected_plugin_id: artifact.plugin_id,
646                    actual_plugin_id: plugin.plugin_id().to_owned(),
647                });
648            }
649            self.insert_native(path, plugin)?;
650        }
651        Ok(())
652    }
653
654    #[cfg(feature = "wasm")]
655    pub fn load_wasm_artifacts(
656        artifacts: impl IntoIterator<Item = WasmPluginArtifact>,
657    ) -> Result<Self, PluginRegistryBuildError> {
658        let mut registry = Self::default();
659        registry.extend_wasm_artifacts(artifacts)?;
660        Ok(registry)
661    }
662
663    #[cfg(feature = "wasm")]
664    pub fn load_artifacts(
665        native_artifacts: impl IntoIterator<Item = NativePluginArtifact>,
666        wasm_artifacts: impl IntoIterator<Item = WasmPluginArtifact>,
667    ) -> Result<Self, PluginRegistryBuildError> {
668        let mut registry = Self::default();
669        registry.extend_native_artifacts(native_artifacts)?;
670        registry.extend_wasm_artifacts(wasm_artifacts)?;
671        Ok(registry)
672    }
673
674    #[cfg(feature = "installed-catalog")]
675    pub fn load_verified_installed_catalog(
676        catalog: &VerifiedInstalledPluginCatalog,
677    ) -> Result<Self, PluginRegistryBuildError> {
678        let mut registry = Self::default();
679        #[cfg(feature = "wasm")]
680        let mut wasm_runtime = None;
681        for artifact in catalog.artifacts() {
682            match artifact.transport() {
683                PluginArtifactTransport::Native => {
684                    let plugin = LoadedNativePlugin::load_host_verified(artifact.snapshot_path())
685                        .map_err(|source| PluginRegistryBuildError::Load {
686                        path: artifact.installed_path().display().to_string(),
687                        source,
688                    })?;
689                    if plugin.plugin_id() != artifact.plugin_id() {
690                        return Err(PluginRegistryBuildError::PluginIdentityMismatch {
691                            path: artifact.installed_path().display().to_string(),
692                            expected_plugin_id: artifact.plugin_id().to_owned(),
693                            actual_plugin_id: plugin.plugin_id().to_owned(),
694                        });
695                    }
696                    validate_installed_native_capabilities(artifact, &plugin)?;
697                    registry
698                        .insert_native(artifact.installed_path().to_path_buf(), Arc::new(plugin))?;
699                }
700                PluginArtifactTransport::Wasm => {
701                    #[cfg(feature = "wasm")]
702                    {
703                        let declarations = installed_wasm_declarations(artifact)?;
704                        let declared = WasmPluginArtifact::new(
705                            artifact.plugin_id(),
706                            artifact.snapshot_path(),
707                            declarations,
708                        )
709                        .map_err(|error| {
710                            PluginRegistryBuildError::InstalledCatalog {
711                                message: error.to_string(),
712                            }
713                        })?;
714                        let runtime = match wasm_runtime.as_ref() {
715                            Some(runtime) => runtime,
716                            None => {
717                                wasm_runtime =
718                                    Some(WasmPluginRuntime::new().map_err(|source| {
719                                        PluginRegistryBuildError::WasmRuntime { source }
720                                    })?);
721                                wasm_runtime.as_ref().ok_or_else(|| {
722                                    PluginRegistryBuildError::InstalledCatalog {
723                                        message: "WASM runtime initialization was lost".to_owned(),
724                                    }
725                                })?
726                            }
727                        };
728                        let plugin =
729                            LoadedWasmPlugin::load(&declared, runtime).map_err(|source| {
730                                PluginRegistryBuildError::WasmLoad {
731                                    path: artifact.installed_path().display().to_string(),
732                                    plugin_id: artifact.plugin_id().to_owned(),
733                                    source,
734                                }
735                            })?;
736                        registry.insert_wasm(
737                            artifact.installed_path().to_path_buf(),
738                            Arc::new(plugin),
739                        )?;
740                    }
741                    #[cfg(not(feature = "wasm"))]
742                    return Err(PluginRegistryBuildError::InstalledWasmUnsupported {
743                        plugin_id: artifact.plugin_id().to_owned(),
744                    });
745                }
746            }
747        }
748        Ok(registry)
749    }
750
751    #[cfg(feature = "wasm")]
752    fn extend_wasm_artifacts(
753        &mut self,
754        artifacts: impl IntoIterator<Item = WasmPluginArtifact>,
755    ) -> Result<(), PluginRegistryBuildError> {
756        let mut artifacts = artifacts.into_iter().peekable();
757        if artifacts.peek().is_none() {
758            return Ok(());
759        }
760        let runtime = WasmPluginRuntime::new()
761            .map_err(|source| PluginRegistryBuildError::WasmRuntime { source })?;
762        for artifact in artifacts {
763            let path = artifact.path().to_path_buf();
764            let plugin_id = artifact.plugin_id().to_owned();
765            let plugin = LoadedWasmPlugin::load(&artifact, &runtime).map_err(|source| {
766                PluginRegistryBuildError::WasmLoad {
767                    path: path.display().to_string(),
768                    plugin_id,
769                    source,
770                }
771            })?;
772            self.insert_wasm(path, Arc::new(plugin))?;
773        }
774        Ok(())
775    }
776
777    fn insert_native(
778        &mut self,
779        artifact_path: PathBuf,
780        plugin: Arc<LoadedNativePlugin>,
781    ) -> Result<(), PluginRegistryBuildError> {
782        let identity = PluginIdentityKey {
783            transport: PluginTransport::Native,
784            plugin_id: plugin.plugin_id().to_owned(),
785        };
786        if let Some(first_path) = self.plugin_paths.get(&identity) {
787            return Err(PluginRegistryBuildError::DuplicatePluginIdentity {
788                transport: identity.transport,
789                plugin_id: identity.plugin_id,
790                first_path: first_path.display().to_string(),
791                duplicate_path: artifact_path.display().to_string(),
792            });
793        }
794
795        let mut pending = Vec::with_capacity(plugin.interfaces().len());
796        for interface in plugin.interfaces() {
797            let key = PluginInterfaceKey {
798                transport: PluginTransport::Native,
799                plugin_id: plugin.plugin_id().to_owned(),
800                interface_id: interface.metadata.interface_id,
801                instance_id: interface.metadata.instance_id.clone(),
802            };
803            if self.interface_index.contains_key(&key)
804                || pending
805                    .iter()
806                    .any(|(pending_key, _): &(PluginInterfaceKey, _)| pending_key == &key)
807            {
808                return Err(PluginRegistryBuildError::DuplicateInterfaceIdentity {
809                    transport: key.transport,
810                    plugin_id: key.plugin_id,
811                    interface_id: key.interface_id,
812                    instance_id: key.instance_id,
813                });
814            }
815            pending.push((
816                key,
817                RegisteredPluginInterface {
818                    artifact_path: artifact_path.clone(),
819                    transport: PluginTransport::Native,
820                    plugin_id: plugin.plugin_id().to_owned(),
821                    interface: interface.clone(),
822                },
823            ));
824        }
825
826        self.plugin_paths.insert(identity.clone(), artifact_path);
827        self.plugins.insert(identity, plugin);
828        for (key, interface) in pending {
829            let index = self.interfaces.len();
830            self.interfaces.push(interface);
831            self.interface_index.insert(key, index);
832        }
833        Ok(())
834    }
835
836    #[cfg(feature = "wasm")]
837    fn insert_wasm(
838        &mut self,
839        artifact_path: PathBuf,
840        plugin: Arc<LoadedWasmPlugin>,
841    ) -> Result<(), PluginRegistryBuildError> {
842        let identity = PluginIdentityKey {
843            transport: PluginTransport::Wasm,
844            plugin_id: plugin.plugin_id().to_owned(),
845        };
846        if let Some(first_path) = self.plugin_paths.get(&identity) {
847            return Err(PluginRegistryBuildError::DuplicatePluginIdentity {
848                transport: identity.transport,
849                plugin_id: identity.plugin_id,
850                first_path: first_path.display().to_string(),
851                duplicate_path: artifact_path.display().to_string(),
852            });
853        }
854
855        let mut pending = Vec::with_capacity(plugin.interfaces().len());
856        for interface in plugin.interfaces() {
857            let key = PluginInterfaceKey {
858                transport: PluginTransport::Wasm,
859                plugin_id: plugin.plugin_id().to_owned(),
860                interface_id: interface.metadata.interface_id,
861                instance_id: interface.metadata.instance_id.clone(),
862            };
863            if self.interface_index.contains_key(&key)
864                || pending
865                    .iter()
866                    .any(|(pending_key, _): &(PluginInterfaceKey, _)| pending_key == &key)
867            {
868                return Err(PluginRegistryBuildError::DuplicateInterfaceIdentity {
869                    transport: key.transport,
870                    plugin_id: key.plugin_id,
871                    interface_id: key.interface_id,
872                    instance_id: key.instance_id,
873                });
874            }
875            pending.push((
876                key,
877                RegisteredPluginInterface {
878                    artifact_path: artifact_path.clone(),
879                    transport: PluginTransport::Wasm,
880                    plugin_id: plugin.plugin_id().to_owned(),
881                    interface: interface.clone(),
882                },
883            ));
884        }
885
886        self.plugin_paths.insert(identity.clone(), artifact_path);
887        self.wasm_plugins.insert(identity, plugin);
888        for (key, interface) in pending {
889            let index = self.interfaces.len();
890            self.interfaces.push(interface);
891            self.interface_index.insert(key, index);
892        }
893        Ok(())
894    }
895
896    pub fn registered_interfaces(&self) -> &[RegisteredPluginInterface] {
897        &self.interfaces
898    }
899
900    pub fn post_download_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
901        self.references_for_interface(player_plugin_abi::POST_DOWNLOAD_PROCESSOR_INTERFACE_ID.0)
902    }
903
904    pub fn pipeline_event_hook_references(
905        &self,
906    ) -> Result<Vec<PluginReference>, PluginSelectionError> {
907        self.references_for_interface(player_plugin_abi::PIPELINE_EVENT_HOOK_INTERFACE_ID.0)
908    }
909
910    pub fn benchmark_sink_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
911        self.references_for_interface(player_plugin_abi::BENCHMARK_SINK_INTERFACE_ID.0)
912    }
913
914    pub fn native_decoder_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
915        self.references_for_interface(player_plugin_abi::NATIVE_DECODER_INTERFACE_ID.0)
916    }
917
918    pub fn frame_processor_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
919        self.references_for_interface(player_plugin_abi::FRAME_PROCESSOR_INTERFACE_ID.0)
920    }
921
922    pub fn audio_processor_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
923        self.references_for_interface(player_plugin_abi::AUDIO_PROCESSOR_INTERFACE_ID.0)
924    }
925
926    pub fn source_packet_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
927        self.references_for_interface(player_plugin_abi::SOURCE_NORMALIZER_PACKET_INTERFACE_ID.0)
928    }
929
930    pub fn source_resource_references(&self) -> Result<Vec<PluginReference>, PluginSelectionError> {
931        self.references_for_interface(player_plugin_abi::SOURCE_NORMALIZER_RESOURCE_INTERFACE_ID.0)
932    }
933
934    fn references_for_interface(
935        &self,
936        interface_id: [u8; 16],
937    ) -> Result<Vec<PluginReference>, PluginSelectionError> {
938        self.interfaces
939            .iter()
940            .filter(|interface| {
941                interface.interface.state == PluginInterfaceState::Available
942                    && interface.interface.metadata.interface_id == interface_id
943            })
944            .map(|interface| {
945                PluginReference::new(
946                    interface.plugin_id.clone(),
947                    Some(interface.interface.metadata.instance_id.clone()),
948                    interface.transport,
949                )
950                .map_err(|_| PluginSelectionError::InvalidLoadedIdentity {
951                    plugin_id: interface.plugin_id.clone(),
952                    instance_id: interface.interface.metadata.instance_id.clone(),
953                })
954            })
955            .collect()
956    }
957
958    pub fn resolve_post_download(
959        &self,
960        reference: &PluginReference,
961    ) -> Result<ResolvedPluginCapability<dyn PostDownloadProcessor>, PluginSelectionError> {
962        self.validate_invocation(reference, PluginInvocationWorkload::Offline)?;
963        let plugin = self.plugin_for(reference)?;
964        let (instance_id, capability) = plugin.resolve_post_download_selected(reference)?;
965        self.resolved(reference, instance_id, capability)
966    }
967
968    pub fn resolve_pipeline_event_hook(
969        &self,
970        reference: &PluginReference,
971    ) -> Result<ResolvedPluginCapability<dyn PipelineEventHook>, PluginSelectionError> {
972        self.validate_invocation(reference, PluginInvocationWorkload::Observer)?;
973        if reference.transport() == PluginTransport::Wasm {
974            #[cfg(feature = "wasm")]
975            {
976                let plugin = self.wasm_plugin_for(reference)?;
977                plugin.select_pipeline_event_hook(reference)?;
978                return Err(PluginSelectionError::ScopeRequired {
979                    plugin_id: reference.plugin_id().to_owned(),
980                    interface: "PipelineEventHook",
981                });
982            }
983            #[cfg(not(feature = "wasm"))]
984            {
985                return Err(PluginSelectionError::PluginNotFound {
986                    plugin_id: reference.plugin_id().to_owned(),
987                    transport: PluginTransport::Wasm,
988                });
989            }
990        }
991        let plugin = self.plugin_for(reference)?;
992        let (instance_id, capability) = plugin.resolve_pipeline_event_hook_selected(reference)?;
993        self.resolved(reference, instance_id, capability)
994    }
995
996    /// Resolves one EventHook and binds a fresh WASM worker/session to `scope`.
997    /// Native capabilities keep their existing trusted owner semantics.
998    pub fn resolve_pipeline_event_hook_in_scope(
999        &self,
1000        reference: &PluginReference,
1001        _scope: &PluginScope,
1002    ) -> Result<ResolvedPluginCapability<dyn PipelineEventHook>, PluginSelectionError> {
1003        self.validate_invocation(reference, PluginInvocationWorkload::Observer)?;
1004        if reference.transport() == PluginTransport::Wasm {
1005            #[cfg(feature = "wasm")]
1006            {
1007                let plugin = self.wasm_plugin_for(reference)?;
1008                let (instance_id, adapter) = plugin.instantiate_pipeline_event_hook(reference)?;
1009                let owner = adapter.clone();
1010                if let Err(source) = _scope.add_fallible_owner_disposer(move || {
1011                    owner
1012                        .close(std::time::Duration::from_millis(
1013                            WASM_PLUGIN_FLUSH_TIMEOUT_MILLIS,
1014                        ))
1015                        .map_err(|_| PluginOwnerDisposalError)
1016                }) {
1017                    let _ = adapter.close(std::time::Duration::from_millis(
1018                        WASM_PLUGIN_FLUSH_TIMEOUT_MILLIS,
1019                    ));
1020                    return Err(PluginSelectionError::ScopeRegistration {
1021                        plugin_id: reference.plugin_id().to_owned(),
1022                        interface: "PipelineEventHook",
1023                        source,
1024                    });
1025                }
1026                let capability: Arc<dyn PipelineEventHook> = adapter;
1027                return self.resolved(reference, instance_id, capability);
1028            }
1029            #[cfg(not(feature = "wasm"))]
1030            {
1031                return Err(PluginSelectionError::PluginNotFound {
1032                    plugin_id: reference.plugin_id().to_owned(),
1033                    transport: PluginTransport::Wasm,
1034                });
1035            }
1036        }
1037        self.resolve_pipeline_event_hook(reference)
1038    }
1039
1040    pub fn resolve_benchmark_sink(
1041        &self,
1042        reference: &PluginReference,
1043    ) -> Result<ResolvedPluginCapability<dyn BenchmarkSink>, PluginSelectionError> {
1044        self.validate_invocation(reference, PluginInvocationWorkload::Offline)?;
1045        if reference.transport() == PluginTransport::Wasm {
1046            #[cfg(feature = "wasm")]
1047            {
1048                let plugin = self.wasm_plugin_for(reference)?;
1049                plugin.select_benchmark_sink(reference)?;
1050                return Err(PluginSelectionError::ScopeRequired {
1051                    plugin_id: reference.plugin_id().to_owned(),
1052                    interface: "BenchmarkSink",
1053                });
1054            }
1055            #[cfg(not(feature = "wasm"))]
1056            {
1057                return Err(PluginSelectionError::PluginNotFound {
1058                    plugin_id: reference.plugin_id().to_owned(),
1059                    transport: PluginTransport::Wasm,
1060                });
1061            }
1062        }
1063        let plugin = self.plugin_for(reference)?;
1064        let (instance_id, capability) = plugin.resolve_benchmark_sink_selected(reference)?;
1065        self.resolved(reference, instance_id, capability)
1066    }
1067
1068    /// Resolves one BenchmarkSink and binds a fresh WASM worker/session to
1069    /// `scope`. Native capabilities keep their existing trusted owner semantics.
1070    pub fn resolve_benchmark_sink_in_scope(
1071        &self,
1072        reference: &PluginReference,
1073        _scope: &PluginScope,
1074    ) -> Result<ResolvedPluginCapability<dyn BenchmarkSink>, PluginSelectionError> {
1075        self.validate_invocation(reference, PluginInvocationWorkload::Offline)?;
1076        if reference.transport() == PluginTransport::Wasm {
1077            #[cfg(feature = "wasm")]
1078            {
1079                let plugin = self.wasm_plugin_for(reference)?;
1080                let (instance_id, adapter) = plugin.instantiate_benchmark_sink(reference)?;
1081                let owner = adapter.clone();
1082                if let Err(source) = _scope.add_fallible_owner_disposer(move || {
1083                    owner
1084                        .close(std::time::Duration::from_millis(
1085                            WASM_PLUGIN_FLUSH_TIMEOUT_MILLIS,
1086                        ))
1087                        .map(|_| ())
1088                        .map_err(|_| PluginOwnerDisposalError)
1089                }) {
1090                    let _ = adapter.close(std::time::Duration::from_millis(
1091                        WASM_PLUGIN_FLUSH_TIMEOUT_MILLIS,
1092                    ));
1093                    return Err(PluginSelectionError::ScopeRegistration {
1094                        plugin_id: reference.plugin_id().to_owned(),
1095                        interface: "BenchmarkSink",
1096                        source,
1097                    });
1098                }
1099                let capability: Arc<dyn BenchmarkSink> = adapter;
1100                return self.resolved(reference, instance_id, capability);
1101            }
1102            #[cfg(not(feature = "wasm"))]
1103            {
1104                return Err(PluginSelectionError::PluginNotFound {
1105                    plugin_id: reference.plugin_id().to_owned(),
1106                    transport: PluginTransport::Wasm,
1107                });
1108            }
1109        }
1110        self.resolve_benchmark_sink(reference)
1111    }
1112
1113    pub fn resolve_native_decoder(
1114        &self,
1115        reference: &PluginReference,
1116    ) -> Result<ResolvedPluginCapability<dyn NativeDecoderPluginFactory>, PluginSelectionError>
1117    {
1118        self.validate_invocation(reference, PluginInvocationWorkload::RealtimeMedia)?;
1119        let plugin = self.plugin_for(reference)?;
1120        let (instance_id, capability) = plugin.resolve_native_decoder_selected(reference)?;
1121        self.resolved(reference, instance_id, capability)
1122    }
1123
1124    pub fn resolve_frame_processor(
1125        &self,
1126        reference: &PluginReference,
1127    ) -> Result<ResolvedPluginCapability<dyn FrameProcessorPluginFactory>, PluginSelectionError>
1128    {
1129        self.validate_invocation(reference, PluginInvocationWorkload::RealtimeMedia)?;
1130        let plugin = self.plugin_for(reference)?;
1131        let (instance_id, capability) = plugin.resolve_frame_processor_selected(reference)?;
1132        self.resolved(reference, instance_id, capability)
1133    }
1134
1135    pub fn resolve_audio_processor(
1136        &self,
1137        reference: &PluginReference,
1138    ) -> Result<ResolvedPluginCapability<dyn AudioProcessorPluginFactory>, PluginSelectionError>
1139    {
1140        self.validate_invocation(reference, PluginInvocationWorkload::RealtimeMedia)?;
1141        let plugin = self.plugin_for(reference)?;
1142        let (instance_id, capability) = plugin.resolve_audio_processor_selected(reference)?;
1143        self.resolved(reference, instance_id, capability)
1144    }
1145
1146    pub fn resolve_source_packet(
1147        &self,
1148        reference: &PluginReference,
1149    ) -> Result<
1150        ResolvedPluginCapability<dyn SourceNormalizerPacketPluginFactory>,
1151        PluginSelectionError,
1152    > {
1153        self.validate_invocation(reference, PluginInvocationWorkload::RealtimeMedia)?;
1154        let plugin = self.plugin_for(reference)?;
1155        let (instance_id, capability) = plugin.resolve_source_packet_selected(reference)?;
1156        self.resolved(reference, instance_id, capability)
1157    }
1158
1159    pub fn resolve_source_resource(
1160        &self,
1161        reference: &PluginReference,
1162    ) -> Result<
1163        ResolvedPluginCapability<dyn SourceNormalizerResourcePluginFactory>,
1164        PluginSelectionError,
1165    > {
1166        self.validate_invocation(reference, PluginInvocationWorkload::RealtimeMedia)?;
1167        let plugin = self.plugin_for(reference)?;
1168        let (instance_id, capability) = plugin.resolve_source_resource_selected(reference)?;
1169        self.resolved(reference, instance_id, capability)
1170    }
1171
1172    fn validate_invocation(
1173        &self,
1174        reference: &PluginReference,
1175        workload: PluginInvocationWorkload,
1176    ) -> Result<(), PluginSelectionError> {
1177        PluginInvocationPolicy::standard()
1178            .validate(workload, reference.transport())
1179            .map_err(PluginSelectionError::from)
1180    }
1181
1182    /// Retains the loaded native library so another consumer can use it without
1183    /// reopening its path. Diagnostic-only records cannot produce an artifact.
1184    /// Capability selection remains the consumer's responsibility.
1185    pub fn native_artifact(
1186        &self,
1187        reference: &PluginReference,
1188    ) -> Result<NativePluginArtifact, PluginSelectionError> {
1189        let plugin = self.plugin_for(reference)?;
1190        let identity = PluginIdentityKey {
1191            transport: reference.transport(),
1192            plugin_id: reference.plugin_id().to_owned(),
1193        };
1194        let path = self.plugin_paths.get(&identity).cloned().ok_or_else(|| {
1195            PluginSelectionError::PluginNotFound {
1196                plugin_id: reference.plugin_id().to_owned(),
1197                transport: reference.transport(),
1198            }
1199        })?;
1200        Ok(NativePluginArtifact {
1201            plugin_id: reference.plugin_id().to_owned(),
1202            path,
1203            loaded: Some(plugin),
1204        })
1205    }
1206
1207    fn plugin_for(
1208        &self,
1209        reference: &PluginReference,
1210    ) -> Result<Arc<LoadedNativePlugin>, PluginSelectionError> {
1211        let identity = PluginIdentityKey {
1212            transport: reference.transport(),
1213            plugin_id: reference.plugin_id().to_owned(),
1214        };
1215        self.plugins
1216            .get(&identity)
1217            .cloned()
1218            .ok_or_else(|| PluginSelectionError::PluginNotFound {
1219                plugin_id: reference.plugin_id().to_owned(),
1220                transport: reference.transport(),
1221            })
1222    }
1223
1224    #[cfg(feature = "wasm")]
1225    fn wasm_plugin_for(
1226        &self,
1227        reference: &PluginReference,
1228    ) -> Result<Arc<LoadedWasmPlugin>, PluginSelectionError> {
1229        let identity = PluginIdentityKey {
1230            transport: reference.transport(),
1231            plugin_id: reference.plugin_id().to_owned(),
1232        };
1233        self.wasm_plugins.get(&identity).cloned().ok_or_else(|| {
1234            PluginSelectionError::PluginNotFound {
1235                plugin_id: reference.plugin_id().to_owned(),
1236                transport: reference.transport(),
1237            }
1238        })
1239    }
1240
1241    fn resolved<T: ?Sized>(
1242        &self,
1243        reference: &PluginReference,
1244        instance_id: String,
1245        capability: Arc<T>,
1246    ) -> Result<ResolvedPluginCapability<T>, PluginSelectionError> {
1247        let canonical = PluginReference::new(
1248            reference.plugin_id(),
1249            Some(instance_id.clone()),
1250            reference.transport(),
1251        )
1252        .map_err(|_| PluginSelectionError::InvalidLoadedIdentity {
1253            plugin_id: reference.plugin_id().to_owned(),
1254            instance_id,
1255        })?;
1256        Ok(ResolvedPluginCapability {
1257            reference: canonical,
1258            capability,
1259        })
1260    }
1261
1262    pub fn records(&self) -> &[PluginDiagnosticRecord] {
1263        &self.records
1264    }
1265
1266    /// Returns the canonical plugin reference associated with an inspection record.
1267    pub fn reference_for_record(
1268        &self,
1269        record: &PluginDiagnosticRecord,
1270    ) -> Option<&PluginReference> {
1271        self.records
1272            .iter()
1273            .position(|candidate| std::ptr::eq(candidate, record))
1274            .and_then(|index| self.record_references.get(index))
1275            .and_then(Option::as_ref)
1276    }
1277
1278    pub fn best_decoder_for(
1279        &self,
1280        request: &DecoderPluginMatchRequest,
1281    ) -> Option<&PluginDiagnosticRecord> {
1282        self.records.iter().find(|record| {
1283            record.status == PluginDiagnosticStatus::DecoderSupported
1284                && decoder_capability_summary(record).is_some_and(|capabilities| {
1285                    capabilities.typed_codecs.iter().any(|codec| {
1286                        codec.media_kind == request.media_kind
1287                            && codec.codec.eq_ignore_ascii_case(&request.codec)
1288                    })
1289                })
1290        })
1291    }
1292
1293    pub fn best_native_decoder_for(
1294        &self,
1295        request: &DecoderPluginMatchRequest,
1296    ) -> Option<&PluginDiagnosticRecord> {
1297        self.records.iter().find(|record| {
1298            record.status == PluginDiagnosticStatus::DecoderSupported
1299                && decoder_capability_summary(record).is_some_and(|capabilities| {
1300                    capabilities.supports_native_frame_output
1301                        && capabilities.typed_codecs.iter().any(|codec| {
1302                            codec.media_kind == request.media_kind
1303                                && codec.codec.eq_ignore_ascii_case(&request.codec)
1304                        })
1305                })
1306        })
1307    }
1308
1309    pub fn best_pcm_audio_decoder_for(
1310        &self,
1311        request: &DecoderPluginMatchRequest,
1312    ) -> Option<&PluginDiagnosticRecord> {
1313        if request.media_kind != DecoderMediaKind::Audio {
1314            return None;
1315        }
1316        self.records.iter().find(|record| {
1317            record.status == PluginDiagnosticStatus::DecoderSupported
1318                && decoder_capability_summary(record).is_some_and(|capabilities| {
1319                    capabilities.supports_pcm_frames
1320                        && capabilities.typed_codecs.iter().any(|codec| {
1321                            codec.media_kind == request.media_kind
1322                                && codec.codec.eq_ignore_ascii_case(&request.codec)
1323                        })
1324                })
1325        })
1326    }
1327
1328    pub fn supports_decoder(&self, request: &DecoderPluginMatchRequest) -> bool {
1329        self.best_decoder_for(request).is_some()
1330    }
1331
1332    pub fn supports_native_decoder(&self, request: &DecoderPluginMatchRequest) -> bool {
1333        self.best_native_decoder_for(request).is_some()
1334    }
1335
1336    pub fn supports_pcm_audio_decoder(&self, request: &DecoderPluginMatchRequest) -> bool {
1337        self.best_pcm_audio_decoder_for(request).is_some()
1338    }
1339
1340    pub fn frame_processor_supported_plugin_names(&self) -> Vec<&str> {
1341        self.records
1342            .iter()
1343            .filter(|record| record.status == PluginDiagnosticStatus::FrameProcessorSupported)
1344            .filter_map(|record| record.plugin_name.as_deref())
1345            .collect()
1346    }
1347
1348    pub fn source_normalizer_supported_plugin_names(&self) -> Vec<&str> {
1349        self.records
1350            .iter()
1351            .filter(|record| record.status == PluginDiagnosticStatus::SourceNormalizerSupported)
1352            .filter_map(|record| record.plugin_name.as_deref())
1353            .collect()
1354    }
1355
1356    pub fn best_source_normalizer(&self) -> Option<&PluginDiagnosticRecord> {
1357        self.records
1358            .iter()
1359            .find(|record| record.status == PluginDiagnosticStatus::SourceNormalizerSupported)
1360    }
1361
1362    pub fn best_source_normalizer_packet(&self) -> Option<&PluginDiagnosticRecord> {
1363        self.records.iter().find(|record| {
1364            record.status == PluginDiagnosticStatus::SourceNormalizerSupported
1365                && source_normalizer_packet_capability_summary(record).is_some()
1366        })
1367    }
1368
1369    pub fn best_source_normalizer_packet_for_profile(
1370        &self,
1371        runtime_profile: &str,
1372    ) -> Option<&PluginDiagnosticRecord> {
1373        self.records.iter().find(|record| {
1374            record.status == PluginDiagnosticStatus::SourceNormalizerSupported
1375                && source_normalizer_packet_capability_summary(record).is_some_and(|capabilities| {
1376                    capabilities
1377                        .supported_runtime_profiles
1378                        .iter()
1379                        .any(|profile| profile.eq_ignore_ascii_case(runtime_profile))
1380                })
1381        })
1382    }
1383
1384    pub fn best_source_normalizer_resource(&self) -> Option<&PluginDiagnosticRecord> {
1385        self.records.iter().find(|record| {
1386            record.status == PluginDiagnosticStatus::SourceNormalizerSupported
1387                && source_normalizer_resource_capability_summary(record).is_some()
1388        })
1389    }
1390
1391    pub fn best_source_normalizer_resource_for_profile(
1392        &self,
1393        runtime_profile: &str,
1394    ) -> Option<&PluginDiagnosticRecord> {
1395        self.records.iter().find(|record| {
1396            record.status == PluginDiagnosticStatus::SourceNormalizerSupported
1397                && source_normalizer_resource_capability_summary(record).is_some_and(
1398                    |capabilities| {
1399                        capabilities
1400                            .supported_runtime_profiles
1401                            .iter()
1402                            .any(|profile| profile.eq_ignore_ascii_case(runtime_profile))
1403                    },
1404                )
1405        })
1406    }
1407
1408    pub fn best_source_normalizer_for_profile(
1409        &self,
1410        runtime_profile: &str,
1411    ) -> Option<&PluginDiagnosticRecord> {
1412        self.records.iter().find(|record| {
1413            record.status == PluginDiagnosticStatus::SourceNormalizerSupported
1414                && (source_normalizer_resource_capability_summary(record).is_some_and(
1415                    |capabilities| {
1416                        capabilities
1417                            .supported_runtime_profiles
1418                            .iter()
1419                            .any(|profile| profile.eq_ignore_ascii_case(runtime_profile))
1420                    },
1421                ) || source_normalizer_packet_capability_summary(record).is_some_and(
1422                    |capabilities| {
1423                        capabilities
1424                            .supported_runtime_profiles
1425                            .iter()
1426                            .any(|profile| profile.eq_ignore_ascii_case(runtime_profile))
1427                    },
1428                ))
1429        })
1430    }
1431
1432    pub fn decoder_supported_plugin_names(&self) -> Vec<&str> {
1433        self.records
1434            .iter()
1435            .filter(|record| record.status == PluginDiagnosticStatus::DecoderSupported)
1436            .filter_map(|record| record.plugin_name.as_deref())
1437            .collect()
1438    }
1439
1440    pub fn diagnostic_notes(&self) -> Vec<String> {
1441        self.records
1442            .iter()
1443            .filter(|record| {
1444                !matches!(
1445                    record.status,
1446                    PluginDiagnosticStatus::DecoderSupported
1447                        | PluginDiagnosticStatus::FrameProcessorSupported
1448                        | PluginDiagnosticStatus::SourceNormalizerSupported
1449                )
1450            })
1451            .map(PluginDiagnosticRecord::summary)
1452            .collect()
1453    }
1454
1455    pub fn report(&self) -> PluginRegistryReport {
1456        let mut report = PluginRegistryReport {
1457            total: self.records.len(),
1458            ..PluginRegistryReport::default()
1459        };
1460
1461        for record in &self.records {
1462            match record.status {
1463                PluginDiagnosticStatus::Loaded => {
1464                    report.loaded += 1;
1465                    report.diagnostic_notes.push(record.summary());
1466                }
1467                PluginDiagnosticStatus::LoadFailed => {
1468                    report.failed += 1;
1469                    report.diagnostic_notes.push(record.summary());
1470                }
1471                PluginDiagnosticStatus::UnsupportedKind => {
1472                    report.loaded += 1;
1473                    report.unsupported_kind += 1;
1474                    report.diagnostic_notes.push(record.summary());
1475                }
1476                PluginDiagnosticStatus::DecoderSupported => {
1477                    report.loaded += 1;
1478                    report.decoder_supported += 1;
1479                    if report.best_supported_decoder_name.is_none() {
1480                        report.best_supported_decoder_name = record.plugin_name.clone();
1481                    }
1482                }
1483                PluginDiagnosticStatus::DecoderUnsupported => {
1484                    report.loaded += 1;
1485                    report.decoder_unsupported += 1;
1486                    report.diagnostic_notes.push(record.summary());
1487                }
1488                PluginDiagnosticStatus::FrameProcessorSupported => {
1489                    report.loaded += 1;
1490                    report.frame_processor_supported += 1;
1491                    if report.best_supported_frame_processor_name.is_none() {
1492                        report.best_supported_frame_processor_name = record.plugin_name.clone();
1493                    }
1494                }
1495                PluginDiagnosticStatus::FrameProcessorUnsupported => {
1496                    report.loaded += 1;
1497                    report.frame_processor_unsupported += 1;
1498                    report.diagnostic_notes.push(record.summary());
1499                }
1500                PluginDiagnosticStatus::SourceNormalizerSupported => {
1501                    report.loaded += 1;
1502                    report.source_normalizer_supported += 1;
1503                    if report.best_supported_source_normalizer_name.is_none() {
1504                        report.best_supported_source_normalizer_name = record.plugin_name.clone();
1505                    }
1506                }
1507                PluginDiagnosticStatus::SourceNormalizerUnsupported => {
1508                    report.loaded += 1;
1509                    report.source_normalizer_unsupported += 1;
1510                    report.diagnostic_notes.push(record.summary());
1511                }
1512            }
1513        }
1514
1515        report
1516    }
1517}
1518
1519#[cfg(feature = "installed-catalog")]
1520fn validate_installed_native_capabilities(
1521    artifact: &VerifiedInstalledArtifact,
1522    plugin: &LoadedNativePlugin,
1523) -> Result<(), PluginRegistryBuildError> {
1524    let declared = artifact
1525        .capabilities()
1526        .iter()
1527        .map(|capability| {
1528            let interface_id = uuid::Uuid::parse_str(&capability.interface_id)
1529                .map(|interface_id| *interface_id.as_bytes())
1530                .map_err(|error| PluginRegistryBuildError::InstalledCatalog {
1531                    message: format!(
1532                        "invalid interface UUID '{}': {error}",
1533                        capability.interface_id
1534                    ),
1535                })?;
1536            Ok((
1537                interface_id,
1538                capability.instance_id.clone(),
1539                capability.interface_major,
1540                capability.interface_minor,
1541            ))
1542        })
1543        .collect::<Result<BTreeSet<_>, PluginRegistryBuildError>>()?;
1544    let actual = plugin
1545        .interfaces()
1546        .iter()
1547        .filter(|interface| interface.state == PluginInterfaceState::Available)
1548        .map(|interface| {
1549            (
1550                interface.metadata.interface_id,
1551                interface.metadata.instance_id.clone(),
1552                interface.metadata.major,
1553                interface.metadata.minor,
1554            )
1555        })
1556        .collect::<BTreeSet<_>>();
1557    if declared != actual {
1558        return Err(PluginRegistryBuildError::InstalledCapabilityMismatch {
1559            path: artifact.installed_path().display().to_string(),
1560            plugin_id: artifact.plugin_id().to_owned(),
1561            message: format!("declared {declared:?}, Root ABI reported {actual:?}"),
1562        });
1563    }
1564    Ok(())
1565}
1566
1567#[cfg(all(feature = "installed-catalog", feature = "wasm"))]
1568fn installed_wasm_declarations(
1569    artifact: &VerifiedInstalledArtifact,
1570) -> Result<Vec<WasmPluginInterfaceDeclaration>, PluginRegistryBuildError> {
1571    artifact
1572        .capabilities()
1573        .iter()
1574        .map(|capability| {
1575            let interface_id = uuid::Uuid::parse_str(&capability.interface_id)
1576                .map(|interface_id| *interface_id.as_bytes())
1577                .map_err(|error| PluginRegistryBuildError::InstalledCatalog {
1578                    message: format!(
1579                        "invalid interface UUID '{}': {error}",
1580                        capability.interface_id
1581                    ),
1582                })?;
1583            Ok(WasmPluginInterfaceDeclaration::new(
1584                interface_id,
1585                capability.interface_major,
1586                capability.interface_minor,
1587                capability.instance_id.clone(),
1588            ))
1589        })
1590        .collect()
1591}