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