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