provenant/parsers/rpm_license_files.rs
1// SPDX-FileCopyrightText: nexB Inc. and others
2// ScanCode is a trademark of nexB Inc.
3// SPDX-FileCopyrightText: Provenant contributors
4// SPDX-License-Identifier: Apache-2.0
5// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
6
7//! Parser for RPM license files in /usr/share/licenses/ directories.
8//!
9//! Identifies packages from their license files installed in the standard
10//! /usr/share/licenses/ location, primarily used in Mariner distroless containers.
11
12use crate::models::{DatasourceId, PackageType};
13use std::path::Path;
14
15use crate::models::PackageData;
16use crate::parsers::PackageParser;
17use crate::parsers::utils::truncate_field;
18
19const PACKAGE_TYPE: PackageType = PackageType::Rpm;
20
21/// Parser for RPM license files in /usr/share/licenses/ directories.
22///
23/// Identifies packages from their license files installed in the standard
24/// /usr/share/licenses/ location, primarily used in Mariner distroless containers.
25///
26/// # Supported Formats
27/// - `/usr/share/licenses/*/COPYING*` - COPYING license files
28/// - `/usr/share/licenses/*/LICENSE*` - LICENSE files
29///
30/// # Key Features
31/// - Extracts package name from directory path
32/// - Supports Mariner distroless container convention
33/// - Package URL generation with mariner namespace
34///
35/// # Implementation Notes
36/// - Package name is extracted from the directory between `licenses/` and the filename
37/// - For example: `/usr/share/licenses/openssl/LICENSE` → package name is "openssl"
38/// - Does NOT perform license detection (that's handled by the license scanner)
39/// - datasource_id: "rpm_package_licenses"
40/// - namespace: "mariner"
41pub struct RpmLicenseFilesParser;
42
43impl PackageParser for RpmLicenseFilesParser {
44 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
45
46 fn is_match(path: &Path) -> bool {
47 let path_str = path.to_string_lossy();
48
49 // Check if path contains usr/share/licenses/
50 if !path_str.contains("usr/share/licenses/") {
51 return false;
52 }
53
54 // Get the filename
55 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
56 // Match files starting with COPYING or LICENSE (case-sensitive)
57 filename.starts_with("COPYING") || filename.starts_with("LICENSE")
58 } else {
59 false
60 }
61 }
62
63 fn extract_packages(path: &Path) -> Vec<PackageData> {
64 // Extract package name from path
65 let path_str = path.to_string_lossy();
66
67 // Split by usr/share/licenses/ and get the next path component
68 let name = if let Some(after_licenses) = path_str.split("usr/share/licenses/").nth(1) {
69 // Get the first path component after licenses/ (the package name)
70 after_licenses
71 .split('/')
72 .next()
73 .map(|s| truncate_field(s.to_string()))
74 } else {
75 None
76 };
77
78 // Build package data
79 let mut pkg = PackageData {
80 package_type: Some(PACKAGE_TYPE),
81 datasource_id: Some(DatasourceId::RpmPackageLicenses),
82 namespace: Some("mariner".to_string()),
83 name: name.clone(),
84 ..Default::default()
85 };
86
87 // Build PURL if we have a name
88 if let Some(ref package_name) = name {
89 use packageurl::PackageUrl;
90 if let Ok(mut purl) = PackageUrl::new(PACKAGE_TYPE.as_str(), package_name)
91 && purl.with_namespace("mariner").is_ok()
92 {
93 pkg.purl = Some(truncate_field(purl.to_string()));
94 }
95 }
96
97 vec![pkg]
98 }
99
100 fn metadata() -> Vec<super::metadata::ParserMetadata> {
101 vec![super::metadata::ParserMetadata {
102 description: "RPM mariner distroless package license files",
103 file_patterns: &[
104 "*usr/share/licenses/*/COPYING*",
105 "*usr/share/licenses/*/LICENSE*",
106 ],
107 package_type: "rpm",
108 primary_language: "",
109 documentation_url: Some("https://github.com/microsoft/marinara/"),
110 }]
111 }
112}