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::CappedIterExt;
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            .capped("packages emitted by parser")
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
441/// Make a license-detection engine the active one for parser-level declared-license
442/// normalization within `f`. Used to share the scan's already-built engine with the
443/// assembly phase so cross-file resolution does not rebuild a redundant engine.
444pub(crate) fn with_parser_license_engine<T>(
445    engine: Option<Arc<LicenseDetectionEngine>>,
446    f: impl FnOnce() -> T,
447) -> T {
448    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
449        stack.borrow_mut().push(engine);
450    });
451    let result = f();
452    PARSER_LICENSE_ENGINE_STACK.with(|stack| {
453        stack.borrow_mut().pop();
454    });
455    result
456}
457
458pub(crate) fn record_parser_diagnostic(message: String, severity: DiagnosticSeverity) -> bool {
459    PARSER_DIAGNOSTIC_STACK.with(|stack| {
460        let mut stack = stack.borrow_mut();
461        let Some(active) = stack.last_mut() else {
462            return false;
463        };
464        active.push(ScanDiagnostic { severity, message });
465        true
466    })
467}
468
469#[macro_export]
470macro_rules! parser_warn {
471    ($($arg:tt)*) => {{
472        let message = format!($($arg)*);
473        if !$crate::parsers::record_parser_diagnostic(
474            message.clone(),
475            $crate::models::DiagnosticSeverity::Warning,
476        ) {
477            log::warn!("{message}");
478        }
479    }};
480}
481
482/// Package parser trait for extracting metadata from package manifest files.
483///
484/// Each parser implementation handles a specific package manager/ecosystem
485/// (npm, Maven, Python, Cargo, etc.) and extracts standardized metadata into
486/// `PackageData` structures compatible with ScanCode Toolkit JSON output format.
487///
488/// # Implementation Guide
489///
490/// Implementors must provide:
491/// - `PACKAGE_TYPE`: Package URL (purl) type identifier (e.g., "npm", "pypi", "maven")
492/// - `is_match()`: Returns true if the given file path matches this parser's expected format
493/// - `extract_packages()`: Parses the file and returns all extracted package metadata
494///
495/// # Error Handling
496///
497/// Parsers should handle errors gracefully by returning default/empty `PackageData`
498/// and logging warnings with [`crate::parser_warn!`] rather than panicking. Scanner
499/// dispatch captures those warnings and attaches them to `FileInfo.scan_diagnostics` so
500/// CI output and serialized scan results stay aligned.
501/// This allows the scan to continue processing other files even when individual
502/// files fail to parse.
503///
504/// # Example
505///
506/// ```no_run
507/// use provenant::models::{PackageData, PackageType};
508/// use provenant::parsers::PackageParser;
509/// use std::path::Path;
510///
511/// pub struct MyParser;
512///
513/// impl PackageParser for MyParser {
514///     const PACKAGE_TYPE: PackageType = PackageType::Npm;
515///
516///     fn is_match(path: &Path) -> bool {
517///         path.file_name().is_some_and(|name| name == "package.json")
518///     }
519///
520///     fn extract_packages(path: &Path) -> Vec<PackageData> {
521///         vec![PackageData::default()]
522///     }
523/// }
524/// ```
525pub trait PackageParser {
526    /// Package URL type identifier for this parser (e.g., PackageType::Npm, PackageType::Pypi).
527    const PACKAGE_TYPE: PackageType;
528
529    /// Extracts all packages from the given file path.
530    ///
531    /// Returns a vector of `PackageData` structures containing all extracted metadata
532    /// including name, version, dependencies, licenses, etc. Most parsers return a
533    /// single-element vector, but some (e.g., Bazel BUILD, Buck BUCK, Debian control)
534    /// can contain multiple packages in a single file.
535    ///
536    /// On parse errors, returns a vector with a default `PackageData` with minimal or
537    /// no fields populated.
538    fn extract_packages(path: &Path) -> Vec<PackageData>;
539
540    /// Checks if the given file path matches this parser's expected format.
541    ///
542    /// Returns true if the file should be handled by this parser based on filename,
543    /// extension, or path patterns. Used by the scanner to route files to appropriate parsers.
544    fn is_match(path: &Path) -> bool;
545
546    /// Returns the first package from [`extract_packages()`](Self::extract_packages),
547    /// or a default [`PackageData`] if the file contains no packages.
548    fn extract_first_package(path: &Path) -> PackageData {
549        Self::extract_packages(path)
550            .into_iter()
551            .map(|mut package| {
552                populate_declared_license_and_holder(&mut package);
553                finalize_package_declared_license_references(&mut package);
554                package
555            })
556            .next()
557            .unwrap_or_default()
558    }
559
560    /// Returns documentation metadata for the file-format surfaces this parser handles.
561    ///
562    /// Used to auto-generate `docs/SUPPORTED_FORMATS.md`. Parsers that share a
563    /// documentation entry with another parser (e.g., AlpineApkParser and
564    /// AlpineInstalledParser) should return the entry from only one of them
565    /// and use the default empty implementation in the other.
566    fn metadata() -> Vec<metadata::ParserMetadata> {
567        Vec::new()
568    }
569}
570
571pub fn try_parse_rpm_archive_with_license_engine(
572    path: &Path,
573    license_engine: Option<Arc<LicenseDetectionEngine>>,
574) -> Option<ParsePackagesResult> {
575    if !self::rpm_parser::path_looks_like_rpm_archive(path) {
576        return None;
577    }
578
579    if <RpmParser as PackageParser>::is_match(path) {
580        return Some(capture_parser_diagnostics(
581            || self::rpm_parser::extract_rpm_packages(path),
582            stringify!(RpmParser),
583            path,
584            license_engine,
585        ));
586    }
587
588    None
589}
590
591pub fn try_parse_rpm_archive(path: &Path) -> Option<ParsePackagesResult> {
592    try_parse_rpm_archive_with_license_engine(path, None)
593}
594
595#[cfg(feature = "golden-tests")]
596pub fn try_parse_compiled_bytes(bytes: &[u8]) -> Option<ParsePackagesResult> {
597    self::compiled_binary::try_parse_compiled_bytes(bytes)
598}
599
600#[cfg(feature = "golden-tests")]
601pub fn try_parse_windows_executable_bytes(
602    path: &Path,
603    bytes: &[u8],
604) -> Option<ParsePackagesResult> {
605    self::windows_executable::try_parse_windows_executable_bytes(path, bytes)
606}
607
608pub fn path_looks_like_rpm_archive(path: &Path) -> bool {
609    self::rpm_parser::path_looks_like_rpm_archive(path)
610}
611
612/// Collects all registered parser and detection-surface metadata.
613///
614/// Used by the `generate-supported-formats` xtask to auto-generate
615/// `docs/SUPPORTED_FORMATS.md`.
616pub fn all_metadata() -> Vec<metadata::ParserMetadata> {
617    let mut entries = collect_parser_metadata();
618    entries.extend_from_slice(self::compiled_binary::COMPILED_BINARY_METADATA);
619    entries.extend_from_slice(self::windows_executable::WINDOWS_EXE_METADATA);
620    entries.extend_from_slice(self::misc::RECOGNIZER_METADATA);
621    entries.extend_from_slice(crate::utils::font::FONT_METADATA);
622    entries
623}
624
625pub use self::about::AboutFileParser;
626pub use self::alpine::{AlpineApkParser, AlpineApkbuildParser, AlpineInstalledParser};
627pub use self::android::{
628    AndroidAabParser, AndroidApkParser, AndroidManifestParser, AndroidSoongMetadataParser,
629};
630#[cfg(feature = "golden-tests")]
631pub use self::android::{
632    ProtoItem, ProtoPrimitive, ProtoRawStringValue, ProtoSourcePosition, ProtoStringValue,
633    ProtoXmlAttribute, ProtoXmlElement, ProtoXmlNamespace, ProtoXmlNode, proto_item,
634    proto_primitive, proto_xml_node,
635};
636pub use self::arch::{ArchPkginfoParser, ArchSrcinfoParser};
637pub use self::autotools::AutotoolsConfigureParser;
638pub use self::bazel::{BazelBuildParser, BazelModuleParser};
639pub use self::bitbake::BitbakeRecipeParser;
640pub use self::bower::BowerJsonParser;
641pub use self::buck::{BuckBuildParser, BuckMetadataBzlParser};
642pub use self::bun_lock::BunLockParser;
643pub use self::bun_lockb::BunLockbParser;
644pub use self::cargo::CargoParser;
645#[cfg_attr(not(test), allow(unused_imports))]
646pub use self::cargo_lock::CargoLockParser;
647pub use self::carthage::{CarthageCartfileParser, CarthageCartfileResolvedParser};
648pub use self::chef::{ChefMetadataJsonParser, ChefMetadataRbParser};
649pub use self::citation::CitationCffParser;
650pub use self::clojure::{ClojureDepsEdnParser, ClojureProjectCljParser};
651pub use self::composer::{ComposerJsonParser, ComposerLockParser};
652pub use self::conan::{ConanFilePyParser, ConanLockParser, ConanfileTxtParser};
653pub use self::conan_data::ConanDataParser;
654pub use self::conda::{CondaEnvironmentYmlParser, CondaMetaYamlParser};
655pub use self::conda_meta_json::CondaMetaJsonParser;
656pub use self::cpan::{CpanManifestParser, CpanMetaJsonParser, CpanMetaYmlParser};
657pub use self::cpan_dist_ini::CpanDistIniParser;
658pub use self::cpan_makefile_pl::CpanMakefilePlParser;
659pub use self::cran::CranParser;
660pub use self::dart::{PubspecLockParser, PubspecYamlParser};
661pub use self::debian::{
662    DebianControlInExtractedDebParser, DebianControlParser, DebianCopyrightParser, DebianDebParser,
663    DebianDebianTarParser, DebianDistrolessInstalledParser, DebianDscParser,
664    DebianInstalledListParser, DebianInstalledMd5sumsParser, DebianInstalledParser,
665    DebianMd5sumInPackageParser, DebianOrigTarParser,
666};
667pub use self::deno::DenoParser;
668pub use self::deno_lock::DenoLockParser;
669pub use self::docker::DockerfileParser;
670pub use self::erlang_otp::{ErlangAppSrcParser, RebarConfigParser, RebarLockParser};
671pub use self::freebsd::FreebsdCompactManifestParser;
672pub use self::gitmodules::GitmodulesParser;
673pub use self::go::{GoModParser, GoSumParser, GoWorkParser, GodepsParser};
674pub use self::go_mod_graph::GoModGraphParser;
675pub use self::gradle::GradleParser;
676pub use self::gradle_lock::GradleLockfileParser;
677pub use self::gradle_module::GradleModuleParser;
678pub use self::hackage::{HackageCabalParser, HackageCabalProjectParser, HackageStackYamlParser};
679pub use self::haxe::HaxeParser;
680pub use self::helm::{HelmChartLockParser, HelmChartYamlParser};
681pub use self::hex_lock::HexLockParser;
682pub use self::huggingface::{
683    HuggingfaceConfigParser, HuggingfaceModelCardParser, HuggingfaceModelIndexParser,
684};
685pub use self::ivy::IvyXmlParser;
686pub use self::julia::{JuliaManifestTomlParser, JuliaProjectTomlParser};
687pub use self::maven::MavenParser;
688pub use self::meson::MesonParser;
689pub use self::microsoft_update_manifest::MicrosoftUpdateManifestParser;
690pub use self::misc::{
691    AndroidLibraryRecognizer, AppleDmgRecognizer, Axis2MarRecognizer, Axis2ModuleXmlRecognizer,
692    CabArchiveRecognizer, ChromeCrxRecognizer, InstallShieldRecognizer, IosIpaRecognizer,
693    IsoImageRecognizer, JBossSarRecognizer, JBossServiceXmlRecognizer, JavaEarAppXmlRecognizer,
694    JavaEarRecognizer, JavaJarRecognizer, JavaWarRecognizer, JavaWarWebXmlRecognizer,
695    MeteorPackageRecognizer, MozillaXpiRecognizer, NsisRecognizer, SharArchiveRecognizer,
696    SquashfsRecognizer,
697};
698pub use self::nix::{NixDefaultParser, NixFlakeLockParser, NixFlakeParser};
699pub use self::npm::NpmParser;
700pub use self::npm_lock::NpmLockParser;
701pub use self::npm_workspace::NpmWorkspaceParser;
702pub use self::nuget::{
703    CentralPackageManagementPropsParser, DirectoryBuildPropsParser, DotNetDepsJsonParser,
704    NupkgParser, NuspecParser, PackageReferenceProjectParser, PackagesConfigParser,
705    PackagesLockParser, ProjectJsonParser, ProjectLockJsonParser,
706};
707pub use self::oci::OciImageLayoutParser;
708pub use self::opam::OpamParser;
709pub use self::os_release::OsReleaseParser;
710pub use self::pip_inspect_deplock::PipInspectDeplockParser;
711pub use self::pipfile_lock::PipfileLockParser;
712pub use self::pixi::{PixiLockParser, PixiTomlParser};
713pub use self::pnpm_lock::PnpmLockParser;
714pub use self::podfile::PodfileParser;
715pub use self::podfile_lock::PodfileLockParser;
716pub use self::podspec::PodspecParser;
717pub use self::podspec_json::PodspecJsonParser;
718pub use self::poetry_lock::PoetryLockParser;
719pub use self::publiccode::PubliccodeParser;
720pub use self::pylock_toml::PylockTomlParser;
721pub use self::python::PythonParser;
722pub use self::readme::ReadmeParser;
723pub use self::requirements_txt::RequirementsTxtParser;
724#[cfg(feature = "rpm-sqlite")]
725pub use self::rpm_db::RpmSqliteDatabaseParser;
726pub use self::rpm_db::{RpmBdbDatabaseParser, RpmNdbDatabaseParser};
727pub use self::rpm_license_files::RpmLicenseFilesParser;
728pub use self::rpm_mariner_manifest::RpmMarinerManifestParser;
729pub use self::rpm_parser::RpmParser;
730pub use self::rpm_specfile::RpmSpecfileParser;
731pub use self::rpm_yumdb::RpmYumdbParser;
732pub use self::ruby::{
733    GemArchiveParser, GemMetadataExtractedParser, GemfileLockParser, GemfileParser, GemspecParser,
734};
735pub use self::sbt::SbtParser;
736pub use self::swift_manifest_json::SwiftManifestJsonParser;
737pub use self::swift_resolved::SwiftPackageResolvedParser;
738pub use self::swift_show_dependencies::SwiftShowDependenciesParser;
739pub use self::uv_lock::UvLockParser;
740pub use self::vcpkg::{VcpkgLockParser, VcpkgManifestParser};
741pub use self::yarn_lock::YarnLockParser;
742pub use self::yarn_pnp::YarnPnpParser;
743
744/// Registers all parsers and recognizers, generating dispatch functions.
745///
746/// Parsers are tried first, then recognizers. This ordering is important because
747/// recognizers match broadly by file extension (e.g., `.jar`) and would shadow
748/// more specific parsers if checked first.
749macro_rules! register_package_handlers {
750    (
751        parsers: [$($(#[$parser_meta:meta])* $parser:ty),* $(,)?],
752        recognizers: [$($recognizer:ty),* $(,)?] $(,)?
753    ) => {
754        pub fn try_parse_file_with_license_engine(
755            path: &Path,
756            license_engine: Option<Arc<LicenseDetectionEngine>>,
757        ) -> Option<ParsePackagesResult> {
758            $(
759                $(#[$parser_meta])*
760                if <$parser>::is_match(path) {
761                    return Some(capture_parser_diagnostics(
762                        || <$parser>::extract_packages(path),
763                        stringify!($parser),
764                        path,
765                        license_engine.clone(),
766                    ));
767                }
768            )*
769            $(
770                if <$recognizer>::is_match(path) {
771                    return Some(capture_parser_diagnostics(
772                        || <$recognizer>::extract_packages(path),
773                        stringify!($recognizer),
774                        path,
775                        license_engine.clone(),
776                    ));
777                }
778            )*
779            None
780        }
781
782        pub fn try_parse_file(path: &Path) -> Option<ParsePackagesResult> {
783            try_parse_file_with_license_engine(path, None)
784        }
785
786        // Used by the parser-golden maintenance tool in `xtask`.
787        // Scanner runtime dispatch goes through `try_parse_file()`.
788        #[allow(dead_code)]
789        pub fn parse_by_type_name(type_name: &str, path: &Path) -> Option<PackageData> {
790            match type_name {
791                $(
792                    $(#[$parser_meta])*
793                    stringify!($parser) => Some(<$parser>::extract_first_package(path)),
794                )*
795                $(
796                    stringify!($recognizer) => Some(<$recognizer>::extract_first_package(path)),
797                )*
798                _ => None
799            }
800        }
801
802        // Used by the parser-golden maintenance tool in `xtask` and by
803        // `tests/scanner_integration.rs` to verify parser registration.
804        #[allow(dead_code)]
805        pub fn list_parser_types() -> Vec<&'static str> {
806            vec![
807                $(
808                    $(#[$parser_meta])*
809                    stringify!($parser),
810                )*
811                $(
812                    stringify!($recognizer),
813                )*
814            ]
815        }
816
817        /// Collects documentation metadata from all registered parsers and recognizers.
818        pub fn collect_parser_metadata() -> Vec<metadata::ParserMetadata> {
819            let mut entries = Vec::new();
820            $(
821                $(#[$parser_meta])*
822                entries.extend(<$parser>::metadata());
823            )*
824            $(
825                entries.extend(<$recognizer>::metadata());
826            )*
827            entries
828        }
829    };
830}
831
832#[cfg(test)]
833mod tests {
834    use std::collections::HashMap;
835
836    use super::{active_parser_license_engine, capture_parser_diagnostics};
837    use crate::license_detection::LicenseDetectionEngine;
838    use crate::models::PackageData;
839    use crate::parsers::license_normalization::{
840        clear_last_parser_license_engine_ptr, last_parser_license_engine_ptr,
841    };
842    use std::path::Path;
843    use std::sync::Arc;
844
845    #[test]
846    fn test_capture_parser_diagnostics_exposes_active_license_engine() {
847        let engine =
848            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
849
850        let result = capture_parser_diagnostics(
851            || {
852                assert!(active_parser_license_engine().is_some());
853                vec![PackageData::default()]
854            },
855            "TestParser",
856            Path::new("testdata/package.json"),
857            Some(engine),
858        );
859
860        assert_eq!(result.packages.len(), 1);
861        assert!(active_parser_license_engine().is_none());
862    }
863
864    #[test]
865    fn test_capture_parser_diagnostics_keeps_active_license_engine_for_finalization() {
866        let engine =
867            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
868        clear_last_parser_license_engine_ptr();
869
870        let result = capture_parser_diagnostics(
871            || {
872                vec![PackageData {
873                    declared_license_expression: Some("mit".to_string()),
874                    declared_license_expression_spdx: Some("MIT".to_string()),
875                    extracted_license_statement: Some("MIT".to_string()),
876                    extra_data: Some(HashMap::from([(
877                        "license_file".to_string(),
878                        serde_json::Value::String("LICENSE".to_string()),
879                    )])),
880                    ..Default::default()
881                }]
882            },
883            "TestParser",
884            Path::new("testdata/package.json"),
885            Some(Arc::clone(&engine)),
886        );
887
888        assert_eq!(result.packages.len(), 1);
889        assert_eq!(
890            last_parser_license_engine_ptr(),
891            Some(Arc::as_ptr(&engine) as usize)
892        );
893        assert_eq!(
894            result.packages[0].license_detections[0].matches[0]
895                .referenced_filenames
896                .as_ref(),
897            Some(&vec!["LICENSE".to_string()])
898        );
899        assert!(active_parser_license_engine().is_none());
900    }
901}
902
903register_package_handlers! {
904    parsers: [
905        AboutFileParser,
906        AndroidAabParser,
907        AndroidApkParser,
908        AndroidManifestParser,
909        AndroidSoongMetadataParser,
910        AlpineApkParser,
911        AlpineApkbuildParser,
912        AlpineInstalledParser,
913        ArchPkginfoParser,
914        ArchSrcinfoParser,
915        AutotoolsConfigureParser,
916        BazelBuildParser,
917        BazelModuleParser,
918        BitbakeRecipeParser,
919        BowerJsonParser,
920        BunLockParser,
921        BunLockbParser,
922        BuckBuildParser,
923        BuckMetadataBzlParser,
924        CargoLockParser,
925        CargoParser,
926        CarthageCartfileParser,
927        CarthageCartfileResolvedParser,
928        ChefMetadataJsonParser,
929        ChefMetadataRbParser,
930        CitationCffParser,
931        ClojureDepsEdnParser,
932        ClojureProjectCljParser,
933        ComposerJsonParser,
934        ComposerLockParser,
935        ConanDataParser,
936        ConanFilePyParser,
937        ConanfileTxtParser,
938        ConanLockParser,
939        CondaEnvironmentYmlParser,
940        CondaMetaJsonParser,
941        CondaMetaYamlParser,
942        CpanDistIniParser,
943        CpanMakefilePlParser,
944        CpanManifestParser,
945        CpanMetaJsonParser,
946        CpanMetaYmlParser,
947        CranParser,
948        DebianControlInExtractedDebParser,
949        DebianControlParser,
950        DebianCopyrightParser,
951        DebianDebianTarParser,
952        DebianDebParser,
953        DebianDistrolessInstalledParser,
954        DebianDscParser,
955        DebianInstalledListParser,
956        DebianInstalledMd5sumsParser,
957        DebianInstalledParser,
958        DebianMd5sumInPackageParser,
959        DebianOrigTarParser,
960        DenoParser,
961        DenoLockParser,
962        DockerfileParser,
963        ErlangAppSrcParser,
964        RebarConfigParser,
965        RebarLockParser,
966        FreebsdCompactManifestParser,
967        GemArchiveParser,
968        GemfileLockParser,
969        GemfileParser,
970        GemMetadataExtractedParser,
971        GemspecParser,
972        GitmodulesParser,
973        GodepsParser,
974        GoModParser,
975        GoModGraphParser,
976        GoSumParser,
977        GoWorkParser,
978        GradleLockfileParser,
979        GradleParser,
980        GradleModuleParser,
981        HackageCabalParser,
982        HackageCabalProjectParser,
983        HackageStackYamlParser,
984        HelmChartYamlParser,
985        HelmChartLockParser,
986        HaxeParser,
987        HexLockParser,
988        HuggingfaceModelCardParser,
989        HuggingfaceConfigParser,
990        HuggingfaceModelIndexParser,
991        IvyXmlParser,
992        JuliaManifestTomlParser,
993        JuliaProjectTomlParser,
994        MavenParser,
995        MesonParser,
996        MicrosoftUpdateManifestParser,
997        NixDefaultParser,
998        NixFlakeLockParser,
999        NixFlakeParser,
1000        NpmLockParser,
1001        NpmParser,
1002        NpmWorkspaceParser,
1003        DotNetDepsJsonParser,
1004        CentralPackageManagementPropsParser,
1005        DirectoryBuildPropsParser,
1006        NupkgParser,
1007        NuspecParser,
1008        PackageReferenceProjectParser,
1009        OciImageLayoutParser,
1010        OpamParser,
1011        OsReleaseParser,
1012        PackagesConfigParser,
1013        PackagesLockParser,
1014        ProjectJsonParser,
1015        ProjectLockJsonParser,
1016        PipfileLockParser,
1017        PipInspectDeplockParser,
1018        PixiTomlParser,
1019        PixiLockParser,
1020        PnpmLockParser,
1021        PodfileLockParser,
1022        PodfileParser,
1023        PodspecJsonParser,
1024        PodspecParser,
1025        PoetryLockParser,
1026        PubliccodeParser,
1027        PylockTomlParser,
1028        PubspecLockParser,
1029        PubspecYamlParser,
1030        PythonParser,
1031        UvLockParser,
1032        VcpkgLockParser,
1033        VcpkgManifestParser,
1034        ReadmeParser,
1035        RequirementsTxtParser,
1036        RpmBdbDatabaseParser,
1037        RpmLicenseFilesParser,
1038        RpmMarinerManifestParser,
1039        RpmNdbDatabaseParser,
1040        RpmParser,
1041        RpmSpecfileParser,
1042        #[cfg(feature = "rpm-sqlite")]
1043        RpmSqliteDatabaseParser,
1044        RpmYumdbParser,
1045        SbtParser,
1046        SwiftManifestJsonParser,
1047        SwiftPackageResolvedParser,
1048        SwiftShowDependenciesParser,
1049        YarnLockParser,
1050        YarnPnpParser,
1051    ],
1052    recognizers: [
1053        AndroidLibraryRecognizer,
1054        AppleDmgRecognizer,
1055        Axis2MarRecognizer,
1056        Axis2ModuleXmlRecognizer,
1057        CabArchiveRecognizer,
1058        ChromeCrxRecognizer,
1059        InstallShieldRecognizer,
1060        IosIpaRecognizer,
1061        IsoImageRecognizer,
1062        JavaEarAppXmlRecognizer,
1063        JavaEarRecognizer,
1064        JavaJarRecognizer,
1065        JavaWarRecognizer,
1066        JavaWarWebXmlRecognizer,
1067        JBossSarRecognizer,
1068        JBossServiceXmlRecognizer,
1069        MeteorPackageRecognizer,
1070        MozillaXpiRecognizer,
1071        NsisRecognizer,
1072        SharArchiveRecognizer,
1073        SquashfsRecognizer,
1074    ],
1075}
1076
1077#[cfg(test)]
1078mod panic_isolation_tests {
1079    use super::*;
1080    use crate::models::DiagnosticSeverity;
1081
1082    #[test]
1083    fn capture_parser_diagnostics_turns_panics_into_scan_errors() {
1084        let path = Path::new("fixtures/panic-package.json");
1085        let result = capture_parser_diagnostics(
1086            || -> Vec<PackageData> { panic!("panic boom") },
1087            "PanicParser",
1088            path,
1089            None,
1090        );
1091
1092        assert!(result.packages.is_empty());
1093        assert_eq!(result.scan_diagnostics.len(), 1);
1094        assert_eq!(
1095            result.scan_diagnostics[0].severity,
1096            DiagnosticSeverity::Error
1097        );
1098        assert!(result.scan_diagnostics[0].message.contains("PanicParser"));
1099        assert!(
1100            result.scan_diagnostics[0]
1101                .message
1102                .contains("fixtures/panic-package.json")
1103        );
1104        assert!(result.scan_diagnostics[0].message.contains("panic boom"));
1105    }
1106
1107    #[test]
1108    fn capture_parser_diagnostics_recovers_after_panic() {
1109        let panic_path = Path::new("fixtures/panic-package.json");
1110        let _ = capture_parser_diagnostics(
1111            || -> Vec<PackageData> { panic!("panic boom") },
1112            "PanicParser",
1113            panic_path,
1114            None,
1115        );
1116
1117        let ok_path = Path::new("fixtures/recovered-package.json");
1118        let result = capture_parser_diagnostics(
1119            || {
1120                crate::parser_warn!("recoverable parser warning");
1121                vec![PackageData {
1122                    package_type: Some(PackageType::Npm),
1123                    ..Default::default()
1124                }]
1125            },
1126            "RecoveringParser",
1127            ok_path,
1128            None,
1129        );
1130
1131        assert_eq!(result.packages.len(), 1);
1132        assert_eq!(result.scan_diagnostics.len(), 1);
1133        assert_eq!(
1134            result.scan_diagnostics[0].message,
1135            "recoverable parser warning"
1136        );
1137        assert_eq!(
1138            result.scan_diagnostics[0].severity,
1139            DiagnosticSeverity::Warning
1140        );
1141    }
1142}