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