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;
721/// Shared so assembly builds NuGet PURLs the same way the project parsers do;
722/// the value is a join key between the two.
723pub(crate) use self::nuget::build_nuget_purl;
724pub use self::nuget::{
725    CentralPackageManagementPropsParser, DirectoryBuildPropsParser, DotNetDepsJsonParser,
726    NupkgParser, NuspecParser, PackageReferenceProjectParser, PackagesConfigParser,
727    PackagesLockParser, ProjectJsonParser, ProjectLockJsonParser,
728};
729pub use self::oci::OciImageLayoutParser;
730pub use self::opam::OpamParser;
731pub use self::os_release::OsReleaseParser;
732pub use self::pip_inspect_deplock::PipInspectDeplockParser;
733pub use self::pipfile_lock::PipfileLockParser;
734pub use self::pixi::{PixiLockParser, PixiTomlParser};
735pub use self::pnpm_lock::PnpmLockParser;
736pub use self::podfile::PodfileParser;
737pub use self::podfile_lock::PodfileLockParser;
738pub use self::podspec::PodspecParser;
739pub use self::podspec_json::PodspecJsonParser;
740pub use self::poetry_lock::PoetryLockParser;
741pub use self::publiccode::PubliccodeParser;
742pub use self::pylock_toml::PylockTomlParser;
743pub use self::python::PythonParser;
744pub use self::readme::ReadmeParser;
745pub use self::requirements_txt::RequirementsTxtParser;
746#[cfg(feature = "rpm-sqlite")]
747pub use self::rpm_db::RpmSqliteDatabaseParser;
748pub use self::rpm_db::{RpmBdbDatabaseParser, RpmNdbDatabaseParser};
749pub use self::rpm_license_files::RpmLicenseFilesParser;
750pub use self::rpm_mariner_manifest::RpmMarinerManifestParser;
751pub use self::rpm_parser::RpmParser;
752pub use self::rpm_specfile::RpmSpecfileParser;
753pub use self::rpm_yumdb::RpmYumdbParser;
754pub use self::ruby::{
755    GemArchiveParser, GemMetadataExtractedParser, GemfileLockParser, GemfileParser, GemspecParser,
756};
757pub use self::sbt::SbtParser;
758pub use self::swift_manifest_json::SwiftManifestJsonParser;
759pub use self::swift_resolved::SwiftPackageResolvedParser;
760pub use self::swift_show_dependencies::SwiftShowDependenciesParser;
761pub use self::uv_lock::UvLockParser;
762pub use self::vcpkg::{
763    VcpkgConfigurationParser, VcpkgControlParser, VcpkgLockParser, VcpkgManifestParser,
764};
765pub use self::vscode_extension::VscodeExtensionManifestParser;
766pub use self::yarn_lock::YarnLockParser;
767pub use self::yarn_pnp::YarnPnpParser;
768
769/// Registers all parsers and recognizers, generating dispatch functions.
770///
771/// Parsers are tried first, then recognizers. This ordering is important because
772/// recognizers match broadly by file extension (e.g., `.jar`) and would shadow
773/// more specific parsers if checked first.
774macro_rules! register_package_handlers {
775    (
776        parsers: [$($(#[$parser_meta:meta])* $parser:ty),* $(,)?],
777        recognizers: [$($recognizer:ty),* $(,)?] $(,)?
778    ) => {
779        pub fn try_parse_file_with_license_engine(
780            path: &Path,
781            license_engine: Option<Arc<LicenseDetectionEngine>>,
782        ) -> Option<ParsePackagesResult> {
783            $(
784                $(#[$parser_meta])*
785                if <$parser>::is_match(path) {
786                    return Some(capture_parser_diagnostics(
787                        || <$parser>::extract_packages(path),
788                        stringify!($parser),
789                        path,
790                        license_engine.clone(),
791                    ));
792                }
793            )*
794            $(
795                if <$recognizer>::is_match(path) {
796                    return Some(capture_parser_diagnostics(
797                        || <$recognizer>::extract_packages(path),
798                        stringify!($recognizer),
799                        path,
800                        license_engine.clone(),
801                    ));
802                }
803            )*
804            None
805        }
806
807        pub fn try_parse_file(path: &Path) -> Option<ParsePackagesResult> {
808            try_parse_file_with_license_engine(path, None)
809        }
810
811        // Used by the parser-golden maintenance tool in `xtask`.
812        // Scanner runtime dispatch goes through `try_parse_file()`.
813        #[allow(dead_code)]
814        pub fn parse_by_type_name(type_name: &str, path: &Path) -> Option<PackageData> {
815            match type_name {
816                $(
817                    $(#[$parser_meta])*
818                    stringify!($parser) => Some(<$parser>::extract_first_package(path)),
819                )*
820                $(
821                    stringify!($recognizer) => Some(<$recognizer>::extract_first_package(path)),
822                )*
823                _ => None
824            }
825        }
826
827        // Used by the parser-golden maintenance tool in `xtask` and by
828        // `tests/scanner_integration.rs` to verify parser registration.
829        #[allow(dead_code)]
830        pub fn list_parser_types() -> Vec<&'static str> {
831            vec![
832                $(
833                    $(#[$parser_meta])*
834                    stringify!($parser),
835                )*
836                $(
837                    stringify!($recognizer),
838                )*
839            ]
840        }
841
842        /// Collects documentation metadata from all registered parsers and recognizers.
843        pub fn collect_parser_metadata() -> Vec<metadata::ParserMetadata> {
844            let mut entries = Vec::new();
845            $(
846                $(#[$parser_meta])*
847                entries.extend(<$parser>::metadata());
848            )*
849            $(
850                entries.extend(<$recognizer>::metadata());
851            )*
852            entries
853        }
854    };
855}
856
857#[cfg(test)]
858mod tests {
859    use std::collections::HashMap;
860
861    use super::{active_parser_license_engine, capture_parser_diagnostics};
862    use crate::license_detection::LicenseDetectionEngine;
863    use crate::models::PackageData;
864    use crate::parsers::license_normalization::{
865        clear_last_parser_license_engine_ptr, last_parser_license_engine_ptr,
866    };
867    use std::path::Path;
868    use std::sync::Arc;
869
870    #[test]
871    fn test_capture_parser_diagnostics_exposes_active_license_engine() {
872        let engine =
873            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
874
875        let result = capture_parser_diagnostics(
876            || {
877                assert!(active_parser_license_engine().is_some());
878                vec![PackageData::default()]
879            },
880            "TestParser",
881            Path::new("testdata/package.json"),
882            Some(engine),
883        );
884
885        assert_eq!(result.packages.len(), 1);
886        assert!(active_parser_license_engine().is_none());
887    }
888
889    #[test]
890    fn test_capture_parser_diagnostics_keeps_active_license_engine_for_finalization() {
891        let engine =
892            Arc::new(LicenseDetectionEngine::from_embedded().expect("embedded engine should load"));
893        clear_last_parser_license_engine_ptr();
894
895        let result = capture_parser_diagnostics(
896            || {
897                vec![PackageData {
898                    declared_license_expression: Some("mit".to_string()),
899                    declared_license_expression_spdx: Some("MIT".to_string()),
900                    extracted_license_statement: Some("MIT".to_string()),
901                    extra_data: Some(HashMap::from([(
902                        "license_file".to_string(),
903                        serde_json::Value::String("LICENSE".to_string()),
904                    )])),
905                    ..Default::default()
906                }]
907            },
908            "TestParser",
909            Path::new("testdata/package.json"),
910            Some(Arc::clone(&engine)),
911        );
912
913        assert_eq!(result.packages.len(), 1);
914        assert_eq!(
915            last_parser_license_engine_ptr(),
916            Some(Arc::as_ptr(&engine) as usize)
917        );
918        assert_eq!(
919            result.packages[0].license_detections[0].matches[0]
920                .referenced_filenames
921                .as_ref(),
922            Some(&vec!["LICENSE".to_string()])
923        );
924        assert!(active_parser_license_engine().is_none());
925    }
926}
927
928register_package_handlers! {
929    parsers: [
930        AboutFileParser,
931        AndroidAabParser,
932        AndroidApkParser,
933        AndroidManifestParser,
934        AndroidSoongMetadataParser,
935        AlpineApkParser,
936        AlpineApkbuildParser,
937        AlpineInstalledParser,
938        ArchPkginfoParser,
939        ArchSrcinfoParser,
940        AutotoolsConfigureParser,
941        BazelBuildParser,
942        BazelModuleParser,
943        BitbakeRecipeParser,
944        BowerJsonParser,
945        BunLockParser,
946        BunLockbParser,
947        BuckBuildParser,
948        BuckMetadataBzlParser,
949        CargoLockParser,
950        CargoParser,
951        CarthageCartfileParser,
952        CarthageCartfileResolvedParser,
953        ChefMetadataJsonParser,
954        ChefMetadataRbParser,
955        CitationCffParser,
956        ClojureDepsEdnParser,
957        ClojureProjectCljParser,
958        ComposerJsonParser,
959        ComposerLockParser,
960        ConanDataParser,
961        ConanFilePyParser,
962        ConanfileTxtParser,
963        ConanLockParser,
964        CondaEnvironmentYmlParser,
965        CondaMetaJsonParser,
966        CondaMetaYamlParser,
967        CpanDistIniParser,
968        CpanMakefilePlParser,
969        CpanManifestParser,
970        CpanMetaJsonParser,
971        CpanMetaYmlParser,
972        CranParser,
973        DebianControlInExtractedDebParser,
974        DebianControlParser,
975        DebianCopyrightParser,
976        DebianDebianTarParser,
977        DebianDebParser,
978        DebianDistrolessInstalledParser,
979        DebianDscParser,
980        DebianInstalledListParser,
981        DebianInstalledMd5sumsParser,
982        DebianInstalledParser,
983        DebianMd5sumInPackageParser,
984        DebianOrigTarParser,
985        DenoParser,
986        DenoLockParser,
987        DockerfileParser,
988        ErlangAppSrcParser,
989        RebarConfigParser,
990        RebarLockParser,
991        FreebsdCompactManifestParser,
992        GemArchiveParser,
993        GemfileLockParser,
994        GemfileParser,
995        GemMetadataExtractedParser,
996        GemspecParser,
997        GitmodulesParser,
998        GodepsParser,
999        GoModParser,
1000        GoModGraphParser,
1001        GoSumParser,
1002        GoWorkParser,
1003        GradleLockfileParser,
1004        GradleParser,
1005        GradleModuleParser,
1006        GradleSettingsParser,
1007        HackageCabalParser,
1008        HackageCabalProjectParser,
1009        HackageStackYamlParser,
1010        HelmChartYamlParser,
1011        HelmChartLockParser,
1012        HaxeParser,
1013        HexLockParser,
1014        MixExsParser,
1015        HuggingfaceModelCardParser,
1016        HuggingfaceConfigParser,
1017        HuggingfaceModelIndexParser,
1018        IvyDependenciesPropertiesParser,
1019        IvyXmlParser,
1020        JuliaManifestTomlParser,
1021        JuliaProjectTomlParser,
1022        MavenParser,
1023        MesonParser,
1024        MicrosoftUpdateManifestParser,
1025        NixDefaultParser,
1026        NixFlakeLockParser,
1027        NixFlakeParser,
1028        NpmLockParser,
1029        NpmParser,
1030        NpmWorkspaceParser,
1031        DotNetDepsJsonParser,
1032        CentralPackageManagementPropsParser,
1033        DirectoryBuildPropsParser,
1034        NupkgParser,
1035        NuspecParser,
1036        PackageReferenceProjectParser,
1037        OciImageLayoutParser,
1038        OpamParser,
1039        OsReleaseParser,
1040        PackagesConfigParser,
1041        PackagesLockParser,
1042        ProjectJsonParser,
1043        ProjectLockJsonParser,
1044        PipfileLockParser,
1045        PipInspectDeplockParser,
1046        PixiTomlParser,
1047        PixiLockParser,
1048        PnpmLockParser,
1049        PodfileLockParser,
1050        PodfileParser,
1051        PodspecJsonParser,
1052        PodspecParser,
1053        PoetryLockParser,
1054        PubliccodeParser,
1055        PylockTomlParser,
1056        PubspecLockParser,
1057        PubspecYamlParser,
1058        PythonParser,
1059        UvLockParser,
1060        VcpkgConfigurationParser,
1061        VcpkgControlParser,
1062        VcpkgLockParser,
1063        VcpkgManifestParser,
1064        VscodeExtensionManifestParser,
1065        ReadmeParser,
1066        RequirementsTxtParser,
1067        RpmBdbDatabaseParser,
1068        RpmLicenseFilesParser,
1069        RpmMarinerManifestParser,
1070        RpmNdbDatabaseParser,
1071        RpmParser,
1072        RpmSpecfileParser,
1073        #[cfg(feature = "rpm-sqlite")]
1074        RpmSqliteDatabaseParser,
1075        RpmYumdbParser,
1076        SbtParser,
1077        SwiftManifestJsonParser,
1078        SwiftPackageResolvedParser,
1079        SwiftShowDependenciesParser,
1080        YarnLockParser,
1081        YarnPnpParser,
1082    ],
1083    recognizers: [
1084        AndroidLibraryRecognizer,
1085        AppleDmgRecognizer,
1086        Axis2MarRecognizer,
1087        Axis2ModuleXmlRecognizer,
1088        CabArchiveRecognizer,
1089        ChromeCrxRecognizer,
1090        InstallShieldRecognizer,
1091        IosIpaRecognizer,
1092        IsoImageRecognizer,
1093        JavaEarAppXmlRecognizer,
1094        JavaEarRecognizer,
1095        JavaJarRecognizer,
1096        JavaWarRecognizer,
1097        JavaWarWebXmlRecognizer,
1098        JBossSarRecognizer,
1099        JBossServiceXmlRecognizer,
1100        MeteorPackageRecognizer,
1101        MozillaXpiRecognizer,
1102        NsisRecognizer,
1103        SharArchiveRecognizer,
1104        SquashfsRecognizer,
1105    ],
1106}
1107
1108#[cfg(test)]
1109mod panic_isolation_tests {
1110    use super::*;
1111    use crate::models::DiagnosticSeverity;
1112
1113    #[test]
1114    fn capture_parser_diagnostics_turns_panics_into_scan_errors() {
1115        let path = Path::new("fixtures/panic-package.json");
1116        let result = capture_parser_diagnostics(
1117            || -> Vec<PackageData> { panic!("panic boom") },
1118            "PanicParser",
1119            path,
1120            None,
1121        );
1122
1123        assert!(result.packages.is_empty());
1124        assert_eq!(result.scan_diagnostics.len(), 1);
1125        assert_eq!(
1126            result.scan_diagnostics[0].severity,
1127            DiagnosticSeverity::Error
1128        );
1129        assert!(result.scan_diagnostics[0].message.contains("PanicParser"));
1130        assert!(
1131            result.scan_diagnostics[0]
1132                .message
1133                .contains("fixtures/panic-package.json")
1134        );
1135        assert!(result.scan_diagnostics[0].message.contains("panic boom"));
1136    }
1137
1138    #[test]
1139    fn capture_parser_diagnostics_recovers_after_panic() {
1140        let panic_path = Path::new("fixtures/panic-package.json");
1141        let _ = capture_parser_diagnostics(
1142            || -> Vec<PackageData> { panic!("panic boom") },
1143            "PanicParser",
1144            panic_path,
1145            None,
1146        );
1147
1148        let ok_path = Path::new("fixtures/recovered-package.json");
1149        let result = capture_parser_diagnostics(
1150            || {
1151                crate::parser_warn!("recoverable parser warning");
1152                vec![PackageData {
1153                    package_type: Some(PackageType::Npm),
1154                    ..Default::default()
1155                }]
1156            },
1157            "RecoveringParser",
1158            ok_path,
1159            None,
1160        );
1161
1162        assert_eq!(result.packages.len(), 1);
1163        assert_eq!(result.scan_diagnostics.len(), 1);
1164        assert_eq!(
1165            result.scan_diagnostics[0].message,
1166            "recoverable parser warning"
1167        );
1168        assert_eq!(
1169            result.scan_diagnostics[0].severity,
1170            DiagnosticSeverity::Warning
1171        );
1172    }
1173}