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