Skip to main content

provenant/parsers/
mod.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4mod about;
5#[cfg(test)]
6mod about_scan_test;
7#[cfg(test)]
8mod about_test;
9mod alpine;
10#[cfg(test)]
11mod alpine_scan_test;
12mod android;
13#[cfg(test)]
14mod android_test;
15mod arch;
16#[cfg(test)]
17mod arch_scan_test;
18#[cfg(test)]
19mod arch_test;
20pub(crate) mod archive;
21mod autotools;
22#[cfg(test)]
23mod autotools_test;
24mod bazel;
25#[cfg(test)]
26mod bazel_module_test;
27#[cfg(test)]
28mod bazel_test;
29mod bitbake;
30#[cfg(test)]
31mod bitbake_scan_test;
32#[cfg(test)]
33mod bitbake_test;
34mod bower;
35#[cfg(test)]
36mod bower_scan_test;
37#[cfg(test)]
38mod bower_test;
39mod buck;
40#[cfg(test)]
41mod buck_test;
42mod bun_lock;
43#[cfg(test)]
44mod bun_lock_test;
45mod bun_lockb;
46#[cfg(test)]
47mod bun_lockb_test;
48mod cargo;
49mod cargo_lock;
50#[cfg(test)]
51mod cargo_lock_test;
52#[cfg(test)]
53mod cargo_scan_test;
54#[cfg(test)]
55mod cargo_test;
56mod carthage;
57#[cfg(test)]
58mod carthage_scan_test;
59#[cfg(test)]
60mod carthage_test;
61mod chef;
62#[cfg(test)]
63mod chef_scan_test;
64#[cfg(test)]
65mod chef_test;
66mod citation;
67#[cfg(test)]
68mod citation_test;
69mod clojure;
70#[cfg(test)]
71mod clojure_test;
72#[cfg(test)]
73mod cocoapods_scan_test;
74pub(crate) mod compiled_binary;
75mod composer;
76#[cfg(test)]
77mod composer_scan_test;
78#[cfg(test)]
79mod composer_test;
80mod conan;
81mod conan_data;
82#[cfg(test)]
83mod conan_data_test;
84#[cfg(test)]
85mod conan_scan_test;
86#[cfg(test)]
87mod conan_test;
88mod conda;
89mod conda_meta_json;
90#[cfg(test)]
91mod conda_meta_json_test;
92#[cfg(test)]
93mod conda_scan_test;
94#[cfg(test)]
95mod conda_test;
96mod cpan;
97mod cpan_dist_ini;
98#[cfg(test)]
99mod cpan_dist_ini_test;
100mod cpan_makefile_pl;
101#[cfg(test)]
102mod cpan_makefile_pl_test;
103#[cfg(test)]
104mod cpan_scan_test;
105#[cfg(test)]
106mod cpan_test;
107mod cran;
108#[cfg(test)]
109mod cran_scan_test;
110#[cfg(test)]
111mod cran_test;
112mod dart;
113#[cfg(test)]
114mod dart_scan_test;
115#[cfg(test)]
116mod dart_test;
117mod debian;
118mod declared_license_aliases;
119mod deno;
120mod deno_lock;
121#[cfg(test)]
122mod deno_lock_test;
123#[cfg(test)]
124mod deno_scan_test;
125#[cfg(test)]
126mod deno_test;
127mod docker;
128#[cfg(test)]
129mod docker_scan_test;
130#[cfg(test)]
131mod docker_test;
132mod erlang_otp;
133#[cfg(test)]
134mod erlang_otp_scan_test;
135#[cfg(test)]
136mod erlang_otp_test;
137mod freebsd;
138#[cfg(test)]
139mod freebsd_scan_test;
140#[cfg(test)]
141mod freebsd_test;
142mod gitmodules;
143#[cfg(test)]
144mod gitmodules_scan_test;
145mod go;
146mod go_mod_graph;
147#[cfg(test)]
148mod go_scan_test;
149#[cfg(test)]
150mod go_test;
151#[cfg(test)]
152mod go_work_test;
153#[cfg(feature = "golden-tests")]
154pub mod golden_test_utils;
155mod gradle;
156mod gradle_lock;
157#[cfg(test)]
158mod gradle_lock_test;
159mod gradle_module;
160#[cfg(test)]
161mod gradle_module_scan_test;
162#[cfg(test)]
163mod gradle_module_test;
164#[cfg(test)]
165mod gradle_scan_test;
166mod hackage;
167#[cfg(test)]
168mod hackage_scan_test;
169#[cfg(test)]
170mod hackage_test;
171mod haxe;
172#[cfg(test)]
173mod haxe_scan_test;
174#[cfg(test)]
175mod haxe_test;
176mod helm;
177#[cfg(test)]
178mod helm_scan_test;
179#[cfg(test)]
180mod helm_test;
181mod hex_lock;
182#[cfg(test)]
183mod hex_lock_test;
184mod huggingface;
185#[cfg(test)]
186mod huggingface_scan_test;
187#[cfg(test)]
188mod huggingface_test;
189mod ivy;
190#[cfg(test)]
191mod ivy_scan_test;
192#[cfg(test)]
193mod ivy_test;
194mod julia;
195#[cfg(test)]
196mod julia_test;
197pub(crate) mod license_normalization;
198mod maven;
199mod meson;
200#[cfg(test)]
201mod meson_scan_test;
202#[cfg(test)]
203mod meson_test;
204pub mod metadata;
205mod microsoft_update_manifest;
206#[cfg(test)]
207mod microsoft_update_manifest_test;
208mod misc;
209#[cfg(test)]
210mod misc_test;
211mod nix;
212#[cfg(test)]
213mod nix_scan_test;
214#[cfg(test)]
215mod nix_test;
216mod npm;
217mod npm_lock;
218#[cfg(test)]
219mod npm_lock_test;
220#[cfg(test)]
221mod npm_scan_test;
222#[cfg(test)]
223mod npm_test;
224mod npm_workspace;
225#[cfg(test)]
226mod npm_workspace_test;
227mod nuget;
228mod oci;
229#[cfg(test)]
230mod oci_scan_test;
231#[cfg(test)]
232mod oci_test;
233mod opam;
234#[cfg(test)]
235mod opam_scan_test;
236mod os_release;
237#[cfg(test)]
238mod os_release_test;
239mod pep508;
240mod pip_inspect_deplock;
241#[cfg(test)]
242mod pip_inspect_deplock_test;
243mod pipfile_lock;
244#[cfg(test)]
245mod pipfile_lock_test;
246mod pixi;
247#[cfg(test)]
248mod pixi_scan_test;
249#[cfg(test)]
250mod pixi_test;
251mod pnpm_lock;
252#[cfg(test)]
253mod pnpm_lock_test;
254mod podfile;
255mod podfile_lock;
256#[cfg(test)]
257mod podfile_lock_test;
258mod podspec;
259mod podspec_json;
260#[cfg(test)]
261mod podspec_json_test;
262mod poetry_lock;
263#[cfg(test)]
264mod poetry_lock_test;
265mod publiccode;
266#[cfg(test)]
267mod publiccode_test;
268mod pylock_toml;
269#[cfg(test)]
270mod pylock_toml_test;
271mod python;
272mod readme;
273#[cfg(test)]
274mod readme_test;
275mod requirements_txt;
276#[cfg(test)]
277mod requirements_txt_test;
278pub(crate) mod rfc822;
279mod rpm_db;
280mod rpm_db_native;
281#[cfg(test)]
282mod rpm_db_scan_test;
283mod rpm_license_files;
284#[cfg(test)]
285mod rpm_license_files_test;
286mod rpm_mariner_manifest;
287#[cfg(test)]
288mod rpm_mariner_manifest_test;
289mod rpm_parser;
290#[cfg(test)]
291mod rpm_scan_test;
292mod rpm_specfile;
293#[cfg(test)]
294mod rpm_specfile_test;
295mod rpm_yumdb;
296mod ruby;
297#[cfg(test)]
298mod ruby_scan_test;
299#[cfg(test)]
300mod ruby_test;
301mod sbt;
302#[cfg(test)]
303mod sbt_test;
304#[cfg(test)]
305mod scan_test_utils;
306mod starlark_parse;
307mod swift_manifest_json;
308#[cfg(test)]
309mod swift_manifest_json_test;
310mod swift_resolved;
311#[cfg(test)]
312mod swift_resolved_test;
313#[cfg(test)]
314mod swift_scan_test;
315mod swift_show_dependencies;
316#[cfg(test)]
317mod swift_show_dependencies_test;
318pub mod utils;
319mod uv_lock;
320#[cfg(test)]
321mod uv_lock_test;
322mod vcpkg;
323#[cfg(test)]
324mod vcpkg_scan_test;
325#[cfg(test)]
326mod vcpkg_test;
327pub(crate) mod windows_executable;
328#[cfg(test)]
329mod windows_executable_golden_test;
330mod yarn_lock;
331#[cfg(test)]
332mod yarn_lock_test;
333mod yarn_pnp;
334#[cfg(test)]
335mod yarn_pnp_test;
336
337use std::cell::RefCell;
338use std::panic::{AssertUnwindSafe, catch_unwind};
339use std::path::Path;
340use std::sync::Arc;
341
342use crate::license_detection::LicenseDetectionEngine;
343use crate::models::{DiagnosticSeverity, PackageData, PackageType, ScanDiagnostic};
344use crate::parsers::license_normalization::{
345    finalize_package_declared_license_references, populate_declared_license_and_holder,
346};
347use crate::parsers::utils::CappedIterExt;
348
349thread_local! {
350    static PARSER_DIAGNOSTIC_STACK: RefCell<Vec<Vec<ScanDiagnostic>>> = const { RefCell::new(Vec::new()) };
351    static PARSER_LICENSE_ENGINE_STACK: RefCell<Vec<Option<Arc<LicenseDetectionEngine>>>> = const { RefCell::new(Vec::new()) };
352    static PARSER_SCAN_ROOT_STACK: RefCell<Vec<Option<std::path::PathBuf>>> = const { RefCell::new(Vec::new()) };
353}
354
355#[derive(Debug, Default)]
356pub struct ParsePackagesResult {
357    pub packages: Vec<PackageData>,
358    pub scan_diagnostics: Vec<ScanDiagnostic>,
359}
360
361fn panic_payload_to_string(payload: &(dyn std::any::Any + Send)) -> String {
362    if let Some(message) = payload.downcast_ref::<&str>() {
363        (*message).to_string()
364    } else if let Some(message) = payload.downcast_ref::<String>() {
365        message.clone()
366    } else {
367        "unknown panic payload".to_string()
368    }
369}
370
371pub(crate) fn capture_parser_diagnostics<F>(
372    extract: F,
373    handler_name: &str,
374    path: &Path,
375    license_engine: Option<Arc<LicenseDetectionEngine>>,
376) -> ParsePackagesResult
377where
378    F: FnOnce() -> Vec<PackageData>,
379{
380    PARSER_DIAGNOSTIC_STACK.with(|stack| {
381        stack.borrow_mut().push(Vec::new());
382    });
383    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
384        stack.borrow_mut().push(license_engine);
385    });
386
387    let extract_result = catch_unwind(AssertUnwindSafe(|| {
388        extract()
389            .into_iter()
390            .map(|mut package| {
391                populate_declared_license_and_holder(&mut package);
392                finalize_package_declared_license_references(&mut package);
393                package
394            })
395            .capped("packages emitted by parser")
396            .collect::<Vec<_>>()
397    }));
398    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
399        stack.borrow_mut().pop();
400    });
401    let mut scan_diagnostics =
402        PARSER_DIAGNOSTIC_STACK.with(|stack| stack.borrow_mut().pop().unwrap_or_default());
403
404    match extract_result {
405        Ok(packages) => ParsePackagesResult {
406            packages,
407            scan_diagnostics,
408        },
409        Err(payload) => {
410            scan_diagnostics.push(ScanDiagnostic::error(format!(
411                "{} panicked while parsing {}: {}",
412                handler_name,
413                path.display(),
414                panic_payload_to_string(payload.as_ref())
415            )));
416            ParsePackagesResult {
417                packages: Vec::new(),
418                scan_diagnostics,
419            }
420        }
421    }
422}
423
424pub(crate) fn active_parser_license_engine() -> Option<Arc<LicenseDetectionEngine>> {
425    PARSER_LICENSE_ENGINE_STACK.with(|stack| stack.borrow().last().cloned().flatten())
426}
427
428pub(crate) fn active_parser_scan_root() -> Option<std::path::PathBuf> {
429    PARSER_SCAN_ROOT_STACK.with(|stack| stack.borrow().last().cloned().flatten())
430}
431
432pub(crate) fn with_parser_scan_root<T>(scan_root: Option<&Path>, f: impl FnOnce() -> T) -> T {
433    PARSER_SCAN_ROOT_STACK.with(|stack| {
434        stack.borrow_mut().push(scan_root.map(Path::to_path_buf));
435    });
436    let result = f();
437    PARSER_SCAN_ROOT_STACK.with(|stack| {
438        stack.borrow_mut().pop();
439    });
440    result
441}
442
443/// Make a license-detection engine the active one for parser-level declared-license
444/// normalization within `f`. Used to share the scan's already-built engine with the
445/// assembly phase so cross-file resolution does not rebuild a redundant engine.
446pub(crate) fn with_parser_license_engine<T>(
447    engine: Option<Arc<LicenseDetectionEngine>>,
448    f: impl FnOnce() -> T,
449) -> T {
450    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
451        stack.borrow_mut().push(engine);
452    });
453    let result = f();
454    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
455        stack.borrow_mut().pop();
456    });
457    result
458}
459
460pub(crate) fn record_parser_diagnostic(message: String, severity: DiagnosticSeverity) -> bool {
461    PARSER_DIAGNOSTIC_STACK.with(|stack| {
462        let mut stack = stack.borrow_mut();
463        let Some(active) = stack.last_mut() else {
464            return false;
465        };
466        active.push(ScanDiagnostic { severity, message });
467        true
468    })
469}
470
471#[macro_export]
472macro_rules! parser_warn {
473    ($($arg:tt)*) => {{
474        let message = format!($($arg)*);
475        if !$crate::parsers::record_parser_diagnostic(
476            message.clone(),
477            $crate::models::DiagnosticSeverity::Warning,
478        ) {
479            log::warn!("{message}");
480        }
481    }};
482}
483
484/// Package parser trait for extracting metadata from package manifest files.
485///
486/// Each parser implementation handles a specific package manager/ecosystem
487/// (npm, Maven, Python, Cargo, etc.) and extracts standardized metadata into
488/// `PackageData` structures compatible with ScanCode Toolkit JSON output format.
489///
490/// # Implementation Guide
491///
492/// Implementors must provide:
493/// - `PACKAGE_TYPE`: Package URL (purl) type identifier (e.g., "npm", "pypi", "maven")
494/// - `is_match()`: Returns true if the given file path matches this parser's expected format
495/// - `extract_packages()`: Parses the file and returns all extracted package metadata
496///
497/// # Error Handling
498///
499/// Parsers should handle errors gracefully by returning default/empty `PackageData`
500/// and logging warnings with [`crate::parser_warn!`] rather than panicking. Scanner
501/// dispatch captures those warnings and attaches them to `FileInfo.scan_diagnostics` so
502/// CI output and serialized scan results stay aligned.
503/// This allows the scan to continue processing other files even when individual
504/// files fail to parse.
505///
506/// # Example
507///
508/// ```no_run
509/// use provenant::models::{PackageData, PackageType};
510/// use provenant::parsers::PackageParser;
511/// use std::path::Path;
512///
513/// pub struct MyParser;
514///
515/// impl PackageParser for MyParser {
516///     const PACKAGE_TYPE: PackageType = PackageType::Npm;
517///
518///     fn is_match(path: &Path) -> bool {
519///         path.file_name().is_some_and(|name| name == "package.json")
520///     }
521///
522///     fn extract_packages(path: &Path) -> Vec<PackageData> {
523///         vec![PackageData::default()]
524///     }
525/// }
526/// ```
527pub trait PackageParser {
528    /// Package URL type identifier for this parser (e.g., PackageType::Npm, PackageType::Pypi).
529    const PACKAGE_TYPE: PackageType;
530
531    /// Extracts all packages from the given file path.
532    ///
533    /// Returns a vector of `PackageData` structures containing all extracted metadata
534    /// including name, version, dependencies, licenses, etc. Most parsers return a
535    /// single-element vector, but some (e.g., Bazel BUILD, Buck BUCK, Debian control)
536    /// can contain multiple packages in a single file.
537    ///
538    /// On parse errors, returns a vector with a default `PackageData` with minimal or
539    /// no fields populated.
540    fn extract_packages(path: &Path) -> Vec<PackageData>;
541
542    /// Checks if the given file path matches this parser's expected format.
543    ///
544    /// Returns true if the file should be handled by this parser based on filename,
545    /// extension, or path patterns. Used by the scanner to route files to appropriate parsers.
546    fn is_match(path: &Path) -> bool;
547
548    /// Returns the first package from [`extract_packages()`](Self::extract_packages),
549    /// or a default [`PackageData`] if the file contains no packages.
550    fn extract_first_package(path: &Path) -> PackageData {
551        Self::extract_packages(path)
552            .into_iter()
553            .map(|mut package| {
554                populate_declared_license_and_holder(&mut package);
555                finalize_package_declared_license_references(&mut package);
556                package
557            })
558            .next()
559            .unwrap_or_default()
560    }
561
562    /// Returns documentation metadata for the file-format surfaces this parser handles.
563    ///
564    /// Used to auto-generate `docs/SUPPORTED_FORMATS.md`. Parsers that share a
565    /// documentation entry with another parser (e.g., AlpineApkParser and
566    /// AlpineInstalledParser) should return the entry from only one of them
567    /// and use the default empty implementation in the other.
568    fn metadata() -> Vec<metadata::ParserMetadata> {
569        Vec::new()
570    }
571}
572
573pub fn try_parse_rpm_archive_with_license_engine(
574    path: &Path,
575    license_engine: Option<Arc<LicenseDetectionEngine>>,
576) -> Option<ParsePackagesResult> {
577    if !self::rpm_parser::path_looks_like_rpm_archive(path) {
578        return None;
579    }
580
581    if <RpmParser as PackageParser>::is_match(path) {
582        return Some(capture_parser_diagnostics(
583            || self::rpm_parser::extract_rpm_packages(path),
584            stringify!(RpmParser),
585            path,
586            license_engine,
587        ));
588    }
589
590    None
591}
592
593pub fn try_parse_rpm_archive(path: &Path) -> Option<ParsePackagesResult> {
594    try_parse_rpm_archive_with_license_engine(path, None)
595}
596
597#[cfg(feature = "golden-tests")]
598pub fn try_parse_compiled_bytes(bytes: &[u8]) -> Option<ParsePackagesResult> {
599    self::compiled_binary::try_parse_compiled_bytes(bytes)
600}
601
602#[cfg(feature = "golden-tests")]
603pub fn try_parse_windows_executable_bytes(
604    path: &Path,
605    bytes: &[u8],
606) -> Option<ParsePackagesResult> {
607    self::windows_executable::try_parse_windows_executable_bytes(path, bytes)
608}
609
610pub fn path_looks_like_rpm_archive(path: &Path) -> bool {
611    self::rpm_parser::path_looks_like_rpm_archive(path)
612}
613
614/// Collects all registered parser and detection-surface metadata.
615///
616/// Used by the `generate-supported-formats` xtask to auto-generate
617/// `docs/SUPPORTED_FORMATS.md`.
618pub fn all_metadata() -> Vec<metadata::ParserMetadata> {
619    let mut entries = collect_parser_metadata();
620    entries.extend_from_slice(self::compiled_binary::COMPILED_BINARY_METADATA);
621    entries.extend_from_slice(self::windows_executable::WINDOWS_EXE_METADATA);
622    entries.extend_from_slice(self::misc::RECOGNIZER_METADATA);
623    entries.extend_from_slice(crate::utils::font::FONT_METADATA);
624    entries
625}
626
627pub use self::about::AboutFileParser;
628pub use self::alpine::{AlpineApkParser, AlpineApkbuildParser, AlpineInstalledParser};
629pub use self::android::{
630    AndroidAabParser, AndroidApkParser, AndroidManifestParser, AndroidSoongMetadataParser,
631};
632#[cfg(feature = "golden-tests")]
633pub use self::android::{
634    ProtoItem, ProtoPrimitive, ProtoRawStringValue, ProtoSourcePosition, ProtoStringValue,
635    ProtoXmlAttribute, ProtoXmlElement, ProtoXmlNamespace, ProtoXmlNode, proto_item,
636    proto_primitive, proto_xml_node,
637};
638pub use self::arch::{ArchPkginfoParser, ArchSrcinfoParser};
639pub use self::autotools::AutotoolsConfigureParser;
640pub use self::bazel::{BazelBuildParser, BazelModuleParser};
641pub use self::bitbake::BitbakeRecipeParser;
642pub use self::bower::BowerJsonParser;
643pub use self::buck::{BuckBuildParser, BuckMetadataBzlParser};
644pub use self::bun_lock::BunLockParser;
645pub use self::bun_lockb::BunLockbParser;
646pub use self::cargo::CargoParser;
647#[cfg_attr(not(test), allow(unused_imports))]
648pub use self::cargo_lock::CargoLockParser;
649pub use self::carthage::{CarthageCartfileParser, CarthageCartfileResolvedParser};
650pub use self::chef::{ChefMetadataJsonParser, ChefMetadataRbParser};
651pub use self::citation::CitationCffParser;
652pub use self::clojure::{ClojureDepsEdnParser, ClojureProjectCljParser};
653pub use self::composer::{ComposerJsonParser, ComposerLockParser};
654pub use self::conan::{ConanFilePyParser, ConanLockParser, ConanfileTxtParser};
655pub use self::conan_data::ConanDataParser;
656pub use self::conda::{CondaEnvironmentYmlParser, CondaMetaYamlParser};
657pub use self::conda_meta_json::CondaMetaJsonParser;
658pub use self::cpan::{CpanManifestParser, CpanMetaJsonParser, CpanMetaYmlParser};
659pub use self::cpan_dist_ini::CpanDistIniParser;
660pub use self::cpan_makefile_pl::CpanMakefilePlParser;
661pub use self::cran::CranParser;
662pub use self::dart::{PubspecLockParser, PubspecYamlParser};
663pub use self::debian::{
664    DebianControlInExtractedDebParser, DebianControlParser, DebianCopyrightParser, DebianDebParser,
665    DebianDebianTarParser, DebianDistrolessInstalledParser, DebianDscParser,
666    DebianInstalledListParser, DebianInstalledMd5sumsParser, DebianInstalledParser,
667    DebianMd5sumInPackageParser, DebianOrigTarParser,
668};
669pub use self::deno::DenoParser;
670pub use self::deno_lock::DenoLockParser;
671pub use self::docker::DockerfileParser;
672pub use self::erlang_otp::{ErlangAppSrcParser, RebarConfigParser, RebarLockParser};
673pub use self::freebsd::FreebsdCompactManifestParser;
674pub use self::gitmodules::GitmodulesParser;
675pub use self::go::{GoModParser, GoSumParser, GoWorkParser, GodepsParser};
676pub use self::go_mod_graph::GoModGraphParser;
677pub use self::gradle::GradleParser;
678pub use self::gradle_lock::GradleLockfileParser;
679pub use self::gradle_module::GradleModuleParser;
680pub use self::hackage::{HackageCabalParser, HackageCabalProjectParser, HackageStackYamlParser};
681pub use self::haxe::HaxeParser;
682pub use self::helm::{HelmChartLockParser, HelmChartYamlParser};
683pub use self::hex_lock::HexLockParser;
684pub use self::huggingface::{
685    HuggingfaceConfigParser, HuggingfaceModelCardParser, HuggingfaceModelIndexParser,
686};
687pub use self::ivy::{IvyDependenciesPropertiesParser, IvyXmlParser};
688pub use self::julia::{JuliaManifestTomlParser, JuliaProjectTomlParser};
689pub use self::maven::MavenParser;
690pub use self::meson::MesonParser;
691pub use self::microsoft_update_manifest::MicrosoftUpdateManifestParser;
692pub use self::misc::{
693    AndroidLibraryRecognizer, AppleDmgRecognizer, Axis2MarRecognizer, Axis2ModuleXmlRecognizer,
694    CabArchiveRecognizer, ChromeCrxRecognizer, InstallShieldRecognizer, IosIpaRecognizer,
695    IsoImageRecognizer, JBossSarRecognizer, JBossServiceXmlRecognizer, JavaEarAppXmlRecognizer,
696    JavaEarRecognizer, JavaJarRecognizer, JavaWarRecognizer, JavaWarWebXmlRecognizer,
697    MeteorPackageRecognizer, MozillaXpiRecognizer, NsisRecognizer, SharArchiveRecognizer,
698    SquashfsRecognizer,
699};
700pub use self::nix::{NixDefaultParser, NixFlakeLockParser, NixFlakeParser};
701pub use self::npm::NpmParser;
702pub use self::npm_lock::NpmLockParser;
703pub use self::npm_workspace::NpmWorkspaceParser;
704pub use self::nuget::{
705    CentralPackageManagementPropsParser, DirectoryBuildPropsParser, DotNetDepsJsonParser,
706    NupkgParser, NuspecParser, PackageReferenceProjectParser, PackagesConfigParser,
707    PackagesLockParser, ProjectJsonParser, ProjectLockJsonParser,
708};
709pub use self::oci::OciImageLayoutParser;
710pub use self::opam::OpamParser;
711pub use self::os_release::OsReleaseParser;
712pub use self::pip_inspect_deplock::PipInspectDeplockParser;
713pub use self::pipfile_lock::PipfileLockParser;
714pub use self::pixi::{PixiLockParser, PixiTomlParser};
715pub use self::pnpm_lock::PnpmLockParser;
716pub use self::podfile::PodfileParser;
717pub use self::podfile_lock::PodfileLockParser;
718pub use self::podspec::PodspecParser;
719pub use self::podspec_json::PodspecJsonParser;
720pub use self::poetry_lock::PoetryLockParser;
721pub use self::publiccode::PubliccodeParser;
722pub use self::pylock_toml::PylockTomlParser;
723pub use self::python::PythonParser;
724pub use self::readme::ReadmeParser;
725pub use self::requirements_txt::RequirementsTxtParser;
726#[cfg(feature = "rpm-sqlite")]
727pub use self::rpm_db::RpmSqliteDatabaseParser;
728pub use self::rpm_db::{RpmBdbDatabaseParser, RpmNdbDatabaseParser};
729pub use self::rpm_license_files::RpmLicenseFilesParser;
730pub use self::rpm_mariner_manifest::RpmMarinerManifestParser;
731pub use self::rpm_parser::RpmParser;
732pub use self::rpm_specfile::RpmSpecfileParser;
733pub use self::rpm_yumdb::RpmYumdbParser;
734pub use self::ruby::{
735    GemArchiveParser, GemMetadataExtractedParser, GemfileLockParser, GemfileParser, GemspecParser,
736};
737pub use self::sbt::SbtParser;
738pub use self::swift_manifest_json::SwiftManifestJsonParser;
739pub use self::swift_resolved::SwiftPackageResolvedParser;
740pub use self::swift_show_dependencies::SwiftShowDependenciesParser;
741pub use self::uv_lock::UvLockParser;
742pub use self::vcpkg::{
743    VcpkgConfigurationParser, VcpkgControlParser, VcpkgLockParser, VcpkgManifestParser,
744};
745pub use self::yarn_lock::YarnLockParser;
746pub use self::yarn_pnp::YarnPnpParser;
747
748/// Registers all parsers and recognizers, generating dispatch functions.
749///
750/// Parsers are tried first, then recognizers. This ordering is important because
751/// recognizers match broadly by file extension (e.g., `.jar`) and would shadow
752/// more specific parsers if checked first.
753macro_rules! register_package_handlers {
754    (
755        parsers: [$($(#[$parser_meta:meta])* $parser:ty),* $(,)?],
756        recognizers: [$($recognizer:ty),* $(,)?] $(,)?
757    ) => {
758        pub fn try_parse_file_with_license_engine(
759            path: &Path,
760            license_engine: Option<Arc<LicenseDetectionEngine>>,
761        ) -> Option<ParsePackagesResult> {
762            $(
763                $(#[$parser_meta])*
764                if <$parser>::is_match(path) {
765                    return Some(capture_parser_diagnostics(
766                        || <$parser>::extract_packages(path),
767                        stringify!($parser),
768                        path,
769                        license_engine.clone(),
770                    ));
771                }
772            )*
773            $(
774                if <$recognizer>::is_match(path) {
775                    return Some(capture_parser_diagnostics(
776                        || <$recognizer>::extract_packages(path),
777                        stringify!($recognizer),
778                        path,
779                        license_engine.clone(),
780                    ));
781                }
782            )*
783            None
784        }
785
786        pub fn try_parse_file(path: &Path) -> Option<ParsePackagesResult> {
787            try_parse_file_with_license_engine(path, None)
788        }
789
790        // Used by the parser-golden maintenance tool in `xtask`.
791        // Scanner runtime dispatch goes through `try_parse_file()`.
792        #[allow(dead_code)]
793        pub fn parse_by_type_name(type_name: &str, path: &Path) -> Option<PackageData> {
794            match type_name {
795                $(
796                    $(#[$parser_meta])*
797                    stringify!($parser) => Some(<$parser>::extract_first_package(path)),
798                )*
799                $(
800                    stringify!($recognizer) => Some(<$recognizer>::extract_first_package(path)),
801                )*
802                _ => None
803            }
804        }
805
806        // Used by the parser-golden maintenance tool in `xtask` and by
807        // `tests/scanner_integration.rs` to verify parser registration.
808        #[allow(dead_code)]
809        pub fn list_parser_types() -> Vec<&'static str> {
810            vec![
811                $(
812                    $(#[$parser_meta])*
813                    stringify!($parser),
814                )*
815                $(
816                    stringify!($recognizer),
817                )*
818            ]
819        }
820
821        /// Collects documentation metadata from all registered parsers and recognizers.
822        pub fn collect_parser_metadata() -> Vec<metadata::ParserMetadata> {
823            let mut entries = Vec::new();
824            $(
825                $(#[$parser_meta])*
826                entries.extend(<$parser>::metadata());
827            )*
828            $(
829                entries.extend(<$recognizer>::metadata());
830            )*
831            entries
832        }
833    };
834}
835
836#[cfg(test)]
837mod tests {
838    use std::collections::HashMap;
839
840    use super::{active_parser_license_engine, capture_parser_diagnostics};
841    use crate::license_detection::LicenseDetectionEngine;
842    use crate::models::PackageData;
843    use crate::parsers::license_normalization::{
844        clear_last_parser_license_engine_ptr, last_parser_license_engine_ptr,
845    };
846    use std::path::Path;
847    use std::sync::Arc;
848
849    #[test]
850    fn test_capture_parser_diagnostics_exposes_active_license_engine() {
851        let engine =
852            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
853
854        let result = capture_parser_diagnostics(
855            || {
856                assert!(active_parser_license_engine().is_some());
857                vec![PackageData::default()]
858            },
859            "TestParser",
860            Path::new("testdata/package.json"),
861            Some(engine),
862        );
863
864        assert_eq!(result.packages.len(), 1);
865        assert!(active_parser_license_engine().is_none());
866    }
867
868    #[test]
869    fn test_capture_parser_diagnostics_keeps_active_license_engine_for_finalization() {
870        let engine =
871            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
872        clear_last_parser_license_engine_ptr();
873
874        let result = capture_parser_diagnostics(
875            || {
876                vec![PackageData {
877                    declared_license_expression: Some("mit".to_string()),
878                    declared_license_expression_spdx: Some("MIT".to_string()),
879                    extracted_license_statement: Some("MIT".to_string()),
880                    extra_data: Some(HashMap::from([(
881                        "license_file".to_string(),
882                        serde_json::Value::String("LICENSE".to_string()),
883                    )])),
884                    ..Default::default()
885                }]
886            },
887            "TestParser",
888            Path::new("testdata/package.json"),
889            Some(Arc::clone(&engine)),
890        );
891
892        assert_eq!(result.packages.len(), 1);
893        assert_eq!(
894            last_parser_license_engine_ptr(),
895            Some(Arc::as_ptr(&engine) as usize)
896        );
897        assert_eq!(
898            result.packages[0].license_detections[0].matches[0]
899                .referenced_filenames
900                .as_ref(),
901            Some(&vec!["LICENSE".to_string()])
902        );
903        assert!(active_parser_license_engine().is_none());
904    }
905}
906
907register_package_handlers! {
908    parsers: [
909        AboutFileParser,
910        AndroidAabParser,
911        AndroidApkParser,
912        AndroidManifestParser,
913        AndroidSoongMetadataParser,
914        AlpineApkParser,
915        AlpineApkbuildParser,
916        AlpineInstalledParser,
917        ArchPkginfoParser,
918        ArchSrcinfoParser,
919        AutotoolsConfigureParser,
920        BazelBuildParser,
921        BazelModuleParser,
922        BitbakeRecipeParser,
923        BowerJsonParser,
924        BunLockParser,
925        BunLockbParser,
926        BuckBuildParser,
927        BuckMetadataBzlParser,
928        CargoLockParser,
929        CargoParser,
930        CarthageCartfileParser,
931        CarthageCartfileResolvedParser,
932        ChefMetadataJsonParser,
933        ChefMetadataRbParser,
934        CitationCffParser,
935        ClojureDepsEdnParser,
936        ClojureProjectCljParser,
937        ComposerJsonParser,
938        ComposerLockParser,
939        ConanDataParser,
940        ConanFilePyParser,
941        ConanfileTxtParser,
942        ConanLockParser,
943        CondaEnvironmentYmlParser,
944        CondaMetaJsonParser,
945        CondaMetaYamlParser,
946        CpanDistIniParser,
947        CpanMakefilePlParser,
948        CpanManifestParser,
949        CpanMetaJsonParser,
950        CpanMetaYmlParser,
951        CranParser,
952        DebianControlInExtractedDebParser,
953        DebianControlParser,
954        DebianCopyrightParser,
955        DebianDebianTarParser,
956        DebianDebParser,
957        DebianDistrolessInstalledParser,
958        DebianDscParser,
959        DebianInstalledListParser,
960        DebianInstalledMd5sumsParser,
961        DebianInstalledParser,
962        DebianMd5sumInPackageParser,
963        DebianOrigTarParser,
964        DenoParser,
965        DenoLockParser,
966        DockerfileParser,
967        ErlangAppSrcParser,
968        RebarConfigParser,
969        RebarLockParser,
970        FreebsdCompactManifestParser,
971        GemArchiveParser,
972        GemfileLockParser,
973        GemfileParser,
974        GemMetadataExtractedParser,
975        GemspecParser,
976        GitmodulesParser,
977        GodepsParser,
978        GoModParser,
979        GoModGraphParser,
980        GoSumParser,
981        GoWorkParser,
982        GradleLockfileParser,
983        GradleParser,
984        GradleModuleParser,
985        HackageCabalParser,
986        HackageCabalProjectParser,
987        HackageStackYamlParser,
988        HelmChartYamlParser,
989        HelmChartLockParser,
990        HaxeParser,
991        HexLockParser,
992        HuggingfaceModelCardParser,
993        HuggingfaceConfigParser,
994        HuggingfaceModelIndexParser,
995        IvyDependenciesPropertiesParser,
996        IvyXmlParser,
997        JuliaManifestTomlParser,
998        JuliaProjectTomlParser,
999        MavenParser,
1000        MesonParser,
1001        MicrosoftUpdateManifestParser,
1002        NixDefaultParser,
1003        NixFlakeLockParser,
1004        NixFlakeParser,
1005        NpmLockParser,
1006        NpmParser,
1007        NpmWorkspaceParser,
1008        DotNetDepsJsonParser,
1009        CentralPackageManagementPropsParser,
1010        DirectoryBuildPropsParser,
1011        NupkgParser,
1012        NuspecParser,
1013        PackageReferenceProjectParser,
1014        OciImageLayoutParser,
1015        OpamParser,
1016        OsReleaseParser,
1017        PackagesConfigParser,
1018        PackagesLockParser,
1019        ProjectJsonParser,
1020        ProjectLockJsonParser,
1021        PipfileLockParser,
1022        PipInspectDeplockParser,
1023        PixiTomlParser,
1024        PixiLockParser,
1025        PnpmLockParser,
1026        PodfileLockParser,
1027        PodfileParser,
1028        PodspecJsonParser,
1029        PodspecParser,
1030        PoetryLockParser,
1031        PubliccodeParser,
1032        PylockTomlParser,
1033        PubspecLockParser,
1034        PubspecYamlParser,
1035        PythonParser,
1036        UvLockParser,
1037        VcpkgConfigurationParser,
1038        VcpkgControlParser,
1039        VcpkgLockParser,
1040        VcpkgManifestParser,
1041        ReadmeParser,
1042        RequirementsTxtParser,
1043        RpmBdbDatabaseParser,
1044        RpmLicenseFilesParser,
1045        RpmMarinerManifestParser,
1046        RpmNdbDatabaseParser,
1047        RpmParser,
1048        RpmSpecfileParser,
1049        #[cfg(feature = "rpm-sqlite")]
1050        RpmSqliteDatabaseParser,
1051        RpmYumdbParser,
1052        SbtParser,
1053        SwiftManifestJsonParser,
1054        SwiftPackageResolvedParser,
1055        SwiftShowDependenciesParser,
1056        YarnLockParser,
1057        YarnPnpParser,
1058    ],
1059    recognizers: [
1060        AndroidLibraryRecognizer,
1061        AppleDmgRecognizer,
1062        Axis2MarRecognizer,
1063        Axis2ModuleXmlRecognizer,
1064        CabArchiveRecognizer,
1065        ChromeCrxRecognizer,
1066        InstallShieldRecognizer,
1067        IosIpaRecognizer,
1068        IsoImageRecognizer,
1069        JavaEarAppXmlRecognizer,
1070        JavaEarRecognizer,
1071        JavaJarRecognizer,
1072        JavaWarRecognizer,
1073        JavaWarWebXmlRecognizer,
1074        JBossSarRecognizer,
1075        JBossServiceXmlRecognizer,
1076        MeteorPackageRecognizer,
1077        MozillaXpiRecognizer,
1078        NsisRecognizer,
1079        SharArchiveRecognizer,
1080        SquashfsRecognizer,
1081    ],
1082}
1083
1084#[cfg(test)]
1085mod panic_isolation_tests {
1086    use super::*;
1087    use crate::models::DiagnosticSeverity;
1088
1089    #[test]
1090    fn capture_parser_diagnostics_turns_panics_into_scan_errors() {
1091        let path = Path::new("fixtures/panic-package.json");
1092        let result = capture_parser_diagnostics(
1093            || -> Vec<PackageData> { panic!("panic boom") },
1094            "PanicParser",
1095            path,
1096            None,
1097        );
1098
1099        assert!(result.packages.is_empty());
1100        assert_eq!(result.scan_diagnostics.len(), 1);
1101        assert_eq!(
1102            result.scan_diagnostics[0].severity,
1103            DiagnosticSeverity::Error
1104        );
1105        assert!(result.scan_diagnostics[0].message.contains("PanicParser"));
1106        assert!(
1107            result.scan_diagnostics[0]
1108                .message
1109                .contains("fixtures/panic-package.json")
1110        );
1111        assert!(result.scan_diagnostics[0].message.contains("panic boom"));
1112    }
1113
1114    #[test]
1115    fn capture_parser_diagnostics_recovers_after_panic() {
1116        let panic_path = Path::new("fixtures/panic-package.json");
1117        let _ = capture_parser_diagnostics(
1118            || -> Vec<PackageData> { panic!("panic boom") },
1119            "PanicParser",
1120            panic_path,
1121            None,
1122        );
1123
1124        let ok_path = Path::new("fixtures/recovered-package.json");
1125        let result = capture_parser_diagnostics(
1126            || {
1127                crate::parser_warn!("recoverable parser warning");
1128                vec![PackageData {
1129                    package_type: Some(PackageType::Npm),
1130                    ..Default::default()
1131                }]
1132            },
1133            "RecoveringParser",
1134            ok_path,
1135            None,
1136        );
1137
1138        assert_eq!(result.packages.len(), 1);
1139        assert_eq!(result.scan_diagnostics.len(), 1);
1140        assert_eq!(
1141            result.scan_diagnostics[0].message,
1142            "recoverable parser warning"
1143        );
1144        assert_eq!(
1145            result.scan_diagnostics[0].severity,
1146            DiagnosticSeverity::Warning
1147        );
1148    }
1149}