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