1use std::path::Path;
23
24use crate::parser_warn as warn;
25
26use crate::models::{DatasourceId, PackageData, PackageType};
27use crate::models::{Dependency, FileReference};
28use crate::parsers::utils::{MAX_ITERATION_COUNT, MAX_MANIFEST_SIZE, truncate_field};
29
30use super::PackageParser;
31use super::license_normalization::{
32 DeclaredLicenseMatchMetadata, build_declared_license_data, empty_declared_license_data,
33};
34use super::rpm_db_native::{InstalledRpmDbKind, InstalledRpmPackage, read_installed_rpm_packages};
35use super::rpm_parser::infer_rpm_namespace;
36use super::rpm_parser::infer_rpm_namespace_from_filename;
37use super::rpm_parser::normalize_rpm_declared_license;
38
39const PACKAGE_TYPE: PackageType = PackageType::Rpm;
40const RPM_BDB_PATH_SUFFIXES: &[&str] = &["var/lib/rpm/Packages", "usr/lib/sysimage/rpm/Packages"];
41const RPM_NDB_PATH_SUFFIXES: &[&str] = &[
42 "var/lib/rpm/Packages.db",
43 "usr/lib/sysimage/rpm/Packages.db",
44];
45const RPM_SQLITE_PATH_SUFFIXES: &[&str] = &[
46 "var/lib/rpm/rpmdb.sqlite",
47 "usr/lib/sysimage/rpm/rpmdb.sqlite",
48];
49
50#[derive(Debug)]
51struct RpmQueryPackage {
52 name: Option<String>,
53 epoch: Option<String>,
54 version: Option<String>,
55 release: Option<String>,
56 vendor: Option<String>,
57 distribution: Option<String>,
58 arch: Option<String>,
59 platform: Option<String>,
60 size: Option<u64>,
61 license: Option<String>,
62 source_rpm: Option<String>,
63 requires: Vec<String>,
64 file_names: Vec<Option<String>>,
65 dir_indexes: Vec<u32>,
66 base_names: Vec<Option<String>>,
67 dir_names: Vec<String>,
68}
69
70fn default_package_data(datasource_id: DatasourceId) -> PackageData {
71 PackageData {
72 package_type: Some(PACKAGE_TYPE),
73 datasource_id: Some(datasource_id),
74 ..Default::default()
75 }
76}
77
78pub struct RpmBdbDatabaseParser;
79
80impl PackageParser for RpmBdbDatabaseParser {
81 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
82
83 fn is_match(path: &Path) -> bool {
84 path_matches_any_suffix(path, RPM_BDB_PATH_SUFFIXES)
85 }
86
87 fn extract_packages(path: &Path) -> Vec<PackageData> {
88 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseBdb) {
89 Ok(pkgs) if !pkgs.is_empty() => pkgs,
90 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)],
91 Err(e) => {
92 warn!("Failed to parse RPM BDB database {:?}: {}", path, e);
93 vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)]
94 }
95 }
96 }
97}
98
99pub struct RpmNdbDatabaseParser;
100
101impl PackageParser for RpmNdbDatabaseParser {
102 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
103
104 fn is_match(path: &Path) -> bool {
105 path_matches_any_suffix(path, RPM_NDB_PATH_SUFFIXES)
106 }
107
108 fn extract_packages(path: &Path) -> Vec<PackageData> {
109 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseNdb) {
110 Ok(pkgs) if !pkgs.is_empty() => pkgs,
111 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)],
112 Err(e) => {
113 warn!("Failed to parse RPM NDB database {:?}: {}", path, e);
114 vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)]
115 }
116 }
117 }
118}
119
120#[cfg(feature = "rpm-sqlite")]
121pub struct RpmSqliteDatabaseParser;
122
123#[cfg(feature = "rpm-sqlite")]
124impl PackageParser for RpmSqliteDatabaseParser {
125 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
126
127 fn is_match(path: &Path) -> bool {
128 path_matches_any_suffix(path, RPM_SQLITE_PATH_SUFFIXES)
129 }
130
131 fn extract_packages(path: &Path) -> Vec<PackageData> {
132 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseSqlite) {
133 Ok(pkgs) if !pkgs.is_empty() => pkgs,
134 Ok(_) => vec![default_package_data(
135 DatasourceId::RpmInstalledDatabaseSqlite,
136 )],
137 Err(e) => {
138 warn!("Failed to parse RPM SQLite database {:?}: {}", path, e);
139 vec![default_package_data(
140 DatasourceId::RpmInstalledDatabaseSqlite,
141 )]
142 }
143 }
144 }
145}
146
147fn parse_rpm_database(
148 path: &Path,
149 datasource_id: DatasourceId,
150) -> Result<Vec<PackageData>, String> {
151 let metadata = std::fs::metadata(path)
152 .map_err(|e| format!("Cannot stat RPM database file {:?}: {}", path, e))?;
153
154 if metadata.len() > MAX_MANIFEST_SIZE {
155 return Err(format!(
156 "RPM database file {:?} is {} bytes, exceeding the {} byte limit",
157 path,
158 metadata.len(),
159 MAX_MANIFEST_SIZE
160 ));
161 }
162
163 let native_kind = native_kind_for_datasource(datasource_id)?;
164 match read_installed_rpm_packages(path, native_kind) {
165 Ok(packages) => Ok(packages
166 .into_iter()
167 .take(MAX_ITERATION_COUNT)
168 .map(native_package_to_query_package)
169 .map(|pkg| build_package_data(pkg, datasource_id))
170 .collect()),
171 Err(native_error) => Err(format!(
172 "native installed RPM reader failed for {:?}: {}",
173 path, native_error
174 )),
175 }
176}
177
178fn path_matches_suffix(path: &Path, suffix: &str) -> bool {
179 path.to_string_lossy().replace('\\', "/").ends_with(suffix)
180}
181
182fn path_matches_any_suffix(path: &Path, suffixes: &[&str]) -> bool {
183 suffixes
184 .iter()
185 .any(|suffix| path_matches_suffix(path, suffix))
186}
187
188fn native_kind_for_datasource(datasource_id: DatasourceId) -> Result<InstalledRpmDbKind, String> {
189 match datasource_id {
190 DatasourceId::RpmInstalledDatabaseBdb => Ok(InstalledRpmDbKind::Bdb),
191 DatasourceId::RpmInstalledDatabaseNdb => Ok(InstalledRpmDbKind::Ndb),
192 DatasourceId::RpmInstalledDatabaseSqlite => Ok(InstalledRpmDbKind::Sqlite),
193 other => Err(format!(
194 "unexpected datasource for installed RPM DB: {other:?}"
195 )),
196 }
197}
198
199fn native_package_to_query_package(package: InstalledRpmPackage) -> RpmQueryPackage {
200 RpmQueryPackage {
201 name: truncate_optional_string(Some(package.name)),
202 epoch: Some(package.epoch.to_string()),
203 version: truncate_optional_string(Some(package.version)),
204 release: truncate_optional_string(Some(package.release)),
205 vendor: truncate_optional_string(Some(package.vendor)),
206 distribution: truncate_optional_string(Some(package.distribution)),
207 arch: truncate_optional_string(Some(package.arch)),
208 platform: truncate_optional_string(Some(package.platform)),
209 size: (package.size > 0).then_some(u64::from(package.size)),
210 license: truncate_optional_string(Some(package.license)),
211 source_rpm: truncate_optional_string(Some(package.source_rpm)),
212 requires: package
213 .requires
214 .into_iter()
215 .take(MAX_ITERATION_COUNT)
216 .map(truncate_field)
217 .collect(),
218 file_names: package
219 .file_names
220 .into_iter()
221 .take(MAX_ITERATION_COUNT)
222 .map(|s| Some(truncate_field(s)))
223 .collect(),
224 dir_indexes: package.dir_indexes,
225 base_names: package
226 .base_names
227 .into_iter()
228 .take(MAX_ITERATION_COUNT)
229 .map(|s| Some(truncate_field(s)))
230 .collect(),
231 dir_names: package
232 .dir_names
233 .into_iter()
234 .take(MAX_ITERATION_COUNT)
235 .map(truncate_field)
236 .collect(),
237 }
238}
239
240fn truncate_optional_string(value: Option<String>) -> Option<String> {
241 value
242 .map(truncate_field)
243 .and_then(|v| normalize_optional_string(Some(v)))
244}
245
246fn build_evr_version(epoch: u32, version: &str, release: &str) -> Option<String> {
247 if version.is_empty() {
248 return None;
249 }
250
251 let mut evr = String::new();
252
253 if epoch > 0 {
254 evr.push_str(&format!("{}:", epoch));
255 }
256
257 evr.push_str(version);
258
259 if !release.is_empty() {
260 evr.push('-');
261 evr.push_str(release);
262 }
263
264 Some(evr)
265}
266
267fn build_file_references(
268 base_names: &[Option<String>],
269 dir_indexes: &[u32],
270 dir_names: &[String],
271) -> Vec<FileReference> {
272 if base_names.is_empty() || dir_names.is_empty() {
273 return Vec::new();
274 }
275
276 base_names
277 .iter()
278 .zip(dir_indexes.iter())
279 .take(MAX_ITERATION_COUNT)
280 .filter_map(|(basename, &dir_idx)| {
281 let dirname = dir_names.get(dir_idx as usize)?;
282 let basename = basename.as_deref().unwrap_or_default();
283 let path = format!("{}{}", dirname, basename);
284 if path.is_empty() || path == "/" {
285 return None;
286 }
287 Some(FileReference {
288 path,
289 size: None,
290 sha1: None,
291 md5: None,
292 sha256: None,
293 sha512: None,
294 extra_data: None,
295 })
296 })
297 .collect()
298}
299
300fn build_file_references_from_paths(paths: &[Option<String>]) -> Vec<FileReference> {
301 paths
302 .iter()
303 .take(MAX_ITERATION_COUNT)
304 .filter_map(|path| {
305 let path = path.as_deref()?.trim();
306 if path.is_empty() || path == "/" {
307 return None;
308 }
309
310 Some(FileReference {
311 path: path.to_string(),
312 size: None,
313 sha1: None,
314 md5: None,
315 sha256: None,
316 sha512: None,
317 extra_data: None,
318 })
319 })
320 .collect()
321}
322
323fn build_package_data(pkg: RpmQueryPackage, datasource_id: DatasourceId) -> PackageData {
324 let name = normalize_optional_string(pkg.name).map(truncate_field);
325 let version_raw = normalize_optional_string(pkg.version).map(truncate_field);
326 let release = normalize_optional_string(pkg.release).map(truncate_field);
327 let version = build_evr_version(
328 parse_epoch(pkg.epoch),
329 version_raw.as_deref().unwrap_or_default(),
330 release.as_deref().unwrap_or_default(),
331 );
332
333 let vendor = normalize_optional_string(pkg.vendor)
334 .map(truncate_field)
335 .or_else(|| normalize_optional_string(pkg.distribution).map(truncate_field));
336 let source_rpm = normalize_optional_string(pkg.source_rpm).map(truncate_field);
337 let namespace =
338 infer_rpm_namespace(None, vendor.as_deref(), release.as_deref(), None).or_else(|| {
339 source_rpm
340 .as_deref()
341 .and_then(|source_rpm| infer_rpm_namespace_from_filename(Path::new(source_rpm)))
342 });
343
344 let architecture = normalize_optional_string(pkg.arch)
345 .map(truncate_field)
346 .or_else(|| infer_platform_architecture(pkg.platform.as_deref()));
347 let dependencies = pkg
348 .requires
349 .into_iter()
350 .take(MAX_ITERATION_COUNT)
351 .filter_map(|require| build_dependency(&require))
352 .collect();
353 let extracted_license_statement = normalize_optional_string(pkg.license).map(truncate_field);
354 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
355 extracted_license_statement
356 .as_deref()
357 .and_then(normalize_rpm_declared_license)
358 .map(|normalized| {
359 build_declared_license_data(
360 normalized,
361 DeclaredLicenseMatchMetadata::single_line(
362 extracted_license_statement.as_deref().unwrap_or_default(),
363 ),
364 )
365 })
366 .map(|(expr, spdx, detections)| {
367 (
368 expr.map(truncate_field),
369 spdx.map(truncate_field),
370 detections,
371 )
372 })
373 .unwrap_or_else(empty_declared_license_data);
374 let source_packages = source_rpm.clone().into_iter().collect();
375 let file_references = {
376 let from_dir_components =
377 build_file_references(&pkg.base_names, &pkg.dir_indexes, &pkg.dir_names);
378 if from_dir_components.is_empty() {
379 build_file_references_from_paths(&pkg.file_names)
380 } else {
381 from_dir_components
382 }
383 };
384 let purl = build_package_purl(
385 name.as_deref(),
386 namespace.as_deref(),
387 version.as_deref(),
388 architecture.as_deref(),
389 );
390
391 PackageData {
392 datasource_id: Some(datasource_id),
393 package_type: Some(PACKAGE_TYPE),
394 namespace,
395 name,
396 version,
397 qualifiers: architecture.as_ref().map(|arch| {
398 let mut q = std::collections::HashMap::new();
399 q.insert("arch".to_string(), arch.clone());
400 q
401 }),
402 subpath: None,
403 primary_language: None,
404 description: None,
405 release_date: None,
406 parties: Vec::new(),
407 keywords: Vec::new(),
408 homepage_url: None,
409 download_url: None,
410 size: pkg.size.filter(|size| *size > 0),
411 sha1: None,
412 md5: None,
413 sha256: None,
414 sha512: None,
415 bug_tracking_url: None,
416 code_view_url: None,
417 vcs_url: None,
418 copyright: None,
419 holder: None,
420 declared_license_expression,
421 declared_license_expression_spdx,
422 license_detections,
423 other_license_expression: None,
424 other_license_expression_spdx: None,
425 other_license_detections: Vec::new(),
426 extracted_license_statement,
427 notice_text: None,
428 source_packages,
429 file_references,
430 is_private: false,
431 is_virtual: false,
432 extra_data: None,
433 dependencies,
434 repository_homepage_url: None,
435 repository_download_url: None,
436 api_data_url: None,
437 purl,
438 }
439}
440
441fn build_dependency(require: &str) -> Option<Dependency> {
442 let require = require.trim();
443 if require.is_empty() || require.starts_with("rpmlib(") || require.starts_with("config(") {
444 return None;
445 }
446
447 let purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), require)
448 .ok()
449 .map(|p| p.to_string());
450
451 Some(Dependency {
452 purl,
453 extracted_requirement: None,
454 scope: Some("requires".to_string()),
455 is_runtime: Some(true),
456 is_optional: Some(false),
457 is_pinned: Some(false),
458 is_direct: Some(true),
459 resolved_package: None,
460 extra_data: None,
461 })
462}
463
464fn build_package_purl(
465 name: Option<&str>,
466 namespace: Option<&str>,
467 version: Option<&str>,
468 arch: Option<&str>,
469) -> Option<String> {
470 let name = name?;
471 let mut purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
472
473 if let Some(namespace) = namespace {
474 purl.with_namespace(namespace).ok()?;
475 }
476
477 if let Some(version) = version {
478 purl.with_version(version).ok()?;
479 }
480
481 if let Some(arch) = arch {
482 purl.add_qualifier("arch", arch).ok()?;
483 }
484
485 Some(purl.to_string())
486}
487
488fn normalize_optional_string(value: Option<String>) -> Option<String> {
489 value.and_then(|value| {
490 let trimmed = value.trim();
491 if trimmed.is_empty() || trimmed == "(none)" || trimmed == "[]" {
492 None
493 } else {
494 Some(trimmed.to_string())
495 }
496 })
497}
498
499fn parse_epoch(value: Option<String>) -> u32 {
500 normalize_optional_string(value)
501 .and_then(|value| value.parse::<u32>().ok())
502 .unwrap_or(0)
503}
504
505fn infer_platform_architecture(platform: Option<&str>) -> Option<String> {
506 let platform = platform?.trim();
507 if platform.is_empty() {
508 return None;
509 }
510
511 platform
512 .split_once('-')
513 .map(|(arch, _)| arch)
514 .filter(|arch| !arch.is_empty())
515 .map(|arch| arch.to_string())
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 use crate::models::DatasourceId;
523 use std::path::PathBuf;
524
525 #[test]
526 fn test_bdb_parser_is_match() {
527 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
528 "/var/lib/rpm/Packages"
529 )));
530 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
531 "rootfs/var/lib/rpm/Packages"
532 )));
533 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
534 "/usr/lib/sysimage/rpm/Packages"
535 )));
536 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
537 "/var/lib/rpm/Packages.db"
538 )));
539 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
540 "lib/modules/datasource/deb/__fixtures__/Packages"
541 )));
542 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from("Packages")));
543 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
544 "testdata/rpm/var/lib/rpm/Packages.expected.json"
545 )));
546 }
547
548 #[test]
549 fn test_ndb_parser_is_match() {
550 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
551 "usr/lib/sysimage/rpm/Packages.db"
552 )));
553 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
554 "/rootfs/usr/lib/sysimage/rpm/Packages.db"
555 )));
556 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
557 "usr/lib/rpm/Packages"
558 )));
559 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
560 "var/lib/rpm/Packages.db"
561 )));
562 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
563 "testdata/rpm/usr/lib/sysimage/rpm/Packages.db.expected.json"
564 )));
565 }
566
567 #[cfg(feature = "rpm-sqlite")]
568 #[test]
569 fn test_sqlite_parser_is_match() {
570 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
571 "var/lib/rpm/rpmdb.sqlite"
572 )));
573 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
574 "/rootfs/var/lib/rpm/rpmdb.sqlite"
575 )));
576 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
577 "/rootfs/usr/lib/sysimage/rpm/rpmdb.sqlite"
578 )));
579 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
580 "/var/lib/rpm/Packages"
581 )));
582 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
583 "testdata/rpm/rpmdb.sqlite.expected.json"
584 )));
585 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
586 "testdata/rpm/rpmdb.sqlite-shm"
587 )));
588 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
589 "testdata/rpm/rpmdb.sqlite-wal"
590 )));
591 }
592
593 #[test]
594 fn test_build_evr_version_full() {
595 assert_eq!(
596 build_evr_version(2, "1.0.0", "1.el7"),
597 Some("2:1.0.0-1.el7".to_string())
598 );
599 }
600
601 #[test]
602 fn test_build_evr_version_no_epoch() {
603 assert_eq!(
604 build_evr_version(0, "1.0.0", "1.el7"),
605 Some("1.0.0-1.el7".to_string())
606 );
607 }
608
609 #[test]
610 fn test_build_evr_version_no_release() {
611 assert_eq!(build_evr_version(0, "1.0.0", ""), Some("1.0.0".to_string()));
612 }
613
614 #[test]
615 fn test_build_evr_version_empty() {
616 assert_eq!(build_evr_version(0, "", ""), None);
617 }
618
619 #[cfg(feature = "rpm-sqlite")]
620 #[test]
621 fn test_parse_rpm_database_sqlite() {
622 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
623
624 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
625
626 assert_eq!(pkg.package_type, Some(PackageType::Rpm));
627 assert_eq!(
628 pkg.datasource_id,
629 Some(DatasourceId::RpmInstalledDatabaseSqlite)
630 );
631 assert!(pkg.name.is_some());
632 }
633
634 #[cfg(feature = "rpm-sqlite")]
635 #[test]
636 fn test_parse_rpm_database_sqlite_preserves_release_in_version() {
637 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
638
639 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
640
641 assert!(
642 pkg.version
643 .as_ref()
644 .is_some_and(|version| version.contains('-'))
645 );
646 }
647
648 #[test]
649 fn test_build_file_references_skips_invalid_entries() {
650 let file_refs = build_file_references(
651 &[
652 Some("valid".to_string()),
653 Some("".to_string()),
654 Some("ignored".to_string()),
655 ],
656 &[0, 0, u32::MAX],
657 &["/usr/bin/".to_string()],
658 );
659
660 assert_eq!(file_refs.len(), 2);
661 assert_eq!(file_refs[0].path, "/usr/bin/valid");
662 assert_eq!(file_refs[1].path, "/usr/bin/");
663 }
664
665 #[test]
666 fn test_build_package_data_falls_back_to_file_names() {
667 let package = build_package_data(
668 RpmQueryPackage {
669 name: Some("libgcc".to_string()),
670 epoch: None,
671 version: Some("13.1.1".to_string()),
672 release: Some("2.fc38".to_string()),
673 vendor: Some("Fedora Project".to_string()),
674 distribution: None,
675 arch: Some("x86_64".to_string()),
676 platform: None,
677 size: Some(235748),
678 license: Some("GPLv3+".to_string()),
679 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
680 requires: Vec::new(),
681 file_names: vec![
682 Some("/usr/share/licenses/libgcc/COPYING".to_string()),
683 Some("/usr/share/licenses/libgcc/COPYING.RUNTIME".to_string()),
684 ],
685 dir_indexes: Vec::new(),
686 base_names: Vec::new(),
687 dir_names: Vec::new(),
688 },
689 DatasourceId::RpmInstalledDatabaseSqlite,
690 );
691
692 assert_eq!(package.file_references.len(), 2);
693 assert_eq!(
694 package.file_references[0].path,
695 "/usr/share/licenses/libgcc/COPYING"
696 );
697 assert_eq!(
698 package.file_references[1].path,
699 "/usr/share/licenses/libgcc/COPYING.RUNTIME"
700 );
701 }
702
703 #[test]
704 fn test_build_package_data_uses_distribution_for_namespace() {
705 let package = build_package_data(
706 RpmQueryPackage {
707 name: Some("libgcc".to_string()),
708 epoch: None,
709 version: Some("13.1.1".to_string()),
710 release: Some("2.fc38".to_string()),
711 vendor: None,
712 distribution: Some("Fedora Project".to_string()),
713 arch: Some("x86_64".to_string()),
714 platform: None,
715 size: Some(235748),
716 license: Some("GPLv3+".to_string()),
717 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
718 requires: Vec::new(),
719 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
720 dir_indexes: Vec::new(),
721 base_names: Vec::new(),
722 dir_names: Vec::new(),
723 },
724 DatasourceId::RpmInstalledDatabaseSqlite,
725 );
726
727 assert_eq!(package.namespace.as_deref(), Some("fedora"));
728 }
729
730 #[test]
731 fn test_build_package_data_normalizes_declared_license_expression() {
732 let package = build_package_data(
733 RpmQueryPackage {
734 name: Some("libgcc".to_string()),
735 epoch: None,
736 version: Some("13.1.1".to_string()),
737 release: Some("2.fc38".to_string()),
738 vendor: Some("Fedora Project".to_string()),
739 distribution: None,
740 arch: Some("x86_64".to_string()),
741 platform: None,
742 size: Some(235748),
743 license: Some("LGPLv2".to_string()),
744 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
745 requires: Vec::new(),
746 file_names: Vec::new(),
747 dir_indexes: Vec::new(),
748 base_names: Vec::new(),
749 dir_names: Vec::new(),
750 },
751 DatasourceId::RpmInstalledDatabaseSqlite,
752 );
753
754 assert_eq!(
755 package.declared_license_expression.as_deref(),
756 Some("lgpl-2.0-only")
757 );
758 assert_eq!(
759 package.declared_license_expression_spdx.as_deref(),
760 Some("LGPL-2.0-only")
761 );
762 assert_eq!(package.license_detections.len(), 1);
763 }
764
765 #[test]
766 fn test_build_package_data_uses_source_rpm_for_namespace() {
767 let package = build_package_data(
768 RpmQueryPackage {
769 name: Some("libgcc".to_string()),
770 epoch: None,
771 version: Some("13.1.1".to_string()),
772 release: None,
773 vendor: None,
774 distribution: None,
775 arch: Some("x86_64".to_string()),
776 platform: None,
777 size: Some(235748),
778 license: Some("GPLv3+".to_string()),
779 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
780 requires: Vec::new(),
781 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
782 dir_indexes: Vec::new(),
783 base_names: Vec::new(),
784 dir_names: Vec::new(),
785 },
786 DatasourceId::RpmInstalledDatabaseSqlite,
787 );
788
789 assert_eq!(package.namespace.as_deref(), Some("fedora"));
790 }
791
792 #[test]
793 fn test_build_package_data_uses_platform_for_architecture() {
794 let package = build_package_data(
795 RpmQueryPackage {
796 name: Some("libgcc".to_string()),
797 epoch: None,
798 version: Some("13.1.1".to_string()),
799 release: None,
800 vendor: None,
801 distribution: None,
802 arch: None,
803 platform: Some("x86_64-redhat-linux".to_string()),
804 size: Some(235748),
805 license: Some("GPLv3+".to_string()),
806 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
807 requires: Vec::new(),
808 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
809 dir_indexes: Vec::new(),
810 base_names: Vec::new(),
811 dir_names: Vec::new(),
812 },
813 DatasourceId::RpmInstalledDatabaseSqlite,
814 );
815
816 assert_eq!(
817 package.qualifiers.as_ref().and_then(|q| q.get("arch")),
818 Some(&"x86_64".to_string())
819 );
820 }
821}
822
823#[cfg(feature = "rpm-sqlite")]
824crate::register_parser!(
825 "RPM installed package database",
826 &[
827 "**/var/lib/rpm/Packages",
828 "**/usr/lib/sysimage/rpm/Packages",
829 "**/var/lib/rpm/Packages.db",
830 "**/usr/lib/sysimage/rpm/Packages.db",
831 "**/var/lib/rpm/rpmdb.sqlite",
832 "**/usr/lib/sysimage/rpm/rpmdb.sqlite"
833 ],
834 "rpm",
835 "",
836 Some("https://rpm.org/"),
837);
838
839#[cfg(not(feature = "rpm-sqlite"))]
840crate::register_parser!(
841 "RPM installed package database",
842 &[
843 "**/var/lib/rpm/Packages",
844 "**/usr/lib/sysimage/rpm/Packages",
845 "**/var/lib/rpm/Packages.db",
846 "**/usr/lib/sysimage/rpm/Packages.db"
847 ],
848 "rpm",
849 "",
850 Some("https://rpm.org/"),
851);