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