Skip to main content

provenant/parsers/
mod.rs

1mod about;
2#[cfg(test)]
3mod about_scan_test;
4#[cfg(test)]
5mod about_test;
6mod alpine;
7#[cfg(test)]
8mod alpine_scan_test;
9mod arch;
10#[cfg(test)]
11mod arch_scan_test;
12#[cfg(test)]
13mod arch_test;
14mod autotools;
15#[cfg(test)]
16mod autotools_test;
17mod bazel;
18#[cfg(test)]
19mod bazel_module_test;
20#[cfg(test)]
21mod bazel_test;
22mod bower;
23#[cfg(test)]
24mod bower_scan_test;
25#[cfg(test)]
26mod bower_test;
27mod buck;
28#[cfg(test)]
29mod buck_test;
30mod bun_lock;
31#[cfg(test)]
32mod bun_lock_test;
33mod bun_lockb;
34#[cfg(test)]
35mod bun_lockb_test;
36mod cargo;
37mod cargo_lock;
38#[cfg(test)]
39mod cargo_lock_test;
40#[cfg(test)]
41mod cargo_scan_test;
42#[cfg(test)]
43mod cargo_test;
44mod chef;
45#[cfg(test)]
46mod chef_scan_test;
47#[cfg(test)]
48mod chef_test;
49mod citation;
50#[cfg(test)]
51mod citation_test;
52mod clojure;
53#[cfg(test)]
54mod clojure_test;
55#[cfg(test)]
56mod cocoapods_scan_test;
57pub(crate) mod compiled_binary;
58mod composer;
59#[cfg(test)]
60mod composer_scan_test;
61#[cfg(test)]
62mod composer_test;
63mod conan;
64mod conan_data;
65#[cfg(test)]
66mod conan_data_test;
67#[cfg(test)]
68mod conan_scan_test;
69#[cfg(test)]
70mod conan_test;
71mod conda;
72mod conda_meta_json;
73#[cfg(test)]
74mod conda_meta_json_test;
75#[cfg(test)]
76mod conda_scan_test;
77#[cfg(test)]
78mod conda_test;
79mod cpan;
80mod cpan_dist_ini;
81#[cfg(test)]
82mod cpan_dist_ini_test;
83mod cpan_makefile_pl;
84#[cfg(test)]
85mod cpan_makefile_pl_test;
86#[cfg(test)]
87mod cpan_scan_test;
88#[cfg(test)]
89mod cpan_test;
90mod cran;
91#[cfg(test)]
92mod cran_scan_test;
93#[cfg(test)]
94mod cran_test;
95mod dart;
96#[cfg(test)]
97mod dart_scan_test;
98#[cfg(test)]
99mod dart_test;
100mod debian;
101#[cfg(test)]
102mod debian_scan_test;
103#[cfg(test)]
104mod debian_test;
105mod deno;
106mod deno_lock;
107#[cfg(test)]
108mod deno_lock_test;
109#[cfg(test)]
110mod deno_scan_test;
111#[cfg(test)]
112mod deno_test;
113mod docker;
114#[cfg(test)]
115mod docker_scan_test;
116#[cfg(test)]
117mod docker_test;
118mod freebsd;
119#[cfg(test)]
120mod freebsd_scan_test;
121#[cfg(test)]
122mod freebsd_test;
123mod gitmodules;
124#[cfg(test)]
125mod gitmodules_scan_test;
126mod go;
127mod go_mod_graph;
128#[cfg(test)]
129mod go_scan_test;
130#[cfg(test)]
131mod go_test;
132#[cfg(test)]
133mod go_work_test;
134#[cfg(all(test, feature = "golden-tests"))]
135pub(crate) mod golden_test_utils;
136mod gradle;
137mod gradle_lock;
138#[cfg(test)]
139mod gradle_lock_test;
140mod gradle_module;
141#[cfg(test)]
142mod gradle_module_scan_test;
143#[cfg(test)]
144mod gradle_module_test;
145#[cfg(test)]
146mod gradle_scan_test;
147mod hackage;
148#[cfg(test)]
149mod hackage_scan_test;
150#[cfg(test)]
151mod hackage_test;
152mod haxe;
153#[cfg(test)]
154mod haxe_scan_test;
155#[cfg(test)]
156mod haxe_test;
157mod helm;
158#[cfg(test)]
159mod helm_scan_test;
160#[cfg(test)]
161mod helm_test;
162mod hex_lock;
163#[cfg(test)]
164mod hex_lock_test;
165mod license_normalization;
166mod maven;
167#[cfg(test)]
168mod maven_scan_test;
169#[cfg(test)]
170mod maven_test;
171mod meson;
172#[cfg(test)]
173mod meson_scan_test;
174#[cfg(test)]
175mod meson_test;
176pub mod metadata;
177mod microsoft_update_manifest;
178#[cfg(test)]
179mod microsoft_update_manifest_test;
180mod misc;
181#[cfg(test)]
182mod misc_test;
183mod nix;
184#[cfg(test)]
185mod nix_scan_test;
186#[cfg(test)]
187mod nix_test;
188mod npm;
189mod npm_lock;
190#[cfg(test)]
191mod npm_lock_test;
192#[cfg(test)]
193mod npm_scan_test;
194#[cfg(test)]
195mod npm_test;
196mod npm_workspace;
197#[cfg(test)]
198mod npm_workspace_test;
199mod nuget;
200#[cfg(test)]
201mod nuget_scan_test;
202#[cfg(test)]
203mod nuget_test;
204mod opam;
205#[cfg(test)]
206mod opam_scan_test;
207mod os_release;
208#[cfg(test)]
209mod os_release_test;
210#[cfg(test)]
211mod osgi_test;
212mod pep508;
213mod pip_inspect_deplock;
214#[cfg(test)]
215mod pip_inspect_deplock_test;
216mod pipfile_lock;
217#[cfg(test)]
218mod pipfile_lock_test;
219mod pixi;
220#[cfg(test)]
221mod pixi_scan_test;
222#[cfg(test)]
223mod pixi_test;
224mod pnpm_lock;
225#[cfg(test)]
226mod pnpm_lock_test;
227mod podfile;
228mod podfile_lock;
229#[cfg(test)]
230mod podfile_lock_test;
231mod podspec;
232mod podspec_json;
233#[cfg(test)]
234mod podspec_json_test;
235mod poetry_lock;
236#[cfg(test)]
237mod poetry_lock_test;
238mod publiccode;
239#[cfg(test)]
240mod publiccode_test;
241mod pylock_toml;
242#[cfg(test)]
243mod pylock_toml_test;
244mod python;
245#[cfg(test)]
246mod python_scan_test;
247#[cfg(test)]
248mod python_test;
249mod readme;
250#[cfg(test)]
251mod readme_test;
252mod requirements_txt;
253#[cfg(test)]
254mod requirements_txt_test;
255pub(crate) mod rfc822;
256mod rpm_db;
257mod rpm_db_native;
258#[cfg(test)]
259mod rpm_db_scan_test;
260mod rpm_license_files;
261#[cfg(test)]
262mod rpm_license_files_test;
263mod rpm_mariner_manifest;
264#[cfg(test)]
265mod rpm_mariner_manifest_test;
266mod rpm_parser;
267#[cfg(test)]
268mod rpm_scan_test;
269mod rpm_specfile;
270#[cfg(test)]
271mod rpm_specfile_test;
272mod rpm_yumdb;
273mod ruby;
274#[cfg(test)]
275mod ruby_scan_test;
276#[cfg(test)]
277mod ruby_test;
278mod sbt;
279#[cfg(test)]
280mod sbt_test;
281#[cfg(test)]
282mod scan_test_utils;
283mod swift_manifest_json;
284#[cfg(test)]
285mod swift_manifest_json_test;
286mod swift_resolved;
287#[cfg(test)]
288mod swift_resolved_test;
289#[cfg(test)]
290mod swift_scan_test;
291mod swift_show_dependencies;
292#[cfg(test)]
293mod swift_show_dependencies_test;
294pub mod utils;
295mod uv_lock;
296#[cfg(test)]
297mod uv_lock_test;
298mod vcpkg;
299#[cfg(test)]
300mod vcpkg_scan_test;
301#[cfg(test)]
302mod vcpkg_test;
303pub(crate) mod windows_executable;
304#[cfg(test)]
305mod windows_executable_golden_test;
306mod yarn_lock;
307#[cfg(test)]
308mod yarn_lock_test;
309mod yarn_pnp;
310#[cfg(test)]
311mod yarn_pnp_test;
312
313#[cfg(all(test, feature = "golden-tests"))]
314mod golden_test;
315
316use std::cell::RefCell;
317use std::panic::{AssertUnwindSafe, catch_unwind};
318use std::path::Path;
319
320use crate::models::{PackageData, PackageType};
321use crate::parsers::license_normalization::finalize_package_declared_license_references;
322
323thread_local! {
324    static PARSER_DIAGNOSTIC_STACK: RefCell<Vec<Vec<String>>> = const { RefCell::new(Vec::new()) };
325}
326
327#[derive(Debug, Default)]
328pub struct ParsePackagesResult {
329    pub packages: Vec<PackageData>,
330    pub scan_errors: Vec<String>,
331}
332
333fn panic_payload_to_string(payload: &(dyn std::any::Any + Send)) -> String {
334    if let Some(message) = payload.downcast_ref::<&str>() {
335        (*message).to_string()
336    } else if let Some(message) = payload.downcast_ref::<String>() {
337        message.clone()
338    } else {
339        "unknown panic payload".to_string()
340    }
341}
342
343pub(crate) fn capture_parser_diagnostics<F>(
344    extract: F,
345    handler_name: &str,
346    path: &Path,
347) -> ParsePackagesResult
348where
349    F: FnOnce() -> Vec<PackageData>,
350{
351    PARSER_DIAGNOSTIC_STACK.with(|stack| {
352        stack.borrow_mut().push(Vec::new());
353    });
354
355    let extract_result = catch_unwind(AssertUnwindSafe(extract));
356    let mut scan_errors =
357        PARSER_DIAGNOSTIC_STACK.with(|stack| stack.borrow_mut().pop().unwrap_or_default());
358
359    match extract_result {
360        Ok(packages) => ParsePackagesResult {
361            packages: packages
362                .into_iter()
363                .map(|mut package| {
364                    finalize_package_declared_license_references(&mut package);
365                    package
366                })
367                .collect(),
368            scan_errors,
369        },
370        Err(payload) => {
371            scan_errors.push(format!(
372                "{} panicked while parsing {}: {}",
373                handler_name,
374                path.display(),
375                panic_payload_to_string(payload.as_ref())
376            ));
377            ParsePackagesResult {
378                packages: Vec::new(),
379                scan_errors,
380            }
381        }
382    }
383}
384
385pub(crate) fn record_parser_diagnostic(message: String) -> bool {
386    PARSER_DIAGNOSTIC_STACK.with(|stack| {
387        let mut stack = stack.borrow_mut();
388        let Some(active) = stack.last_mut() else {
389            return false;
390        };
391        active.push(message);
392        true
393    })
394}
395
396#[macro_export]
397macro_rules! parser_warn {
398    ($($arg:tt)*) => {{
399        let message = format!($($arg)*);
400        if !$crate::parsers::record_parser_diagnostic(message.clone()) {
401            log::warn!("{message}");
402        }
403    }};
404}
405
406/// Package parser trait for extracting metadata from package manifest files.
407///
408/// Each parser implementation handles a specific package manager/ecosystem
409/// (npm, Maven, Python, Cargo, etc.) and extracts standardized metadata into
410/// `PackageData` structures compatible with ScanCode Toolkit JSON output format.
411///
412/// # Implementation Guide
413///
414/// Implementors must provide:
415/// - `PACKAGE_TYPE`: Package URL (purl) type identifier (e.g., "npm", "pypi", "maven")
416/// - `is_match()`: Returns true if the given file path matches this parser's expected format
417/// - `extract_packages()`: Parses the file and returns all extracted package metadata
418///
419/// # Error Handling
420///
421/// Parsers should handle errors gracefully by returning default/empty `PackageData`
422/// and logging warnings with [`crate::parser_warn!`] rather than panicking. Scanner
423/// dispatch captures those warnings and attaches them to `FileInfo.scan_errors` so
424/// CI output and serialized scan results stay aligned.
425/// This allows the scan to continue processing other files even when individual
426/// files fail to parse.
427///
428/// # Example
429///
430/// ```ignore
431/// use provenant::models::{PackageData, PackageType};
432/// use provenant::parsers::PackageParser;
433/// use std::path::Path;
434///
435/// pub struct MyParser;
436///
437/// impl PackageParser for MyParser {
438///     const PACKAGE_TYPE: PackageType = PackageType::Npm;
439///
440///     fn is_match(path: &Path) -> bool {
441///         path.file_name().is_some_and(|name| name == "package.json")
442///     }
443///
444///     fn extract_packages(path: &Path) -> Vec<PackageData> {
445///         vec![PackageData::default()]
446///     }
447/// }
448/// ```
449pub trait PackageParser {
450    /// Package URL type identifier for this parser (e.g., PackageType::Npm, PackageType::Pypi).
451    const PACKAGE_TYPE: PackageType;
452
453    /// Extracts all packages from the given file path.
454    ///
455    /// Returns a vector of `PackageData` structures containing all extracted metadata
456    /// including name, version, dependencies, licenses, etc. Most parsers return a
457    /// single-element vector, but some (e.g., Bazel BUILD, Buck BUCK, Debian control)
458    /// can contain multiple packages in a single file.
459    ///
460    /// On parse errors, returns a vector with a default `PackageData` with minimal or
461    /// no fields populated.
462    fn extract_packages(path: &Path) -> Vec<PackageData>;
463
464    /// Checks if the given file path matches this parser's expected format.
465    ///
466    /// Returns true if the file should be handled by this parser based on filename,
467    /// extension, or path patterns. Used by the scanner to route files to appropriate parsers.
468    fn is_match(path: &Path) -> bool;
469
470    /// Returns the first package from [`extract_packages()`](Self::extract_packages),
471    /// or a default [`PackageData`] if the file contains no packages.
472    fn extract_first_package(path: &Path) -> PackageData {
473        Self::extract_packages(path)
474            .into_iter()
475            .map(|mut package| {
476                finalize_package_declared_license_references(&mut package);
477                package
478            })
479            .next()
480            .unwrap_or_default()
481    }
482}
483
484pub use self::about::AboutFileParser;
485pub use self::alpine::{AlpineApkParser, AlpineApkbuildParser, AlpineInstalledParser};
486pub use self::arch::{ArchPkginfoParser, ArchSrcinfoParser};
487pub use self::autotools::AutotoolsConfigureParser;
488pub use self::bazel::{BazelBuildParser, BazelModuleParser};
489pub use self::bower::BowerJsonParser;
490pub use self::buck::{BuckBuildParser, BuckMetadataBzlParser};
491pub use self::bun_lock::BunLockParser;
492pub use self::bun_lockb::BunLockbParser;
493pub use self::cargo::CargoParser;
494#[cfg_attr(not(test), allow(unused_imports))]
495pub use self::cargo_lock::CargoLockParser;
496pub use self::chef::{ChefMetadataJsonParser, ChefMetadataRbParser};
497pub use self::citation::CitationCffParser;
498pub use self::clojure::{ClojureDepsEdnParser, ClojureProjectCljParser};
499pub use self::composer::{ComposerJsonParser, ComposerLockParser};
500pub use self::conan::{ConanFilePyParser, ConanLockParser, ConanfileTxtParser};
501pub use self::conan_data::ConanDataParser;
502pub use self::conda::{CondaEnvironmentYmlParser, CondaMetaYamlParser};
503pub use self::conda_meta_json::CondaMetaJsonParser;
504pub use self::cpan::{CpanManifestParser, CpanMetaJsonParser, CpanMetaYmlParser};
505pub use self::cpan_dist_ini::CpanDistIniParser;
506pub use self::cpan_makefile_pl::CpanMakefilePlParser;
507pub use self::cran::CranParser;
508pub use self::dart::{PubspecLockParser, PubspecYamlParser};
509pub use self::debian::{
510    DebianControlInExtractedDebParser, DebianControlParser, DebianCopyrightParser, DebianDebParser,
511    DebianDebianTarParser, DebianDistrolessInstalledParser, DebianDscParser,
512    DebianInstalledListParser, DebianInstalledMd5sumsParser, DebianInstalledParser,
513    DebianMd5sumInPackageParser, DebianOrigTarParser,
514};
515pub use self::deno::DenoParser;
516pub use self::deno_lock::DenoLockParser;
517pub use self::docker::DockerfileParser;
518pub use self::freebsd::FreebsdCompactManifestParser;
519pub use self::gitmodules::GitmodulesParser;
520pub use self::go::{GoModParser, GoSumParser, GoWorkParser, GodepsParser};
521pub use self::go_mod_graph::GoModGraphParser;
522pub use self::gradle::GradleParser;
523pub use self::gradle_lock::GradleLockfileParser;
524pub use self::gradle_module::GradleModuleParser;
525pub use self::hackage::{HackageCabalParser, HackageCabalProjectParser, HackageStackYamlParser};
526pub use self::haxe::HaxeParser;
527pub use self::helm::{HelmChartLockParser, HelmChartYamlParser};
528pub use self::hex_lock::HexLockParser;
529pub use self::maven::MavenParser;
530pub use self::meson::MesonParser;
531pub use self::microsoft_update_manifest::MicrosoftUpdateManifestParser;
532pub use self::misc::{
533    AndroidApkRecognizer, AndroidLibraryRecognizer, AppleDmgRecognizer, Axis2MarRecognizer,
534    Axis2ModuleXmlRecognizer, CabArchiveRecognizer, ChromeCrxRecognizer, InstallShieldRecognizer,
535    IosIpaRecognizer, IsoImageRecognizer, IvyXmlRecognizer, JBossSarRecognizer,
536    JBossServiceXmlRecognizer, JavaEarAppXmlRecognizer, JavaEarRecognizer, JavaJarRecognizer,
537    JavaWarRecognizer, JavaWarWebXmlRecognizer, MeteorPackageRecognizer, MozillaXpiRecognizer,
538    NsisRecognizer, SharArchiveRecognizer, SquashfsRecognizer,
539};
540pub use self::nix::{NixDefaultParser, NixFlakeLockParser, NixFlakeParser};
541pub use self::npm::NpmParser;
542pub use self::npm_lock::NpmLockParser;
543pub use self::npm_workspace::NpmWorkspaceParser;
544pub use self::nuget::{
545    CentralPackageManagementPropsParser, DirectoryBuildPropsParser, DotNetDepsJsonParser,
546    NupkgParser, NuspecParser, PackageReferenceProjectParser, PackagesConfigParser,
547    PackagesLockParser, ProjectJsonParser, ProjectLockJsonParser,
548};
549pub use self::opam::OpamParser;
550pub use self::os_release::OsReleaseParser;
551pub use self::pip_inspect_deplock::PipInspectDeplockParser;
552pub use self::pipfile_lock::PipfileLockParser;
553pub use self::pixi::{PixiLockParser, PixiTomlParser};
554pub use self::pnpm_lock::PnpmLockParser;
555pub use self::podfile::PodfileParser;
556pub use self::podfile_lock::PodfileLockParser;
557pub use self::podspec::PodspecParser;
558pub use self::podspec_json::PodspecJsonParser;
559pub use self::poetry_lock::PoetryLockParser;
560pub use self::publiccode::PubliccodeParser;
561pub use self::pylock_toml::PylockTomlParser;
562pub use self::python::PythonParser;
563pub use self::readme::ReadmeParser;
564pub use self::requirements_txt::RequirementsTxtParser;
565#[cfg(feature = "rpm-sqlite")]
566pub use self::rpm_db::RpmSqliteDatabaseParser;
567pub use self::rpm_db::{RpmBdbDatabaseParser, RpmNdbDatabaseParser};
568pub use self::rpm_license_files::RpmLicenseFilesParser;
569pub use self::rpm_mariner_manifest::RpmMarinerManifestParser;
570pub use self::rpm_parser::RpmParser;
571pub use self::rpm_specfile::RpmSpecfileParser;
572pub use self::rpm_yumdb::RpmYumdbParser;
573pub use self::ruby::{
574    GemArchiveParser, GemMetadataExtractedParser, GemfileLockParser, GemfileParser, GemspecParser,
575};
576pub use self::sbt::SbtParser;
577pub use self::swift_manifest_json::SwiftManifestJsonParser;
578pub use self::swift_resolved::SwiftPackageResolvedParser;
579pub use self::swift_show_dependencies::SwiftShowDependenciesParser;
580pub use self::uv_lock::UvLockParser;
581pub use self::vcpkg::VcpkgManifestParser;
582pub use self::yarn_lock::YarnLockParser;
583pub use self::yarn_pnp::YarnPnpParser;
584
585/// Registers all parsers and recognizers, generating dispatch functions.
586///
587/// Parsers are tried first, then recognizers. This ordering is important because
588/// recognizers match broadly by file extension (e.g., `.jar`) and would shadow
589/// more specific parsers if checked first.
590macro_rules! register_package_handlers {
591    (
592        parsers: [$($(#[$parser_meta:meta])* $parser:ty),* $(,)?],
593        recognizers: [$($recognizer:ty),* $(,)?] $(,)?
594    ) => {
595        pub fn try_parse_file(path: &Path) -> Option<ParsePackagesResult> {
596            $(
597                $(#[$parser_meta])*
598                if <$parser>::is_match(path) {
599                    return Some(capture_parser_diagnostics(
600                        || <$parser>::extract_packages(path),
601                        stringify!($parser),
602                        path,
603                    ));
604                }
605            )*
606            $(
607                if <$recognizer>::is_match(path) {
608                    return Some(capture_parser_diagnostics(
609                        || <$recognizer>::extract_packages(path),
610                        stringify!($recognizer),
611                        path,
612                    ));
613                }
614            )*
615            None
616        }
617
618        // Used by the parser-golden maintenance tool in `xtask`.
619        // Scanner runtime dispatch goes through `try_parse_file()`.
620        #[allow(dead_code)]
621        pub fn parse_by_type_name(type_name: &str, path: &Path) -> Option<PackageData> {
622            match type_name {
623                $(
624                    $(#[$parser_meta])*
625                    stringify!($parser) => Some(<$parser>::extract_first_package(path)),
626                )*
627                $(
628                    stringify!($recognizer) => Some(<$recognizer>::extract_first_package(path)),
629                )*
630                _ => None
631            }
632        }
633
634        // Used by the parser-golden maintenance tool in `xtask` and by
635        // `tests/scanner_integration.rs` to verify parser registration.
636        #[allow(dead_code)]
637        pub fn list_parser_types() -> Vec<&'static str> {
638            vec![
639                $(
640                    $(#[$parser_meta])*
641                    stringify!($parser),
642                )*
643                $(
644                    stringify!($recognizer),
645                )*
646            ]
647        }
648    };
649}
650
651register_package_handlers! {
652    parsers: [
653        AboutFileParser,
654        AlpineApkParser,
655        AlpineApkbuildParser,
656        AlpineInstalledParser,
657        ArchPkginfoParser,
658        ArchSrcinfoParser,
659        AutotoolsConfigureParser,
660        BazelBuildParser,
661        BazelModuleParser,
662        BowerJsonParser,
663        BunLockParser,
664        BunLockbParser,
665        BuckBuildParser,
666        BuckMetadataBzlParser,
667        CargoLockParser,
668        CargoParser,
669        ChefMetadataJsonParser,
670        ChefMetadataRbParser,
671        CitationCffParser,
672        ClojureDepsEdnParser,
673        ClojureProjectCljParser,
674        ComposerJsonParser,
675        ComposerLockParser,
676        ConanDataParser,
677        ConanFilePyParser,
678        ConanfileTxtParser,
679        ConanLockParser,
680        CondaEnvironmentYmlParser,
681        CondaMetaJsonParser,
682        CondaMetaYamlParser,
683        CpanDistIniParser,
684        CpanMakefilePlParser,
685        CpanManifestParser,
686        CpanMetaJsonParser,
687        CpanMetaYmlParser,
688        CranParser,
689        DebianControlInExtractedDebParser,
690        DebianControlParser,
691        DebianCopyrightParser,
692        DebianDebianTarParser,
693        DebianDebParser,
694        DebianDistrolessInstalledParser,
695        DebianDscParser,
696        DebianInstalledListParser,
697        DebianInstalledMd5sumsParser,
698        DebianInstalledParser,
699        DebianMd5sumInPackageParser,
700        DebianOrigTarParser,
701        DenoParser,
702        DenoLockParser,
703        DockerfileParser,
704        FreebsdCompactManifestParser,
705        GemArchiveParser,
706        GemfileLockParser,
707        GemfileParser,
708        GemMetadataExtractedParser,
709        GemspecParser,
710        GitmodulesParser,
711        GodepsParser,
712        GoModParser,
713        GoModGraphParser,
714        GoSumParser,
715        GoWorkParser,
716        GradleLockfileParser,
717        GradleParser,
718        GradleModuleParser,
719        HackageCabalParser,
720        HackageCabalProjectParser,
721        HackageStackYamlParser,
722        HelmChartYamlParser,
723        HelmChartLockParser,
724        HaxeParser,
725        HexLockParser,
726        MavenParser,
727        MesonParser,
728        MicrosoftUpdateManifestParser,
729        NixDefaultParser,
730        NixFlakeLockParser,
731        NixFlakeParser,
732        NpmLockParser,
733        NpmParser,
734        NpmWorkspaceParser,
735        DotNetDepsJsonParser,
736        CentralPackageManagementPropsParser,
737        DirectoryBuildPropsParser,
738        NupkgParser,
739        NuspecParser,
740        PackageReferenceProjectParser,
741        OpamParser,
742        OsReleaseParser,
743        PackagesConfigParser,
744        PackagesLockParser,
745        ProjectJsonParser,
746        ProjectLockJsonParser,
747        PipfileLockParser,
748        PipInspectDeplockParser,
749        PixiTomlParser,
750        PixiLockParser,
751        PnpmLockParser,
752        PodfileLockParser,
753        PodfileParser,
754        PodspecJsonParser,
755        PodspecParser,
756        PoetryLockParser,
757        PubliccodeParser,
758        PylockTomlParser,
759        PubspecLockParser,
760        PubspecYamlParser,
761        PythonParser,
762        UvLockParser,
763        VcpkgManifestParser,
764        ReadmeParser,
765        RequirementsTxtParser,
766        RpmBdbDatabaseParser,
767        RpmLicenseFilesParser,
768        RpmMarinerManifestParser,
769        RpmNdbDatabaseParser,
770        RpmParser,
771        RpmSpecfileParser,
772        #[cfg(feature = "rpm-sqlite")]
773        RpmSqliteDatabaseParser,
774        RpmYumdbParser,
775        SbtParser,
776        SwiftManifestJsonParser,
777        SwiftPackageResolvedParser,
778        SwiftShowDependenciesParser,
779        YarnLockParser,
780        YarnPnpParser,
781    ],
782    recognizers: [
783        AndroidApkRecognizer,
784        AndroidLibraryRecognizer,
785        AppleDmgRecognizer,
786        Axis2MarRecognizer,
787        Axis2ModuleXmlRecognizer,
788        CabArchiveRecognizer,
789        ChromeCrxRecognizer,
790        InstallShieldRecognizer,
791        IosIpaRecognizer,
792        IsoImageRecognizer,
793        IvyXmlRecognizer,
794        JavaEarAppXmlRecognizer,
795        JavaEarRecognizer,
796        JavaJarRecognizer,
797        JavaWarRecognizer,
798        JavaWarWebXmlRecognizer,
799        JBossSarRecognizer,
800        JBossServiceXmlRecognizer,
801        MeteorPackageRecognizer,
802        MozillaXpiRecognizer,
803        NsisRecognizer,
804        SharArchiveRecognizer,
805        SquashfsRecognizer,
806    ],
807}
808
809#[cfg(test)]
810mod panic_isolation_tests {
811    use super::*;
812
813    #[test]
814    fn capture_parser_diagnostics_turns_panics_into_scan_errors() {
815        let path = Path::new("fixtures/panic-package.json");
816        let result = capture_parser_diagnostics(
817            || -> Vec<PackageData> { panic!("panic boom") },
818            "PanicParser",
819            path,
820        );
821
822        assert!(result.packages.is_empty());
823        assert_eq!(result.scan_errors.len(), 1);
824        assert!(result.scan_errors[0].contains("PanicParser"));
825        assert!(result.scan_errors[0].contains("fixtures/panic-package.json"));
826        assert!(result.scan_errors[0].contains("panic boom"));
827    }
828
829    #[test]
830    fn capture_parser_diagnostics_recovers_after_panic() {
831        let panic_path = Path::new("fixtures/panic-package.json");
832        let _ = capture_parser_diagnostics(
833            || -> Vec<PackageData> { panic!("panic boom") },
834            "PanicParser",
835            panic_path,
836        );
837
838        let ok_path = Path::new("fixtures/recovered-package.json");
839        let result = capture_parser_diagnostics(
840            || {
841                crate::parser_warn!("recoverable parser warning");
842                vec![PackageData {
843                    package_type: Some(PackageType::Npm),
844                    ..Default::default()
845                }]
846            },
847            "RecoveringParser",
848            ok_path,
849        );
850
851        assert_eq!(result.packages.len(), 1);
852        assert_eq!(result.scan_errors, vec!["recoverable parser warning"]);
853    }
854}