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