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