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