Skip to main content

provenant/parsers/
mod.rs

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