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