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