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